From b49965beb0c4992325964f3d4365a86eda9baf29 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 11 Jul 2024 02:11:56 -0300 Subject: [PATCH 01/82] UserModels: multi-version and tweaked wrappers for backwards compatibility; examples include CppIndMach012. Also tested with Oddie. --- .../UserModels/PyIndMach012/README.ipynb | 757 ++++-------------- dss/UserModels/wrappers.py | 55 +- 2 files changed, 216 insertions(+), 596 deletions(-) diff --git a/docs/examples/UserModels/PyIndMach012/README.ipynb b/docs/examples/UserModels/PyIndMach012/README.ipynb index 1f16a127..6acde216 100644 --- a/docs/examples/UserModels/PyIndMach012/README.ipynb +++ b/docs/examples/UserModels/PyIndMach012/README.ipynb @@ -7,11 +7,17 @@ "source": [ "# PyIndMach012: an example of user-model using DSS-Python\n", "\n", + "*Now also contains example usage of a C++ user-model, CppIndMach012*\n", + "\n", "This example runs a modified example from the OpenDSS distribution for the induction machine model with a sample PyIndMach012 implementation, written in Python, and the original, built-in IndMach012.\n", "\n", - "Check the `PyIndMach012.py` file for more comments. Comparing it to [the Pascal code for IndMach012](https://github.com/dss-extensions/dss_capi/blob/master/src/PCElements/IndMach012.pas) can be useful to understand some of the inner workings of OpenDSS.\n", + "Check the file named `PyIndMach012.py` for more comments. Comparing it to [the Pascal code for IndMach012](https://github.com/dss-extensions/dss_capi/blob/master/src/PCElements/IndMach012.pas) can be useful to understand some of the inner workings of OpenDSS.\n", + "\n", + "The user-model code in DSS-Python was supposed to grow other features, but the effort hasn't been continued for many reasons. The code for generator user-models has been stable for many releases. We believe it can be used to develop new ideas before committing the final model in a traditional DLL user-model. Many users opt to use Delphi to build user-models since the original OpenDSS code uses Delphi and there is the IndMach012a user-model example in the codebase.\n", "\n", - "The user-model code in DSS-Python was supposed to grow other features, but the effort hasn't been continued for many reasons. The code for generator user-models has been stable for many releases. We believe it can be used to develop new ideas before committing the final model in a traditional DLL user-model." + "Acquiring and installing Delphi, and learning Object Pascal can be extra hurdles. In 2024, to also illustrate an alternative approach using C++ instead of Python or Delphi, a working example (\"CppIndMach012\") was added to the AltDSS/DSS C-API repository. Note that the basic C headers were added back in 2018. \"CppIndMach012\" is prepared to build DLLs for multiple DSS engines, including AltDSS and various versions of OpenDSS, since the DLLs are not backward/forward compatible across versions. The CppIndMach012 code is available at https://github.com/dss-extensions/dss_capi/tree/master/examples/UserModels\n", + "\n", + "If either DSS-Python, PyIndMach012 or CppIndMach012 was useful for your research, please cite the relevant work. All code here is available according to the BSD-3 licenses listed on the main DSS C-API and DSS-Python repositories, also listed at https://dss-extensions.org/licenses" ] }, { @@ -22,27 +28,94 @@ "The outputs for this notebook should be listed below, but you can also open and then run this notebook on Google Colab for a quick overview if you don't want to set up a local environment: **[Open in Colab](https://colab.research.google.com/github/dss-extensions/dss_python/blob/master/docs/examples/UserModels/PyIndMach012/README.ipynb)**. The next cell installs DSS-Python and dependencies, and downloads the extra files." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "\n", + "## \"Do I need a user-model?\"\n", + "\n", + "Since the DSS engine can be interfaced and integrated through several means, user-models is just one of the alternatives.\n", + "\n", + "If you don't need the features supported by user-models, you could consider writing a custom solution loop, for example.\n", + "\n", + "## Changes\n", + "\n", + "- 2024-07: \n", + " - `GenUserModel.dll_path` became `GenUserModel(DSS).dll_path`. Pass the target DSS instance and it will try to select the appropriate DLL according to the DSS version. It should support AltDSS (the engine from DSS-Extensions), OpenDSS versions 7, 8, 9 and 10.\n", + " - When running through Google Colab, the user-model `CppIndMach012` is added to the comparison. A few commands are executed to install the required packages and build the shared objects (DLLs).\n", + " - On Windows: the examples work with our own engine (AltDSS), the official OpenDSS COM DLL, and the official OpenDSSDirect.DLL/DCSL implementation (using our Oddie project in DSS-Python).\n", + "----" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*The following cell will run when running on Google Colab.*" + ] + }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os, subprocess\n", + "has_cpp_model = False\n", "if os.getenv(\"COLAB_RELEASE_TAG\"):\n", + " # Install DSS-Python, download the code for Python user-model, and the sample .DSS script\n", " import urllib.request\n", " print(subprocess.check_output('pip install dss-python[plot]', shell=True).decode())\n", " urllib.request.urlretrieve(\"https://raw.githubusercontent.com/dss-extensions/dss_python/master/docs/examples/UserModels/PyIndMach012/PyIndMach012.py\", \"PyIndMach012.py\")\n", - " urllib.request.urlretrieve(\"https://raw.githubusercontent.com/dss-extensions/dss_python/master/docs/examples/UserModels/PyIndMach012/master.dss\", \"master.dss\")\n" + " urllib.request.urlretrieve(\"https://raw.githubusercontent.com/dss-extensions/dss_python/master/docs/examples/UserModels/PyIndMach012/master.dss\", \"master.dss\")\n", + "\n", + " # Try building the C++ user-model too\n", + " try:\n", + " print(subprocess.check_output('git clone --depth=1 --quiet https://github.com/dss-extensions/dss_capi', shell=True).decode())\n", + " print(subprocess.check_output('apt-get -qq update', stderr=subprocess.STDOUT, shell=True).decode())\n", + " print(subprocess.check_output('apt-get -qq install -y libeigen3-dev', stderr=subprocess.STDOUT, shell=True).decode())\n", + " print(subprocess.check_output('cmake dss_capi/examples/UserModels -B build -DCMAKE_BUILD_TYPE=Release', shell=True).decode())\n", + " print(subprocess.check_output('cmake --build build --config=Release', stderr=subprocess.STDOUT, shell=True).decode())\n", + " has_cpp_model = True\n", + " cpp_model_dir = \"build\"\n", + " except subprocess.CalledProcessError as exc:\n", + " print(\"Failed to prepare CppIndMach012:\", exc.output) \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*When running on a local Python installation, it could be easier to build externally and set the path like in the commented code in the next cell.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# has_cpp_model = True\n", + "# cpp_model_dir = \"/tmp/dss_capi/examples/UserModels/build\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*General imports for the example itself*" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import os\n", + "import os, sys\n", + "from time import perf_counter\n", "import numpy as np\n", "from matplotlib import pyplot as plt\n", "from dss.UserModels import GenUserModel # used to get the DLL path\n", @@ -61,405 +134,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Run `??PyIndMach012` to see the code of the class, or open `PyIndMach012.py` in an editor." + "Run `??PyIndMach012` to see the code of the class, or open `PyIndMach012.py` in an editor. On Colab, JupyterLab and other similar environments, the file should be listed in the sidebar to the left." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "'''\n", - "A `dss_python` User-Model implementation of the IndMach012 Generator model from OpenDSS.\n", - "\n", - "Based on the following files from the official OpenDSS source code:\n", - "- Source/PCElements/IndMach012.pas\n", - "- Source/IndMach012a/IndMach012Model.pas\n", - "\n", - "This Python version was written by Paulo Meira. \n", - "Original code by EPRI, licensed under the 3-clause BSD. See OPENDSS_LICENSE.\n", - "\n", - "This sample code doesn't interact with the main OpenDSS interface directly,\n", - "it only uses the user-model interface. Thus, it is compatible with the official OpenDSS\n", - "distribution as well as DSS-Python. Note that OpenDSS version 7 has a bug on 64-bit \n", - "systems and user-models most likely won't run via COM.\n", - "\n", - "Recent version of OpenDSS 8 also present a bug when handling the editing of \n", - "user-model parameters after the creation of the generator. You can, of course,\n", - "edit the data in Python if you desire.\n", - "\n", - "'''\n", - "from dss.UserModels import GenUserModel\n", - "from dss.enums import SolveModes\n", - "import numpy as np\n", - "\n", - "# This is the user-model we'll use as a base\n", - "Base = GenUserModel.Base\n", - "\n", - "# Symmetrical component transformation matrices\n", - "a = np.exp(1j * 2 * np.pi/3)\n", - "aa = np.exp(1j * 4 * np.pi/3)\n", - "\n", - "Ap2s = np.array([\n", - " [1, 1, 1], \n", - " [1, a, aa],\n", - " [1, aa, a]\n", - "]) / 3.0\n", - "\n", - "As2p = np.array([\n", - " [1, 1, 1], \n", - " [1, aa, a],\n", - " [1, a, aa]\n", - "])\n", - "\n", - "\n", - "@GenUserModel.register # The class needs to be registered\n", - "class PyIndMach012(Base):\n", - " '''\n", - " A Python User-Model implementation of the IndMach012 Generator model for OpenDSS \n", - " '''\n", - " def __init__(self, gen, dyn, callbacks):\n", - " '''\n", - " Initialize the model object instance\n", - " Note: OpenDSS calls `Init` from the UserModel DLL later,\n", - " which calls our `init_state_vars`\n", - " '''\n", - " \n", - " Base.__init__(self, gen, dyn, callbacks)\n", - " \n", - " # You can list the DSS model inputs and default values like this.\n", - " # The UserModel wrapper will create attributes in the instance with\n", - " # the default values and update them later when the UserModel\n", - " # `Edit` function is called.\n", - " self.add_inputs(\n", - " ('H', 0.02),\n", - " ('D', 0.02),\n", - " ('puRs', 0.0053),\n", - " ('puXs', 0.106),\n", - " ('puRr', 0.007),\n", - " ('puXr', 0.12),\n", - " ('puXm', 4.0),\n", - " ('slip', 0.007),\n", - " ('MaxSlip', 0.1),\n", - " ('slipOption', 'variable')\n", - " )\n", - " \n", - " # The outputs can be any variable or Python property, i.e. it can be an \n", - " # input, state variable, property, etc., as long as it is available in\n", - " # the model class\n", - " self.add_outputs(\n", - " 'Slip', # The current slip (`slip` is the DSS input param)\n", - " \n", - " # There don't need to be in the output (they're constant) but are listed \n", - " # in IndMach012.pas -- most likely to debug\n", - " 'puRs',\n", - " 'puXs',\n", - " 'puRr',\n", - " 'puXr',\n", - " 'puXm',\n", - " 'MaxSlip',\n", - " \n", - " # complex variables like these are exported as their absolute values\n", - " 'Is1', \n", - " 'Is2',\n", - " 'Ir1',\n", - " 'Ir2',\n", - " \n", - " # Some properties to mimic the Pascal version\n", - " 'E1_pu',\n", - " 'StatorLosses',\n", - " 'RotorLosses',\n", - " 'ShaftPower_hp',\n", - " 'PowerFactor',\n", - " 'Efficiency_pct'\n", - " )\n", - " \n", - " # These are the state variables. DSS-Python will automatically\n", - " # setup auxiliary variables such as dE1_dt, dE1_dtn, E1n used in\n", - " # the solution process\n", - " self.add_state_vars(\n", - " 'E1', 'E2'\n", - " )\n", - " \n", - " # For some advanced usage, we need some CFFI code.\n", - " # We plan to add a simple wrapper to the callback interface.\n", - " # While writing this, I noticed that there were some changes in \n", - " # OpenDSS version 8 that introduce an extra ActorID parameter in \n", - " # many of the callback functions. This means that we cannot write\n", - " # a user-model that is compatible with both versions. \n", - " # An issue ticket was created to track this at:\n", - " # \n", - " # self.char_buffer = self.ffi.new('char[1024]')\n", - " # self.callbacks.GetActiveElementName(self.char_buffer, 1024)\n", - " # self.element_name = self.ffi.string(self.char_buffer)#.decode('ascii')\n", - " \n", - " # This one is used for the Power property, left as an example\n", - " # self.double_buffer = self.ffi.new('double[2]')\n", - "\n", - " # Update other variables that depend on the input parameters\n", - " self.update()\n", - "\n", - "\n", - " def init_state_vars(self, Vabc, Iabc):\n", - " '''\n", - " Initialize state variables (dynamics mode), equivalent to\n", - " TIndMach012Obj.InitStateVars\n", - " '''\n", - "\n", - " V012 = np.dot(Ap2s, Vabc)\n", - " I012 = np.dot(Ap2s, Iabc)\n", - " \n", - " # The following is done in TIndMach012Obj.InitModel:\n", - " # Compute Voltage behind transient reactance and set derivatives to zero\n", - " self.E1 = V012[1] - I012[1] * self.Zsp\n", - " self.dE1dt = 0\n", - " self.E2 = V012[2] - I012[2] * self.Zsp\n", - " self.dE2dt = 0\n", - "\n", - " # Copy the current state to the previous state\n", - " self.copy_state()\n", - " \n", - " # Initial rotor speed\n", - " self.gen.Speed = -self.S1 * self.gen.w0\n", - " self.gen.dSpeed = 0\n", - " self.gen.Theta = np.angle(self.E1) # overwrite Theta\n", - " self.gen.dTheta = 0\n", - "\n", - "\n", - " def integrate(self):\n", - " '''\n", - " Equivalent to TIndMach012Obj.Integrate\n", - " '''\n", - " if self.dyn.IterationFlag == 0: \n", - " # First iteration of new time step, copy the previous state\n", - " # to be used in the integration process\n", - " self.copy_state()\n", - " \n", - " # Some copies to reduce `self.` spam\n", - " gen = self.gen\n", - " w0 = gen.w0\n", - " S1, S2 = self.S1, self.S2\n", - " E1, E2 = self.E1, self.E2\n", - " Is1, Is2 = self.Is1, self.Is2\n", - " T0p = self.T0p\n", - " Xopen, Xp = self.Xopen, self.Xp\n", - " \n", - " # Derivative of E\n", - " self.dE1_dt = (1j * -w0 * S1 * E1) - ((E1 - 1j * (Xopen - Xp) * Is1) / T0p)\n", - " self.dE2_dt = (1j * -w0 * S2 * E2) - ((E2 - 1j * (Xopen - Xp) * Is2) / T0p)\n", - " \n", - " # Trapezoidal Integration\n", - " Base.integrate(self)\n", - " \n", - " \n", - " def update(self): \n", - " '''\n", - " Propagate changes from the input parameters to the model.\n", - " \n", - " Equivalent to part of TIndMach012Obj.RecalcElementData\n", - " '''\n", - " \n", - " gen = self.gen\n", - " \n", - " self._set_local_slip(self.slip)\n", - " \n", - " # make generator speed agree\n", - " gen.Speed = -self.S1 * self.gen.w0\n", - " self.gen.dSpeed = 0.0\n", - " \n", - " self.fixed_slip = (self.slipOption) and (self.slipOption[0].upper() == 'F')\n", - " self.first_iteration = True\n", - "\n", - " ZBase = 1000.0 * (gen.kVGeneratorBase**2 / gen.kVArating)\n", - " \n", - " Rs = self.puRs * ZBase\n", - " Xs = self.puXs * ZBase\n", - " Rr = self.puRr * ZBase\n", - " Xr = self.puXr * ZBase\n", - " Xm = self.puXm * ZBase\n", - " \n", - " self.Zs = complex(Rs, Xs)\n", - " self.Zm = complex(0, Xm)\n", - " self.Zr = complex(Rr, Xr)\n", - " \n", - " self.Xopen = Xs + Xm\n", - " self.Xp = Xs + (Xr * Xm) / (Xr + Xm)\n", - " self.Zsp = complex(Rs, self.Xp)\n", - " self.T0p = (Xr + Xm) / (gen.w0 * Rr)\n", - " # self.Zrsc = self.Zr + (self.Zs * self.Zm) / (self.Zs + selfg.Zm)\n", - " \n", - " # Init dSdP based on rated slip and rated voltage\n", - " self.V1 = complex(gen.kVGeneratorBase * 1000.0/np.sqrt(3))\n", - " if self.S1 != 0:\n", - " self.Is1, self.Ir1 = self._pfmodel_current(self.V1, self.S1)\n", - " \n", - " self.dSdP = self.S1/(self.V1 * np.conjugate(self.Is1)).real\n", - " \n", - " self.Is1 = complex(0)\n", - " self.V1 = complex(0)\n", - " self.Is2 = complex(0)\n", - " self.V2 = complex(0)\n", - "\n", - "\n", - " def calc(self, Vabc, Iabc):\n", - " '''\n", - " Calculate the new model state. Vabc is used as an\n", - " input, while Iabc is the ouput used in OpenDSS.\n", - " '''\n", - " \n", - " # The next version of DSS-Python should have an option to \n", - " # provide the values in 012 space to simplify the model code\n", - " V012 = np.dot(Ap2s, Vabc)\n", - " I012 = np.dot(Ap2s, Iabc)\n", - " \n", - " if self.dyn.SolutionMode == SolveModes.Dynamic:\n", - " self.calc_dynamic(V012, I012)\n", - " else:\n", - " self.calc_power_flow(V012, I012)\n", - "\n", - " Iabc[:] = iabc = np.dot(As2p, I012)\n", - " \n", - " # We can get the current total power here, or we can use \n", - " # the power property below \n", - " self.Power = sum(np.asarray(Vabc) * iabc.conj())\n", - "\n", - "\n", - " # @property\n", - " # def Power(self):\n", - " # '''\n", - " # This is an example of callback usage, returning the power of the \n", - " # element. Note that we don't really need this here since we can \n", - " # calculate the power in the calc function.\n", - " # '''\n", - " # cmd = b'select %s' % (self.element_name)\n", - " # self.callbacks.DoDSSCommand(cmd, len(cmd) + 1)\n", - " # self.callbacks.GetActiveElementPower(1, self.double_buffer)\n", - " # return complex(self.double_buffer[0], self.double_buffer[1])\n", - "\n", - "\n", - " def calc_dynamic(self, V012, I012):\n", - " '''Equivalent to TIndMach012Obj.CalcDynamic'''\n", - " \n", - " # In dynamics mode, slip is allowed to vary\n", - " \n", - " # Copy some values to local variables\n", - " V1, V2 = self.V1, self.V2 = V012[1], V012[2]\n", - " E1, E2 = self.E1, self.E2\n", - " Zsp, Zm = self.Zsp, self.Zm\n", - " \n", - " # Gets slip from shaft speed\n", - " self._set_local_slip(-self.gen.Speed / self.gen.w0)\n", - " \n", - " # The stator and rotor currents from the Pascal code are \n", - " # computed in TIndMach012Obj.Get_DynamicModelCurrent\n", - "\n", - " # Stator current\n", - " self.Is1 = (V1 - E1) / self.Zsp\n", - " self.Is2 = (V2 - E2) / self.Zsp\n", - "\n", - " # Rotor current\n", - " self.Ir1 = self.Is1 - (V1 - self.Is1 * Zsp) / Zm\n", - " self.Ir2 = self.Is2 - (V2 - self.Is2 * Zsp) / Zm \n", - " \n", - " I012[:] = complex(0.0, 0.0), self.Is1, self.Is2\n", - " \n", - "\n", - " def calc_power_flow(self, V012, I012):\n", - " '''Equivalent to TIndMach012Obj.CalcPFlow'''\n", - " \n", - " self.V1, self.V2 = V012[1], V012[2]\n", - " \n", - " if self.first_iteration:\n", - " # Initialize Is1\n", - " self.Is1, self.Ir1 = self._pfmodel_current(self.V1, self.S1)\n", - "\n", - "\n", - " # If fixed slip option set, then use the value set by the user\n", - " if not self.fixed_slip:\n", - " P_Error = self.gen.PNominalPerPhase - (self.V1 * self.Is1.conjugate()).real\n", - " \n", - " # make new guess at slip\n", - " self._set_local_slip(self.S1 + self.dSdP * P_Error)\n", - "\n", - " \n", - " self.Is1, self.Ir1 = self._pfmodel_current(self.V1, self.S1)\n", - " self.Is2, self.Ir2 = self._pfmodel_current(self.V2, self.S2)\n", - " \n", - " I012[:] = complex(0.0, 0.0), self.Is1, self.Is2\n", - "\n", - " \n", - " def _pfmodel_current(self, V, s, show=False):\n", - " '''Equivalent to TIndMach012Obj.Get_PFlowModelCurrent'''\n", - " \n", - " if s != 0.0:\n", - " RL = self.Zr.real * (1 - s) / s\n", - " else:\n", - " RL = self.Zr.real * 1.0e6\n", - " \n", - " Zrotor = RL + self.Zr\n", - " Zmotor = self.Zs + (Zrotor * self.Zm) / (Zrotor + self.Zm)\n", - " Istator = V / Zmotor\n", - " Irotor = Istator - (V - self.Zs * Istator) / self.Zm\n", - " \n", - " return Istator, Irotor\n", - "\n", - "\n", - " def _set_local_slip(self, value):\n", - " '''Equivalent to TIndMach012Obj.set_Localslip'''\n", - " \n", - " self.S1 = value\n", - " if self.dyn.SolutionMode != SolveModes.Dynamic:\n", - " # Put limits on the slip unless dynamics\n", - " if abs(self.S1) > self.MaxSlip:\n", - " self.S1 = np.sign(self.S1) * self.MaxSlip \n", - " \n", - " self.S2 = 2 - self.S1\n", - "\n", - " \n", - " # The following are properties to emulate the model outputs from \n", - " # the Pascal version of built-in IndMach012 \n", - " \n", - " @property\n", - " def E1_pu(self):\n", - " return np.sqrt(3) * abs(self.E1) / (1000 * self.gen.kVGeneratorBase)\n", - "\n", - " @property\n", - " def Slip(self):\n", - " return self.S1\n", - "\n", - " @property\n", - " def RotorLosses(self):\n", - " Ir1, Ir2, Zr = self.Ir1, self.Ir2, self.Zr\n", - " return 3 * (Ir1.real**2 + Ir1.imag**2 + Ir2.real**2 + Ir2.imag**2) * Zr.real\n", - " \n", - " @property\n", - " def StatorLosses(self):\n", - " Is1, Is2, Zs = self.Is1, self.Is2, self.Zs\n", - " return 3 * (Is1.real**2 + Is1.imag**2 + Is2.real**2 + Is2.imag**2) * Zs.real\n", - " \n", - " @property\n", - " def PowerFactor(self):\n", - " power = self.Power\n", - " return np.sign(power.imag) * power.real / abs(power)\n", - " \n", - " @property\n", - " def Efficiency_pct(self):\n", - " power = self.Power\n", - " return np.clip((1 - (self.StatorLosses + self.RotorLosses) / power.real) * 100, 0, 100)\n", - " \n", - " @property\n", - " def ShaftPower_hp(self):\n", - " Ir1, Ir2, Zr, S1, S2 = self.Ir1, self.Ir2, self.Zr, self.S1, self.S2\n", - " return (3.0/746) * (abs(Ir1)**2 * (1 - S1) / S1 + abs(Ir2)**2 * (1 - S2)/S2) * Zr.real\n", - "\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open('PyIndMach012.py', 'r') as f:\n", " src = f.read()\n", @@ -480,43 +162,45 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For this example, we can use either COM or DSS-Python (DSS C-API)." + "In this example, we can use a OpenDSS engine in three implementations. Set `DSS_ENGINE` accordingly (default is our AltDSS engine):\n", + "\n", + "- `ALT`: : Using DSS-Python, coupled with the AltDSS/DSS C-API engine from DSS-Extensions.\n", + "- `COM`: Using the official OpenDSS COM implementation; must be previously installed/registered. Here we use `comtypes`, which is currently the more performant alternative (win32com is slower in several functions).\n", + "- `ODD.DLL`: Using DSS-Python, coupled with the official (or a custom but compatible) `OpenDSSDirect.DLL` distribution (a.k.a. DCSL). If it is installed in `C:\\Program Files\\OpenDSS\\x64\\OpenDSSDirect.DLL`, it should be picked automatically (otherwise, adjust below). For this alternative, the DLL is wrapped using the AltDSS Oddie wrapper. More at https://github.com/dss-extensions/dss_capi/tree/master/src/altdss_oddie\n" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'DSS C-API Library version DEV revision UNKNOWN based on OpenDSS SVN UNKNOWN [FPC 3.2.2] (64-bit build) MVMULT INCREMENTAL_Y CONTEXT_API PM 20240209064406; License Status: Open \\nDSS-Python version: 0.15.0rc1'" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "original_dir = os.getcwd() # same the original working directory since the COM module messes with it\n", - "USE_COM = False # toggle this value to run with DSS C-API or COM\n", - "if USE_COM:\n", + "DSS_ENGINE = 'ALT'\n", + "DSS_ENGINE = 'COM'\n", + "DSS_ENGINE = 'ODD.DLL'\n", + "\n", + "original_dir = os.getcwd() # same the original working directory since the COM/ODD.DLL module messes with it\n", + "if DSS_ENGINE == 'COM':\n", " from dss import patch_dss_com\n", " import comtypes.client\n", " DSS = patch_dss_com(comtypes.client.CreateObject('OpenDSSengine.DSS'))\n", " DSS.DataPath = original_dir\n", + " DSS.AllowForms = False # disable progress form\n", " os.chdir(original_dir)\n", - "else:\n", + "elif DSS_ENGINE == 'ODD.DLL':\n", + " from dss import IOddieDSS\n", + " DSS = IOddieDSS()\n", + " os.chdir(original_dir)\n", + "elif DSS_ENGINE == 'ALT':\n", " from dss import DSS\n", - " \n", - "DSS.Version " + "\n", + "print(\"Engine selection:\", DSS_ENGINE)\n", + "print(DSS.Version)" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -540,17 +224,49 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def run(pymodel):\n", + "def run(model):\n", " Text.Command = 'redirect \"master.dss\"'\n", "\n", - " if pymodel:\n", + " if model == 'py':\n", " # This uses our custom user-model in Python\n", " Text.Command = 'New \"Generator.Motor1\" bus1=Bg2 kW=1200 conn=delta kVA=1500.000 H=6 model=6 kv=0.48 D=0 usermodel=\"{dll_path}\" userdata=(pymodel=PyIndMach012 purs=0.048 puxs=0.075 purr=0.018 puxr=0.12 puxm=3.8 slip=0.02 SlipOption=variableslip)'.format(\n", - " dll_path=GenUserModel.dll_path,\n", + " dll_path=GenUserModel(DSS).dll_path,\n", + " )\n", + " Text.Command = 'New \"Monitor.mfr2\" element=Generator.Motor1 terminal=1 mode=3'\n", + " elif model == 'cpp':\n", + " # Select the suffix according to the engine version we're running\n", + " ver = DSS.Version\n", + " if 'DSS C-API Library' in ver:\n", + " suffix = 'AltDSS'\n", + " elif ver.startswith('Version 9.') or ver.startswith('Version 8.'):\n", + " suffix = 'OpenDSSv8v9'\n", + " elif ver.startswith('Version 7.'):\n", + " suffix = 'OpenDSSv7'\n", + " else:\n", + " if not ver.startswith('Version 10.'):\n", + " print('Assuming compatibility with OpenDSS v10.0')\n", + "\n", + " suffix = 'OpenDSSv10'\n", + "\n", + " # Try to adjust the name according to common conventions\n", + " dll_ext = 'so' # default to .so\n", + " dll_prefix = 'lib'\n", + " if sys.platform == 'win32':\n", + " dll_ext = 'dll'\n", + " dll_prefix = ''\n", + " elif sys.platform == 'darwin':\n", + " dll_ext = 'dylib'\n", + " dll_prefix = 'lib'\n", + " \n", + " # This uses the custom user-model in C++.\n", + " # If you built the C++ model yourself, remember to update the file path\n", + " # as a whole (path and basename).\n", + " Text.Command = 'New \"Generator.Motor1\" bus1=Bg2 kW=1200 conn=delta kVA=1500.000 H=6 model=6 kv=0.48 D=0 usermodel=\"{dll_path}\" userdata=(purs=0.048 puxs=0.075 purr=0.018 puxr=0.12 puxm=3.8 slip=0.02 SlipOption=variableslip)'.format(\n", + " dll_path=f'{cpp_model_dir}/{dll_prefix}CppIndMach012_{suffix}.{dll_ext}',\n", " )\n", " Text.Command = 'New \"Monitor.mfr2\" element=Generator.Motor1 terminal=1 mode=3'\n", " else:\n", @@ -565,20 +281,23 @@ " Text.Command = 'Set mode=dynamics number=1 h=0.000166667'\n", " \n", " # And finally run 5000 steps for the dynamic simulation\n", - " Text.Command = f'Solve number=5000'\n", - " " + " t0 = perf_counter() \n", + " Text.Command = 'Solve number=5000'\n", + " print(model, perf_counter() - t0)" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# There are the channels from the Pascal/built-in IndMach012\n", + "# These are the channels from the Pascal/built-in IndMach012\n", "channels_pas = ('Frequency', 'Theta (deg)', 'E1', 'dSpeed (deg/sec)', 'dTheta (deg)', 'Slip', 'Is1', 'Is2', 'Ir1', 'Ir2', 'Stator Losses', 'Rotor Losses', 'Shaft Power (hp)', 'Power Factor', 'Efficiency (%)')\n", "\n", - "# There are the channels from the Python module -- we define part of these and part come from the generator model itself\n", + "# These are the channels from the Python module.\n", + "# We define part of these and part come from the generator model itself.\n", + "# If CppIndMach012 is available, it also uses the same channel names.\n", "channels_py = ('Frequency', 'Theta (Deg)', 'E1_pu', 'dSpeed (Deg/sec)', 'dTheta (Deg)', 'Slip', 'Is1', 'Is2', 'Ir1', 'Ir2', 'StatorLosses', 'RotorLosses', 'ShaftPower_hp', 'PowerFactor', 'Efficiency_pct')" ] }, @@ -595,25 +314,35 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's run the Pascal/built-in version of IndMach012 and our custom Python version for comparison:" + "Let's run the Pascal/built-in version of IndMach012 and our custom Python version for comparison. The `run` function also outputs the time it takes to run the 5000 time steps. As expected, the Python version has a lot of overhead, but it doesn't require a compiler and is easier to iterate. The C++ version of the user-model should result in a runtime closer to that of the internal IndMach012 model.\n", + "\n", + "The circuit being tested here is tiny. The performance of these alternatives in real-world scenarios will depend on the number of instances of user-model being used and how big and complicated the base circuit is. That is, be sure to evaluate these according to your needs." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "run(False)\n", + "run(model='built-in')\n", "Monitors.Name = 'mfr2'\n", "header = [x.strip() for x in Monitors.Header]\n", "outputs_pas = {channel: Monitors.Channel(header.index(channel) + 1) for channel in channels_pas}\n", "\n", - "run(True)\n", + "run(model='py')\n", "Monitors.Name = 'mfr2'\n", "header = [x.strip() for x in Monitors.Header]\n", "outputs_py = {channel: Monitors.Channel(header.index(channel) + 1) for channel in channels_py}\n", "\n", + "if has_cpp_model:\n", + " run(model='cpp')\n", + " Monitors.Name = 'mfr2'\n", + " header = [x.strip() for x in Monitors.Header]\n", + " outputs_cpp = {channel: Monitors.Channel(header.index(channel) + 1) for channel in channels_py}\n", + "else:\n", + " outputs_cpp = {}\n", + "\n", "time = np.arange(1, 5000 + 1) * 0.000166667\n", "offset = int(0.1 / 0.000166667)" ] @@ -638,165 +367,17 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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bGVlZdoLN9y/f18700kGKCsDnl4M4/59gDUlIrJqTz/vvsyC0pLgstN6y8nJQePGjS0dg4iIiIjILPLz8+Hv7//CMTYxY2FtPDw8AABXr16Fq6urhdPYvqcnw+fn52sPW6PqYz2Ni/U0PtbUuFhP42I9jY81NS5z11MIgQcPHuisFVcVNhbVIJNVnJri6urKXxAjcnFxYT2NiPU0LtbT+FhT42I9jYv1ND7W1LjMWc+X/Ue6TZy8TURERERE1o2NBRERERERGYyNRTXY29tj7ty5XNvCSFhP42I9jYv1ND7W1LhYT+NiPY2PNTUua64nrwpFREREREQG44wFEREREREZjI0FEREREREZjI0FEREREREZjI1FJVatWoWGDRvCwcEBISEhSEtLe+H41NRUhISEwMHBAY0aNcKaNWvMlNR26FPTgoICREVFITAwEDKZDFOmTDFfUBuhTz13796NN954A97e3nBxcUHnzp3x/fffmzGt9dOnnunp6QgLC4OnpyccHR0RFBSE5cuXmzGtbdD3ffSpI0eOQKFQoG3btqYNaGP0qeehQ4cgSdJz288//2zGxNZN39dnaWkpZs+ejYCAANjb26Nx48bYtGmTmdJaP33qGR0dXenrMzg42IyJrZ++r9GkpCS0adMGTk5O8PPzw3vvvYc7d+6YKe0zBOnYvn27UCqVYv369SIrK0vExsYKZ2dnceXKlUrH5+TkCCcnJxEbGyuysrLE+vXrhVKpFDt37jRzcuulb01zc3PF5MmTxd/+9jfRtm1bERsba97AVk7fesbGxorFixeLkydPiuzsbDFz5kyhVCrF2bNnzZzcOulbz7Nnz4rk5GSRmZkpcnNzxVdffSWcnJzE2rVrzZzceulb06eKi4tFo0aNRO/evUWbNm3ME9YG6FvPgwcPCgDil19+EQUFBdpNrVabObl1qs7rs1+/fqJTp04iJSVF5ObmihMnTogjR46YMbX10reexcXFOq/L/Px84eHhIebOnWve4FZM35qmpaUJmUwmvvjiC5GTkyPS0tJEcHCwGDBggJmTC8HG4jc6duwoYmJidPYFBQWJuLi4Ssd/8MEHIigoSGffuHHjRGhoqMky2hp9a/qs8PBwNha/YUg9n2rRooWIj483djSbZIx6/ulPfxLvvvuusaPZrOrWdNCgQeLDDz8Uc+fOZWPxDH3r+bSxuHfvnhnS2R596/ndd98JV1dXcefOHXPEszmGvofu2bNHSJIk8vLyTBHPJulb0yVLlohGjRrp7EtISBD+/v4my1gVHgr1jLKyMpw5cwa9e/fW2d+7d28cPXq00vscO3bsufERERE4ffo0VCqVybLaiurUlKpmjHpqNBo8ePAAHh4epohoU4xRz4yMDBw9ehTh4eGmiGhzqlvTzZs34/Lly5g7d66pI9oUQ16j7dq1g5+fH3r27ImDBw+aMqbNqE499+7diw4dOuDTTz9FvXr10KxZM0yfPh2PHz82R2SrZoz30I0bN6JXr14ICAgwRUSbU52adunSBdeuXcP+/fshhMDNmzexc+dOvPXWW+aIrENh9ke0Yrdv30Z5eTl8fHx09vv4+KCwsLDS+xQWFlY6Xq1W4/bt2/Dz8zNZXltQnZpS1YxRz6VLl+LRo0d45513TBHRphhST39/f9y6dQtqtRrz5s3D6NGjTRnVZlSnphcvXkRcXBzS0tKgUPDP0rOqU08/Pz+sW7cOISEhKC0txVdffYWePXvi0KFD6N69uzliW63q1DMnJwfp6elwcHDAnj17cPv2bYwfPx5379595c+zMPRvUkFBAb777jskJyebKqLNqU5Nu3TpgqSkJAwaNAhPnjyBWq1Gv379sGLFCnNE1sF38EpIkqTztRDiuX2/N76y/a8yfWtKL1bdem7btg3z5s3DN998gzp16pgqns2pTj3T0tLw8OFDHD9+HHFxcWjSpAmGDBliypg25WVrWl5ejqioKMTHx6NZs2bmimdz9HmNBgYGIjAwUPt1586dkZ+fj88+++yVbyye0qeeGo0GkiQhKSkJrq6uAIBly5Zh4MCBSExMhKOjo8nzWrvq/k3asmUL3NzcMGDAABMls1361DQrKwuTJ0/GnDlzEBERgYKCAsyYMQMxMTHYuHGjOeJqsbF4hpeXF+Ry+XMdYVFR0XOd41O+vr6VjlcoFPD09DRZVltRnZpS1Qyp544dOzBq1Cj8/e9/R69evUwZ02YYUs+GDRsCAFq1aoWbN29i3rx5bCygf00fPHiA06dPIyMjAxMnTgRQ8UFOCAGFQoEDBw6gR48eZslujYz1HhoaGoqtW7caO57NqU49/fz8UK9ePW1TAQDNmzeHEALXrl1D06ZNTZrZmhny+hRCYNOmTRg2bBjs7OxMGdOmVKemixYtQlhYGGbMmAEAaN26NZydndGtWzd8/PHHZj16hudYPMPOzg4hISFISUnR2Z+SkoIuXbpUep/OnTs/N/7AgQPo0KEDlEqlybLaiurUlKpW3Xpu27YN0dHRSE5Otsgxl9bKWK9PIQRKS0uNHc8m6VtTFxcXXLhwAefOndNuMTExCAwMxLlz59CpUydzRbdKxnqNZmRkvPKH5gLVq2dYWBhu3LiBhw8favdlZ2dDJpPB39/fpHmtnSGvz9TUVFy6dAmjRo0yZUSbU52alpSUQCbT/Ugvl8sB/HoUjdmY/XRxK/f0El8bN24UWVlZYsqUKcLZ2Vl7tYK4uDgxbNgw7finl5udOnWqyMrKEhs3buTlZn9D35oKIURGRobIyMgQISEhIioqSmRkZIgff/zREvGtjr71TE5OFgqFQiQmJupc4q+4uNhST8Gq6FvPlStXir1794rs7GyRnZ0tNm3aJFxcXMTs2bMt9RSsTnV+55/Fq0Lp0reey5cvF3v27BHZ2dkiMzNTxMXFCQBi165dlnoKVkXfej548ED4+/uLgQMHih9//FGkpqaKpk2bitGjR1vqKViV6v6+v/vuu6JTp07mjmsT9K3p5s2bhUKhEKtWrRKXL18W6enpokOHDqJjx45mz87GohKJiYkiICBA2NnZifbt24vU1FTt90aMGCHCw8N1xh86dEi0a9dO2NnZiQYNGojVq1ebObH107emAJ7bAgICzBvaiulTz/Dw8ErrOWLECPMHt1L61DMhIUEEBwcLJycn4eLiItq1aydWrVolysvLLZDceun7O/8sNhbP06eeixcvFo0bNxYODg7C3d1ddO3aVezbt88Cqa2Xvq/Pn376SfTq1Us4OjoKf39/MW3aNFFSUmLm1NZL33oWFxcLR0dHsW7dOjMntR361jQhIUG0aNFCODo6Cj8/PzF06FBx7do1M6cWQhLC3HMkRERERERU0/AcCyIiIiIiMhgbCyIiIiIiMhgbCyIiIiIiMhgbCyIiIiIiMhgbCyIiIiIiMhgbCyIiIiIiMhgbCyIiIiIiMhgbCyIiIiIiMhgbCyIiMql58+ahbdu2Fnv8v/71rxg7duxLjZ0+fTomT55s4kRERDUTV94mIqJqkyTphd8fMWIEVq5cidLSUnh6epop1a9u3ryJpk2b4vz582jQoMHvji8qKkLjxo1x/vx5NGzY0PQBiYhqEDYWRERUbYWFhdrbO3bswJw5c/DLL79o9zk6OsLV1dUS0QAACxcuRGpqKr7//vuXvs+f//xnNGnSBIsXLzZhMiKimoeHQhERUbX5+vpqN1dXV0iS9Ny+3x4KFR0djQEDBmDhwoXw8fGBm5sb4uPjoVarMWPGDHh4eMDf3x+bNm3Seazr169j0KBBcHd3h6enJ/r374+8vLwX5tu+fTv69euns2/nzp1o1aoVHB0d4enpiV69euHRo0fa7/fr1w/btm0zuDZERK8aNhZERGR2P/zwA27cuIHDhw9j2bJlmDdvHt5++224u7vjxIkTiImJQUxMDPLz8wEAJSUleP3111GrVi0cPnwY6enpqFWrFvr06YOysrJKH+PevXvIzMxEhw4dtPsKCgowZMgQjBw5Ej/99BMOHTqEyMhIPDt537FjR+Tn5+PKlSumLQIRUQ3DxoKIiMzOw8MDCQkJCAwMxMiRIxEYGIiSkhLMmjULTZs2xcyZM2FnZ4cjR44AqJh5kMlk2LBhA1q1aoXmzZtj8+bNuHr1Kg4dOlTpY1y5cgVCCNStW1e7r6CgAGq1GpGRkWjQoAFatWqF8ePHo1atWtox9erVA4DfnQ0hIiJdCksHICKiV09wcDBksl//t+Xj44OWLVtqv5bL5fD09ERRUREA4MyZM7h06RJq166t83OePHmCy5cvV/oYjx8/BgA4ODho97Vp0wY9e/ZEq1atEBERgd69e2PgwIFwd3fXjnF0dARQMUtCREQvj40FERGZnVKp1PlakqRK92k0GgCARqNBSEgIkpKSnvtZ3t7elT6Gl5cXgIpDop6OkcvlSElJwdGjR3HgwAGsWLECs2fPxokTJ7RXgbp79+4Lfy4REVWOh0IREZHVa9++PS5evIg6deqgSZMmOltVV51q3LgxXFxckJWVpbNfkiSEhYUhPj4eGRkZsLOzw549e7Tfz8zMhFKpRHBwsEmfExFRTcPGgoiIrN7QoUPh5eWF/v37Iy0tDbm5uUhNTUVsbCyuXbtW6X1kMhl69eqF9PR07b4TJ05g4cKFOH36NK5evYrdu3fj1q1baN68uXZMWloaunXrpj0kioiIXg4bCyIisnpOTk44fPgw6tevj8jISDRv3hwjR47E48eP4eLiUuX9xo4di+3bt2sPqXJxccHhw4fx5ptvolmzZvjwww+xdOlS/PGPf9TeZ9u2bRgzZozJnxMRUU3DBfKIiKjGEkIgNDQUU6ZMwZAhQ353/L59+zBjxgycP38eCgVPQyQi0gdnLIiIqMaSJAnr1q2DWq1+qfGPHj3C5s2b2VQQEVUDZyyIiIiIiMhgnLEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKDsbEgIiIiIiKD/T/vl+zD1qnZawAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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wm+//QbAwoUL2bp1Kx9++CGNGzfmtttuY+LEiUyfPt01j2T27NmcP3+eWbNmUa9ePbp06cLw4cOZNGmSfpkUEZfAwEACAwO9HYaIiEih4XFiUbFiRaZNm5bp+FtvvUXFihXzJajcKFWqFLVr1+b999/n7Nmz2O123nrrLcqWLUuTJk2AjKSoXr16REVFueq1b9+e1NRUNmzY4CrTqlUrAgIC3MocPHiQPXv2XNVrEpHCyd/fn6FDhzJ06FD1VuTROQLZ6qzEVmclNjpreDscERHJA4+Xm3311Ve55557WLBgAc2aNQNg9erV7Ny5ky+++CLfA8wpwzBYtGgRnTt3pnjx4lgsFsqWLcsPP/xAiRIlAEhKSqJs2bJu9cLDw/H39ycpKclVJjo62q3MhTpJSUlUrlw502unpqaSmprq2vdkFS0RkWvZCUs4C503APCxow2xXo5HRERyz+Mei44dO5KYmMhdd93F8ePHOXbsGJ07dyYxMdG1rnt+GjlyJIZhXPGxfv16TNNkwIABlClThmXLlrF27Vo6d+7MnXfeyaFDh1zny2p1K9M03Y7/s8yFIVCXWxlr3LhxhIWFuR7e7LkREREREfGGXN0gr2LFiowdOza/Y8nSwIED6d69+xXLREdH89NPPzFv3jxOnDjhupfG1KlTWbRoEe+99x5Dhw4lMjKSNWvWuNU9ceIE6enprl6JyMhIV+/FBYcPHwbI1NtxwbBhw4iPj3ftJycnK7kQKcLsdjvz5s0D4M4778Rm071GRUREcvVpuGzZMt566y127drFZ599Rvny5fnggw+oXLkyN910U74GGBERQURERLblUlJSALBY3DthLBYLTqcTgNjYWMaMGcOhQ4coV64ckDGhOyAgwDUPIzY2luHDh5OWluYaO71w4UKioqIyDZG6ICAgwG1OhogUbU6nk4SEBIAC6am9lmTuIfZSICIikmceD4X64osvaN++PUFBQfzyyy+uuQWnT5++ar0YWYmNjSU8PJzevXvz66+/kpiYyNNPP83u3bu54447AGjXrh116tShV69ebNy4kR9//JHBgwfTr18/Vy9Hz549CQgIIC4uji1btjB37lzGjh1LfHx8jm8SKCJFm9VqpW3btrRt2xar1ertcHxaGY7Qp/pC+lRfyEi/97wdjoiI5IHHicVLL73EtGnTmD59On5+fq7jzZs3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p+05Z84c02azmW+88Ybbsn8nT5701iUUGp625f/+9z/zm2++MRMTE83ExERzxowZZmhoqPnss8966xIKjdz8P7+UVoW6yNO2fPXVV825c+eaiYmJ5pYtW8yhQ4eagPnFF1946xIKFU/b8/Tp02aFChXMf//73+Zvv/1mLlmyxKxevbr50EMPeesSCo3c/j+///77zRtvvPFqh1uoedqWM2fONG02mzl16lRz586d5vLly82YmBizadOmVz12JRa59MYbb5jXXXed6e/vb15//fXmkiVLXM/17t3bbNWqlVv5xYsXm40bNzb9/f3N6Oho880337zKERdenrYlkOlx3XXXXd2gCzFP2rNVq1ZZtmfv3r2vfuCFkCdtOXnyZLNu3bpmcHCwGRoaajZu3NicOnWq6XA4vBB54ePp//NLKbFw50lbvvzyy2bVqlXNwMBAMzw83LzpppvM+fPneyHqwsvTv81t27aZt912mxkUFGRWqFDBjI+PN1NSUq5y1IWTp2158uRJMygoyHz77bevcqSFn6dtOXnyZLNOnTpmUFCQWa5cOfO+++4zDxw4cJWjNk3DNK92H4mIiIiIiBQ1mmMhIiIiIiJ5psRCRERERETyTImFiIiIiIjkmRILERERERHJMyUWIiIiIiKSZ0osREREREQkz5RYiIiIiIhInimxEBERERGRPFNiISIiV9XIkSNp1KiR117/+eef5+GHH85R2cGDB/P4448XcEQiIkWD7rwtIiL5xjCMKz7fu3dv/ve//5GamkqpUqWuUlQX/fXXX1SvXp1NmzYRHR2dbfnDhw9TtWpVNm3aROXKlQs+QBERH6bEQkRE8k1SUpJr+5NPPmHEiBH88ccfrmNBQUGEhYV5IzQAxo4dy5IlS1iwYEGO69xzzz1Uq1aNl19+uQAjExHxfRoKJSIi+SYyMtL1CAsLwzCMTMf+ORQqLi6Ou+++m7Fjx1K2bFlKlCjBqFGjsNvtPP3005QsWZIKFSowY8YMt9f6888/6datG+Hh4ZQqVYrOnTuzZ8+eK8b38ccfc9ddd7kd+/zzz6lfvz5BQUGUKlWK2267jbNnz7qev+uuu/joo4/y3DYiIkWdEgsREfG6n376iYMHD7J06VImTZrEyJEjufPOOwkPD2fNmjX079+f/v37s3//fgBSUlJo06YNxYoVY+nSpSxfvpxixYpx++23k5aWluVrnDhxgi1bthATE+M6dujQIXr06EGfPn3Ytm0bixcvpkuXLlzamd+0aVP279/P3r17C7YRRER8nBILERHxupIlSzJ58mRq1qxJnz59qFmzJikpKQwfPpzq1aszbNgw/P39WbFiBZDR82CxWHjnnXeoX78+tWvXZubMmezbt4/Fixdn+Rp79+7FNE2ioqJcxw4dOoTdbqdLly5ER0dTv359BgwYQLFixVxlypcvD5Btb4iIyLXO5u0ARERE6tati8Vy8beusmXLUq9ePde+1WqlVKlSHD58GIANGzawY8cOihcv7nae8+fPs3Pnzixf49y5cwAEBga6jjVs2JBbb72V+vXr0759e9q1a8e///1vwsPDXWWCgoKAjF4SERG5PCUWIiLidX5+fm77hmFkeczpdALgdDpp0qQJs2fPznSu0qVLZ/kaERERQMaQqAtlrFYrixYtYuXKlSxcuJApU6bw7LPPsmbNGtcqUMePH7/ieUVEJIOGQomIiM+5/vrr2b59O2XKlKFatWpuj8utOlW1alVCQ0PZunWr23HDMGjRogWjRo1i48aN+Pv7M3fuXNfzW7Zswc/Pj7p16xboNYmI+DolFiIi4nPuu+8+IiIi6Ny5M8uWLWP37t0sWbKEJ554ggMHDmRZx2KxcNttt7F8+XLXsTVr1jB27FjWr1/Pvn37+PLLLzly5Ai1a9d2lVm2bBk333yza0iUiIhkTYmFiIj4nODgYJYuXUqlSpXo0qULtWvXpk+fPpw7d47Q0NDL1nv44Yf5+OOPXUOqQkNDWbp0KR07dqRGjRo899xzTJw4kQ4dOrjqfPTRR/Tr16/Ar0lExNfpBnkiInLNME2TZs2aMWjQIHr06JFt+fnz5/P000+zadMmbDZNSxQRuRL1WIiIyDXDMAzefvtt7HZ7jsqfPXuWmTNnKqkQEckB9ViIiIiIiEieqcdCRERERETyTImFiIiIiIjkmRILERERERHJMyUWIiIiIiKSZ0osREREREQkz5RYiIiIiIhInimxEBERERGRPFNiISIiIiIieabEQkRERERE8kyJhYiIiIiI5Nn/AwJnAx8ULla+AAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAGGCAYAAADmRxfNAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACM80lEQVR4nOzddXxV9R/H8de5sY2NBTC6BoxuMAgVDEJCFH8qgoSUqDQYgJSUgCAlIZIKBmKgKAaCAtI4pUNi1KQ3YLDdOL8/rlyZ1AYbdxvv5+NxHo97T37ul3Hv+ZxvGaZpmoiIiIiIiNwCi68DEBERERGRjE+JhYiIiIiI3DIlFiIiIiIicsuUWIiIiIiIyC1TYiEiIiIiIrdMiYWIiIiIiNwyJRYiIiIiInLLlFiIiIiIiMgts/k6gPTG7XZz5MgRgoODMQzD1+GIiIiIiPiMaZqcPXuWfPnyYbFcv05CicV/HDlyhIIFC/o6DBERERGRdOPgwYMUKFDguvsosfiP4OBgwFN4ISEhPo5GRG4Xt9vN/v37AYiIiLjhUxlJhsREGDPG87pXL/Dz8208IiKSYnFxcRQsWNB7j3w9Siz+41Lzp5CQECUWIneYSpUq+TqEzCUxEfz9Pa9DQpRYiIhkYMnpIqBHciIiIiIicstUYyEigqcp1J49ewCIjIxUUygREZEU0i+niAjgdDqZP38+8+fPx+l0+jocERGRDEc1FiIieNqO5suXz/taUoFhwD9lispU5Ja5XC4cDoevw5BMxm63Y7VaU+VchmmaZqqcKZOIi4sjNDSU2NhYdd4WERERnzNNk5iYGM6cOePrUCSTCgsLI0+ePFd9sJaSe2PVWIiIiIikY5eSily5chEYGKhaVUk1pmkSHx/PsWPHAMibN+8tnU+JhYiIiEg65XK5vElFjhw5fB2OZEJZsmQB4NixY+TKleuWmkVlus7bixcv5t577yVLliyEh4fTtGlTX4ckIhmAw+FgxowZzJgxQ22YU4vDAePGeRaVqchNufR9FBgY6ONIJDO79Pd1q79/marGYuHChXTo0IHhw4fz0EMPYZommzdv9nVYIpIBmKbJwYMHva8lFZgmXGoTrjIVuSVq/iRpKbX+vjJNYuF0OunWrRujR4+mXbt23vUlS5a8qfNtOnCKrMEpH3Ly5v9dbu7Am73erfz53Owf381e8+Y/481/ytv9/X27P+OtfL6MEmtKD3O73VR7pDGhWezYbJnmq1FEROS2yTS/nps2beLw4cNYLBYqV65MTEwMlSpV4u2336Zs2bLXPC4hIYGEhATv+7i4OADWzO1HgL/fda851dmYi/h7399t7KC6ZdsNY/2bbHziejDJuv9ZfyEvJ2947AazJKvd/34eG05esH5zw+MAvnDdxxHCve+LGkeob1l3lT2T3pI5sDLd1SjJulqWPyhjHLjhNfeaefjefU+Sdc2tSwkm/obHrnCXZ5sZ4X0fyjmesS674XEAH7seIo4g7/uyxr5k/dvEEsQCV+0k6+pZ1lPAOHaVvZOW0zazcJJ/G4Dnrd8l6wb3B3dVDpm5vO/zcpJ61vXJOBLmuOpiXtaqsaqxk3KW/Tc8LsbMzvfuu5Osa2z5jTDj3A2P3eQuzlaziPd9AAn8z/prsuJd7LqX0/w7qkRR40iy/m0umH587n4gyboali0UNv6+4bH7zLyscZdJsq6p5Vf8jCsfHsSZgYxZto/yJSN5oHhO7i2anUC/TPNVKSKSoRiGwRdffMHjjz/uk+tHRETQvXt3unfv7pPrZzSZ5tdy7969AAwaNIixY8cSERHBmDFjqFWrFrt27SJ79uxXPW7EiBEMHjz4ivUv2b4mxHb9W8JZzvpJEot7Ldvpaf/shrH+4S56RWLxtHU591h23vDYic7H/5NYuHjF/ukNjwNY5y7FEfPfxKK4cZhXk3HsBdPvisSirmUDLWxLb3js9667rkgsXrQuoqDl+A2PPefIwjZXhPd9duMsfe0f3fA4gG/d9xJn/ptY3G3ZyRv2eTc8bq87zxWJxTPWZTxkjbrhsbOc9a5ILPrZ5mEz3Dc8dl9iniSJRRHLUQbZ597wOIAPXHVwXfa+jnUjnWw3TjZXucpekVi8ZFtEaUv0DY8d4XiWra5/E4tgLjDUPitZ8W50l+C0+W9iUdnYwzD7zBsed9wM5fOEpInFM9blNLH+dsNjF7ruvyKxGGD/gDDj/NUPOAdb1kewYm15xpj3E1y4ItWK5uCeItmpXCiMAHvqjPctIpKZtWnThjNnzvDll1+m2jkvtZpYvXo11apV865PSEggX758nDp1imXLllG7du1Uu+aNnD59mq5du7Jo0SIAHnvsMSZOnEhYWJh3n27durFy5Uq2bNlC6dKliYqKSnKO5cuX884777Bu3Tri4uIoXrw4r7zyCi1atLhtnyM1pPvO24MGDcIwjOsuGzZswO323Lz169ePJ598kqpVqzJr1iwMw2DBggXXPH+fPn2IjY31LpfaWIvIncVtmuw/42b/GTdu06ScZT8v2r6mhLmX1XtP8s5Pu3h2+hqqDvqaFpN/ZuSSHSzbeYy4i+qULCJyOxUsWJBZs5I+zPriiy/ImjWrT+Jp3rw5UVFRLFmyhCVLlhAVFUXLli2T7GOaJm3btuWZZ5656jl+++03KlSowMKFC/nzzz9p27YtrVq14uuvv74dHyHVpPsai86dO9OsWbPr7hMREcHZs2cBKFPm36eS/v7+FC1alOjoaz+B9ff3x9/f/4r13emFHwHXva41ICshlxXhz9zPX0Re9xiAc5YsBAdcVvQmTKAlIeaNm6ActOYh2Gq7dBgWLLxsvnbD4wCO+BUmKzZvx9QdlORl96tJ9jG4soOlG4MgP6v3mgCfUZfV7so3vOYJI4xAP2uSfpuDzfZkcSXe8NgdlsIEWP7NfePIThdXzxseB3DOGob/ZXnzb1Smq+vqtVaXO08Afrak+fb7ZhO+ctW64bEHzDz4WZMe28vV5aplejkT2GVEYLf+W0O2j4L0cHa+4XEAFoslSf+Fb80a7HRGJNn38hguvTphhmK1JK2VG+N6hhD31Z/iX/5vuM2M4PJDzxFIL0en68Z7yVFyJOk3sdEsce1jL7tmAvYrNs93PsxKd7kbXvOAO/cV6wY5WmO/rCmUy+Vi7cY1hBnnGHK/SRW753tjhbt8kuPuN39n4t8T2RJThLUrSzHPLElstgoULlyEigXDqFQgjJJ5gq/4OxIRuVVut8np+Bv/fqalbIF+WCwp7/xWu3ZtKlSoQEBAAO+//z5+fn506tSJQYMGeffZvXs37dq1Y926dRQtWpTx48df9VytW7dmwoQJjBs3zjtU6syZM2ndujVDhgxJsu9rr73GF198waFDh8iTJw8tWrRgwIAB2O3//qYsWrSIN998ky1btpA1a1YeeOABPv/8c+/2+Ph42rZty4IFC8iWLRtvvPEGHTt2BGD79u0sWbKENWvWcO+99wIwffp0qlevzs6dO719fSdMmADA8ePH+fPPP6/4TH379k3yvmvXrnz//fd88cUXNG7cOFllnB6k+8QiPDyc8PDwG+5XtWpV/P392blzJ/fddx/gGTJr//79FC5cOMXXHdenx22eebveLRz7aKpFcXvUv4VjfTF88K2Ub9r/24xL1bM1uIVjn0i1KK5l0hVrGt7C2ZIe63A4mDptGkfOXODXuxsyec8BLEc2cJxsSfa717Idu+GisrGHypY9wDdwDg5tCSfqz2Ischdjq1EcM08FShbKS8WCoZTPH0aR8KArErlMzzAgZ85/X4vILTkdn0jVoT/5NIaNbzxCjqxXPpBNjjlz5tCzZ0/Wrl3L6tWradOmDTVr1qROnTq43W6aNm1KeHg4a9asIS4u7pr9GqpWrUqRIkVYuHAhzz33HAcPHuTXX3/l3XffvSKxCA4OZvbs2eTLl4/NmzfToUMHgoODefVVz4PVxYsX07RpU/r168cHH3xAYmIiixcvTnKOMWPGMGTIEPr27ctnn33Giy++yAMPPECpUqVYvXo1oaGh3qQCoFq1aoSGhvLbb7/d9CBCALGxsZQuXfqmj/eFdJ9YJFdISAidOnVi4MCBFCxYkMKFCzN69GgAnnrqKR9HJyLpnd1up0vny2qJ6pXh7MVHWLP3FGv2nmTdvlNsPRLLcTOMPe58RFqOJDm+gHGCAtYTNLKuBWDV32Vpcaifd3uA3UK53FkomT8HZfKFUCZvCKXyhJDFLxP317Db4eWXfR2FiKQTFSpUYODAgQAUL16cSZMmsXTpUurUqcNPP/3E9u3b2b9/PwUKFABg+PDhPPro1R/QPf/888ycOZPnnnuOWbNm0aBBA3JeepBxmTfeeMP7OiIigl69evHJJ594E4thw4bRrFmzJP1tK1asmOQcDRo04KWXXgI8NSDvvPMOy5cvp1SpUsTExJArVy7+K1euXMTExKSkeJL47LPPWL9+PdOmTbvpc/hCpkksAEaPHo3NZqNly5ZcuHCBe++9l59//pls2bLd+GARkf8IDrBTp0xu6pTxNKU6e9HBpuh7+GLfSXbs2UuWo+spz04qWf6ivLGPQOPfEeb+MIslOVeCw8nM489y/Fgo2zcV5kd3YcabEZzPXoa8+QtTJl8IpfIEUypPCLlD/DVmvYhkOhUqVEjyPm/evBw75hl1cfv27RQqVMibVABUr179mud67rnneP3119m7dy+zZ8/2NjX6r88++4xx48axZ88ezp07h9PpTNIiJSoqig4dOiQ7bsMwyJMnjzfuS+v+yzTNm/4eX758OW3atGH69OnXHdk0PcpUiYXdbuftt9/m7bff9nUoIpIJBQfYqVUiJ7VK5IR6pbjoqMf2o3H8cfAMnx48yZkDW8gRt4VKxl/86k76A1rIOEaIEU+IEU8xjtLIusaz4Rwc3xHKtm2F2W4WYpG7AOv8qpEvTx5K5MlKydzBlMwTQsncwYQGXtnPREQko7i8XwN4bsgvDb5ztYlJr3djniNHDho1akS7du24ePEijz76qLe/7SVr1qzx1kbUq1eP0NBQPv74Y8aMGePd51IfjZuNO0+ePPz995XDnh8/fpzcua/s33cjv/zyC40bN2bs2LG0atUqxcf7WqZKLEREbpbD4eCjjzzDGT/77LNX/JBcTYDdSuVC2ahcKBtQBLiL2HgHfx4+w30Hz5D1YCybD5/h77gEgonnD3dRShkH8TeSjiSV04illvVPauHp0FfzYmnW7fdj3f5TAJQ2DlDY+JuTQUUJzFOCEnlCvclGZK6s6bc5lcMB773ned2xo6dplIjctGyBfmx84xGfx5AWypQpQ3R0NEeOHCFfvnyAZ0jZ62nbti0NGjTgtddew2q98ntw1apVFC5cmH79/m2WeuBA0jm4KlSowNKlS3n++edvKu7q1asTGxvLunXruOcez/D6a9euJTY2lho1aqToXMuXL6dRo0aMHDnS2zk8o1FiISKC52nZpflwrvbkLLlCA+3cXzwn9xf/t63viXMJbD8ax+ojDZh1+BRnD+8g5Mw2ShkHKGMcoKxlP9n/mZjwnBnAYZIOWPG4dSUv2BaDAy5G29lzID87zQJ87S7ILgoSH1qCbHkiPMlGnhBK5slKRI4gbFYfj0xlmnD8+L+vReSWWCzGTXecTu8eeeQRSpYsSatWrRgzZgxxcXFJEoKrqV+/PsePH7/mYDuRkZFER0fz8ccfc/fdd7N48WK++OKLJPsMHDiQhx9+mGLFitGsWTOcTiffffedtw/GjZQuXZr69evToUMHb3+Ijh070qhRoyQdty81xYqJieHChQveeSzKlCmDn58fy5cvp2HDhnTr1o0nn3zS2z/Dz8/vmnOxpUdKLEREAJvNRtOmTb2vU1N4Vv/Lko1iwN1cSHSxIyaObUfjWHI4lpjD+7Cd2EGg4zT/ndW9pHHI+zrAcFDO2E859sOlB3TxEPdXIJ/sqs3LzucA8LNaiMyVlVJ5gin5z6L+GyKSXlksFr744gvatWvHPffcQ0REBBMmTKB+/WuPJGkYxnVHDm3SpAk9evSgc+fOJCQk0LBhQ/r3759kiNvatWuzYMEChgwZwltvvUVISAgPPPDANc95NfPmzaNr167UrVsX8EyQN2lS0nEM27dvzy+//OJ9X7myZ8j+ffv2ERERwezZs4mPj2fEiBGMGDHCu1+tWrVYvnx5iuLxJcO8lUdzmVBcXByhoaHExsbe5uFmReROZ5omh89cYGfMWXb+fZZdMWfZEXOWvCdWUd7cQ0lLNCWNQxQxjmI1rvzqnuZsyAjn5bO0mvzk9wqHzJzsMAuy012Qw35FsOUpRWTeHP8kG8GUyB1McEAaNFNKTIThwz2v+/YFv7RpQiGSmV28eJF9+/ZRpEgRAgKuP7+WyM263t9ZSu6NVWMhIpJOGIZBgWyBFMgWyMOl/+3053Ddx/4T59n591m+ijnLX0dOkhiznZCzuylhHKKkEU0JyyF2ugsmOV9+ThBpOUIkR6jNH971zqMW9h3Jy06zID+7CzHFLMiBkKoUzpvrn9oNzwhVRcKDsPu6OZWIiGQYSixERAC3283Ro0cBzxCIFkv6uaG2Wy0Uzx1M8dzBNPIONlWD+EQne46dY0fMWVbFnOV4TBy5/j7HsbOeYW9zG6eJNQMJNeKTnM9muCluHKY4h72jU9WOHcNPZ+Cn7Z4hFIsYR8lvPUNC9lLkz5efUnlDvDUceUIC1JxKRESuoMRCRARwOp1Mnz4dgL59++KXAZrtBPrZqFAgjAoFwpKsP30+kZ1/n2XH0TK8FfMQx4/sx3p8GxGuA5S0HKSUcZBixmH8DScA8aY/0WbSYRH/Z/2Fl22LIA7+jg1jx9ZC7DAL8rW7IIf8imDPXZqilzenyhNMSFo0pxIRkQxDiYWICJ5mSGFhYd7XGVm2ID+qFc1BtaI5/llTEbf7MQ6fucCOmLP8HBPH1KOnOX9kJ0Gxuwgxz+ImaQ1NSeOg93Vu4wy5rWe8w+ECOGMs7D+ahy9dNXnD9QQA+cOyUCpPMKXzhlAmXwilw7NQODQUi2FABi9TERG5MSUWIiJ4JkDq3r27r8NIMxaLQcHsgRTMHuidSRzuIcHpYs+xc1SJOcvOfzqL74w5y5fn7uOwGU4py0FKGgcJM84nOZ/NcBNpHCG7+98JqQ6fucDhM/E891cvDpk5ec8szD5rEfwLVOCF6FhqFLv26C0iIpLxKbEQEbmD+duslM0XStl8oUnWn4m/3zs61ZdH4jh+ZB/WEzso7NznbU4VaRxmh5m0w3huTvOg9Y8k6+IOB/LGzLZsfKgNLz8YicWi2gsRkcxIiYWIiFwhLNCPe4vm4F5vc6oKmOZjHDrtGQ53+d9nee/oafbEnMF2woHT7Rn+NtJy+IpzhRjxTLBPYtay3bTf35W3m91D9qD034dFRERSRomFiAieztufffYZAP/73/9SfZK8zMAw/m1O9UiZfzt7Jzhd7D1+nh0xcWw/WpQXDt+FGbOFAud30WzzD5SwHIZKdp63fU/lA7tpN+5V3niuHlULZ5zZZEVE5Mb0yykigme42R07dnhfS/L526yUzhtC6bwhPFEZoDTwCMdOxLGzq8HBg5u4z9yFP04qWfYyO7EnPd7rQvV6zWh/f5EM31leRNKv5cuX8+CDD3L69GnvAB230/79+ylSpAi///47lSpVuu3Xv93Sz0DtIiI+ZLVaady4MY0bN8Zqtfo6nEwhV0gA95fISckq99PRNoT9bk8tRyAJnHFnYdi32+n4wUZi4x0+jlRE0kKbNm0wDAPDMLDb7RQtWpTevXtz/vz5Gx8MREREMG7cuFSNafny5RiGQbZs2bh48WKSbevWrfPGe7tt3ryZWrVqkSVLFvLnz8+bb76JaZre7UePHqV58+aULFkSi8Vy1cFGpk+fzv3330+2bNnIli0bjzzyCOvWrbuNn0KJhYgI4EksqlatStWqVZVYpLIC2QJ5u0tLhhaYwreuexjpbMYmswQAP277m0aTVvDnoTO+DVJE0kT9+vU5evQoe/fuZejQoUyePJnevXv7OiyCg4P54osvkqybOXMmhQoVuu2xxMXFUadOHfLly8f69euZOHEib7/9NmPHjvXuk5CQQM6cOenXrx8VK1a86nmWL1/Os88+y7Jly1i9ejWFChWibt26HD58Zd+3tKLEQkRE0lzOYH+mtX+IHfdPYoa7QZJth0+d582pc5m7en+SJ3QikvH5+/uTJ08eChYsSPPmzWnRogVffvklkZGRvP3220n23bJlCxaLhb/++uuq5zIMg/fff58nnniCwMBAihcvzqJFi5Ls8+2331KiRAmyZMnCgw8+yP79+696rtatWzNz5kzv+wsXLvDxxx/TunXrJPudPHmSZ599lgIFChAYGEj58uX56KOPkuzjdrsZOXIkkZGR+Pv7U6hQIYYNG5Zkn7179/Lggw8SGBhIxYoVWb16tXfbvHnzuHjxIrNnz6ZcuXI0bdqUvn37MnbsWO93YkREBOPHj6dVq1aEhiYdxe/y87z00ktUqlSJUqVKMX36dNxuN0uXLr3q/mlBiYWICGCaJseOHePYsWO6uU0jVotBz7olmfP8vUlGhXrZ+iWfWgdwevEgus7fwLkEpw+jFJG0lCVLFhwOB23btmXWrFlJts2cOZP777+fYsWKXfP4wYMH8/TTT/Pnn3/SoEEDWrRowalTpwA4ePAgTZs2pUGDBkRFRdG+fXtef/31q56nZcuWrFixgujoaAAWLlxIREQEVapUSbLfxYsXqVq1Kt988w1btmyhY8eOtGzZkrVr13r36dOnDyNHjqR///5s27aN+fPnkzt37iTn6devH7179yYqKooSJUrw7LPP4nR6vutWr15NrVq18Pf39+5fr149jhw5cs3EKDni4+NxOBxkz377BspQYiEiAjgcDiZPnszkyZNxONTmPy09UCIn33a9n7sjslHW2EcP20Ishkk32xc8u6Mrrcd/zfajcb4OU0RS2bp165g/fz4PP/wwzz//PDt37vT2AXA4HHz44Ye0bdv2uudo06YNzz77LJGRkQwfPpzz5897zzFlyhSKFi3KO++8Q8mSJWnRogVt2rS56nly5crFo48+yuzZswFPUnO1a+fPn5/evXtTqVIlihYtSpcuXahXrx4LFiwA4OzZs4wfP55Ro0bRunVrihUrxn333Uf79u2TnKd37940bNiQEiVKMHjwYA4cOMCePXsAiImJuSIRufQ+JibmuuVxPa+//jr58+fnkUceuelzpJRGhRIR+UdgYKCvQ8h8rlGmeUIDmN+hGm9/H8qoVc/Q2/YpNsNNDes2Is93p+e7XXmsydM8dVcBjRolcjW/TYLV7954v7wVofnHSdfNbwZH/7j6/per/jLU6Hxz8f3jm2++IWvWrDidThwOB02aNGHixInkypWLhg0bMnPmTO655x6++eYbLl68yFNPPXXd81WoUMH7OigoiODgYI4dOwbA9u3bqVatWpLvjOrVq1/zXG3btqVbt24899xzrF69mgULFrBixYok+7hcLt566y0++eQTDh8+TEJCAgkJCQQFBXmvmZCQwMMPP5zsuPPmzQvAsWPHKFWq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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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t8+4fv26ByK7P127djVatmwZrz0gIMDo16+fYRiGERwcbGTIkMGYOnWqceDAAWPPnj3GvHnzjMmTJxuGYRhHjx41AGPXrl2GYRjG1KlTjaxZsxo//vijceDAAePtt982smbNajz22GNxzuF0Oo3ChQsb3t7exvvvvx/ntddff9348ccfjSNHjhg7d+40atasabz44ot3fB8tW7Y0ypcvb2zevNkIDQ01mjZtapQsWdKw2WyGYRjG/PnzDS8vL6NmzZrGtm3bjJ07dxqBgYFG7dq13cf48ccfDavVaowYMcL4888/jX379hnLly83hg8f7u4DGF9//XWcc9/L+x07dqxRpEgRY//+/ca5c+cMm81mXLt2zShVqpTRsGFD45dffjGOHDlibNy40ejfv79x8uTJeO8xLf+cpSUul8u4evWqcfXqVcPlcnk6nPTB5TKMq1djHxpTEblH56/GGL0/WW+sf7uBYYzI6n78/HY9o8UH3xj7zsT/TCb3L7HPvP+l2+PlngwaNIjg4GBOnjxJz549+fTTT1mwYAGVKlWiQYMGLFiwAH9//wT37dOnD61bt6Zt27bUqlWL8+fPJ1gXw2w2061bN5xOJ126dInzmtPppF+/fpQrV46mTZtSpkyZRJe4nT9/PgEBATzzzDMEBgZiGAZr1qyJs7pVxowZGTJkCB06dCAwMJAMGTKwfPly9+tNmjRh9erVrF+/nho1alC7dm2mTJly15mte3m/vXr1okyZMu77MLZs2ULGjBn55ZdfKFKkCK1bt6ZcuXJ0796d69evawbDg0wmE5kyZSJTpkxa9zylmEyQKVPsQ2MqIvfgjxMXGTR1AW+f7EOj26poT7E/z/JSk1jcvznl8utvpaeZDOMOF6RLoqKiosiWLRuXL1+O96Hvxo0bHD16FH9//zjX4svd9erVi3/++YdVq1Y90PMsWLCAAQMGcOnSpQd6ngdJP2ciIpLeGYbBZ1uO8vePM3nHssBd8O6CkZlBjlep26QtPev564ufByixz7z/5fE6FiIAly9f5vfff2fJkiV8++23ng5HxM3pdLprm9StW/eOixRIEjgcsHZt7HaTJnHr94iI3HQtxsHQr/ayb8/vrPWeh9UUu4DMLldJ3vF+k3e7NaamvwrepSb6bS6pQsuWLfntt994+eWXeeqppzwdjoib0+nk//7v/wCoXbu2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LFq0CIGBgaipqUFmZiYyMzPR1NQEAOjo6MDs2bPh7++PyspKVFdXw9/fH/Pnz8eXL19sbuPt27eoq6tDfHy8Mtba2orU1FRkZGSgoaEBFRUVSE5Oxo8n7ydPnoympiY8f/7ctUUgIupn2FgQEZHbDRkyBAUFBTAajcjIyIDRaERHRwe2b9+OqKgobNu2DVqtFjdv3gTw9cyDWq3GiRMnYDKZEB0djZKSErx48QIVFRU2t/H8+XOICIYPH66Mtba2oqurC8nJyQgPD4fJZML69evh7++vzBkxYgQA/PZsCBERWdP0dQAiIvr3xMTEQK3+/retkJAQjBs3Trnv4+MDg8GA9vZ2AMDdu3fx+PFjDBo0yOp1Pn/+jCdPntjcxqdPnwAAAwYMUMYmTJiAuXPnwmQyYd68efjvv/+QkpKCwMBAZY5OpwPw9SwJERH9OTYWRETkdr6+vlb3VSqVzbGenh4AQE9PD+Li4nD69OlerzV06FCb2wgKCgLwdUnUtzk+Pj4oLy/HrVu3UFZWhsOHD2PHjh2oqalRrgL15s2bX74uERHZxqVQRETk8WJjY/Ho0SMEBwcjMjLS6vazq05FRERAr9ejvr7ealylUiEhIQF5eXmwWCzQarW4fPmy8nhdXR18fX0RExPj0n0iIupv2FgQEZHHW7FiBYKCgrB48WJUVVXh6dOnMJvN2LhxI5qbm20+R61WIzExEdXV1cpYTU0N8vPzcefOHbx48QKXLl3C69evER0drcypqqrCzJkzlSVRRET0Z9hYEBGRxxs4cCAqKysxcuRIJCcnIzo6GhkZGfj06RP0ev1Pn7du3TqcPXtWWVKl1+tRWVmJhQsXYsyYMdi5cycOHDiABQsWKM8pLS3F2rVrXb5PRET9Df9BHhER9VsigqlTp2LTpk1ITU397fxr164hOzsbtbW10Gj4MUQiInvwjAUREfVbKpUKx48fR1dX1x/N//jxI0pKSthUEBE5gGcsiIiIiIjIaTxjQURERERETmNjQURERERETmNjQURERERETmNjQURERERETmNjQURERERETmNjQURERERETmNjQURERERETmNjQURERERETmNjQURERERETmNjQURERERETvsfLWVEluaovE0AAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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yz9nsYDNlrmyhgEXXhm/XNmAgKzFd5H9+/ezbNkyr6vws2fP5umnn2bmzJk0btyYHTt2MGTIEEJDQ+nfv/8lr+F2uylXrhzvvvsuJUuWZMOGDdx3332UKVOGXr16cdNNN1GlShXmz5/Po48+CoDT6WTBggU8//zzADzwwAOkpaWxZs0aQkND2bVrF0WKFLls3AMGDGDfvn188sknFCtWjMcff5yuXbuya9cuz7mkpKTw3HPP8fbbbxMQEMDw4cO58847Wb9+PQDLly/n7rvv5uWXX+bGG2/k119/5b770i8uPf300573evrpp5k0aRLTpk3DarVmeL6jRo1i9+7dJCcnM3fuXADCw8NJSUmhbdu23HjjjaxZswabzcazzz5L586d+eGHHwjI5v/LK1FiISJyEQPY7q7JSfPyXzZZMWddPJ+t/Aqo8M97GHDvbbfQq2n5HHmPgsbUerMi2TNx4uWfq14d+vb9Z//FF8Hh8F22UiUYMOCf/enTISXl0nLjx2c5xM8++4wiRYrgcrk4d+4cAFOnTvU8/8wzzzBlyhR69uwJQOXKldm1axevv/66z8TCbrczYcIEz37lypXZsGED7777Lr169QJg0KBBzJ0715NYfP7556SkpHieP3ToELfddhv169cHoEqVKpeN/3xCsX79elq1agXAwoULKV++PB999BF33HEHkD5saebMmTRv3hyAt99+m9q1a7NlyxaaNWvGc889x+jRoz3nVKVKFZ555hkee+wxr8SiT58+DBw40CuGK51vkSJFCA4OJjU1laioKE+5BQsWYLFYePPNNz0XrebOnUvx4sVZtWoVHTt2vOw5Z5cSCxERH5533uXZbmNc/RClxVsO8eeXE/ki4D1GOwfzrqstAJN7NrimkopEivOTuyIAO91VaODneEQk97Vt25ZXX32VlJQU3nzzTfbu3ctDDz0EwIkTJzh8+DCDBg1iyJAhnjpOp5OwsLDLvuZrr73Gm2++ycGDBzl79ixpaWk0atTI8/yAAQN44okn2LRpEy1atOCtt96iV69ehIaGAjBixAjuv/9+VqxYQfv27bntttto0MD3J9Lu3bux2WyehAEgIiKCmjVrsnv3bs8xm81G06ZNPfu1atWiePHi7N69m2bNmrF9+3a2bt3Kc8895ylzPtlKSUkhJCQEwOs1Mnu+vmzfvp1ffvmFokW9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dSrz5s2jRo0aPPvss3To0IE9e/ZQtGhRAGJjY/n0009ZsmQJERERjBw5km7durF9+3asVmuOxiwihYPT6eSdd94BoHfv3rpJXjYVMc9QzUj/jqhjHARa+TcgERHJcVn6poyNjaVMmTIEBAT4fD4tLS1Hgjpv48aN9OjRg5tvvhmASpUqsXjxYrZt2wak91ZMnz6dcePG0bNnTwDefvttIiMjWbRoEUOHDiUpKYk5c+Ywf/582rdvD8CCBQsoX748X331FZ06dcrRmEWkcHC73ezbt8+zLdkTZZ6gm3UTAMnWEpjc5eeIREQkp2VpKFTFihWZNm0a8fHxPh+ff/55jgZ3ww038PXXX7N3714Avv/+e9atW0fXrl0BiI+PJyEhgY4dO3rqBAYG0rp1azZs2ADA9u3bcTgcXmWio6OpV6+ep4yIyMWsViu33nort956q3o2c4BpsUAtO9SyY2qVLRGRQilLPRYxMTFs376dXr16+XzeMIwcvZvq448/TlJSErVq1cJqteJyuXjuuee46670K10JCQkAREZGetWLjIzk4MGDnjIBAQGUKFHikjLn6/uSmppKamqqZz85OTlHzklECgar1UqjRo38HUahYVosEJWeoJlOJRYiIoVRlhKL//73v6SkpFz2+Tp16hAfH5/toM575513WLBgAYsWLaJu3brExcURGxtLdHQ0/fv395S7eI150zQzXHc+ozKTJk1iwoQJ2TsBEREREZFrRJaGQtWpU4emTZte9nm73U7FihWzHdR5jz76KKNHj+bOO++kfv369OvXj//85z9MmjQJgKioKIBLeh6OHz/u6cWIiooiLS2NxMTEy5bxZcyYMSQlJXkehw8fzrHzEpH8z+12k5CQQEJCguZY5ADD7YY/XfCnCyMHe7ZFRCT/uKrlZs+ePevVc3Hw4EGmT5/OihUrciwwgJSUFCwW7xCtVqvnS75y5cpERUWxcuVKz/NpaWmsXr2aVq3SVxyJiYnBbrd7lTl27Bg7d+70lPElMDCQYsWKeT1E5NrhdDp57bXXeO2113A6nf4Op8Az3G740QE/OrC4lViIiBRGV7V+Yo8ePejZsyfDhg3j5MmTNG/eHLvdzh9//MHUqVO5//77cyS4W265heeee44KFSpQt25dduzYwdSpUxk4cCCQPgQqNjaWiRMnUr16dapXr87EiRMJCQmh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HI+IiOQ8JRYiIpLrEiyRtEub4tl/zY+xiIhI7lBiISIiuc80sbscADgs+uoRESmM9OkuIuJDWloaU6dOBeCRRx4hICDAzxEVbDaXkwc2vgvAKy17+TkaERHJDUosREQu49y5c/4OodAytdqsiEihU6CWOpk0aRKGYRAbG+s5Zpom48ePJzo6muDgYNq0acNPP/3kVS81NZWHHnqIkiVLEhoaSvfu3Tly5EgeRy8iBYndbuehhx7ioYcewm63+zucAi/C/INbLeu51bKegdYv/B2OiIjkggKTWGzdupU33niDBg0aeB1/4YUXmDp1KjNnzmTr1q1ERUXRoUMHTp065SkTGxvL0qVLWbJkCevWreP06dN069YNl8uV16chIgWEYRhEREQQERGBYWh91OwKIo1KlgQqWRKoadGFHRGRwqhAJBanT5+mb9++zJ49mxIlSniOm6bJ9OnTGTduHD179qRevXq8/fbbpKSksGjRIgCSkpKYM2cOU6ZMoX379jRu3JgFCxbw448/8tVXX/nrlERERERECpUCkVg88MAD3HzzzbRv397reHx8PAkJCXTs2NFzLDAwkNatW7NhwwYAtm/fjsPh8CoTHR1NvXr1PGV8SU1NJTk52eshItcOl8vFli1b2LJli3o3RUREMiHfT95esmQJ3333HVu3br3kuYSEBAAiIyO9jkdGRnLw4EFPmYCAAK+ejvNlztf3ZdKkSUyYMCG74YtIAeVyufjii/S5AI0aNcJqtfo5IhERkfwtX/dYHD58mIcffpgFCxYQFBR02XIXj382TTPDMdEZlRkzZgxJSUmex+HDh7MWvIgUaBaLhTp16lCnTh0slnz9UVkgmIYBpazpD0NLQomIFEb5usdi+/btHD9+nJiYGM8xl8vFmjVrmDlzJnv27AHSeyXKlCnjKXP8+HFPL0ZUVBRpaWkkJiZ69VocP36cVq1aXfa9AwMDCQwMzOlTEpECwmaz0auX7reQU1xWG9RNX13LdFlQaiEiUvjk68tw7dq148cffyQuLs7zaNq0KX379iUuLo4qVaoQFRXFypUrPXXS0tJYvXq1J2mIiYnBbrd7lTl27Bg7d+68YmIhIiI5R+tqiYgUfvm6x6Jo0aLUq1fP61hoaCgRERGe47GxsUycOJHq1atTvXp1Jk6cSEhICH369AEgLCyMQYMGMXLkSCIiIggP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Lyc+S+fMmUP79u2pWLFiboRY4FxNm7Zq1YojR47wxRdfYJomv//+O++//z4333xzXoTsxZbn75gP/fHHH7hcLiIjI72OR0ZGkpCQ4LNOQkKCz/JOp5M//viDMmXK5Fq8+d3VtKdcWU606ZQpUzhz5gy9evXKjRALlOy0Z7ly5Thx4gROp5Px48czePDg3Ay1wLiaNt23bx+jR49m7dq12Gz6OrrY1bRpmTJleOONN4iJiSE1NZX58+fTrl07Vq1axU033ZQXYedbV9Oe+/fvZ926dQQFBbF06VL++OMPhg8fzl9//aV5FmT/u+nYsWN8+eWXLFq0KLdCLHCupk1btWrFwoUL6d27N+fOncPpdNK9e3dmzJiRFyF70Sf5BQzD8No3TfOSYxmV93X8WpXV9pSMXW2bLl68mPHjx/Pxxx9TunTp3AqvwLma9ly7di2nT59m06ZNjB49mmrVqnHXXXflZpgFSmbb1OVy0adPHyZMmECNGjXyKrwCKSt/pzVr1qRmzZqe/ZYtW3L48GH+97//XfOJxXlZaU+3241hGCxcuJCwsDAApk6dyu23384rr7xCcHBwrsdbEFztd9O8efMoXrw4t956ay5FVnBlpU137drFiBEjeOqpp+jUqRPHjh3j0UcfZdiwYcyZMycvwvVQYgGULFkSq9V6SSZ4/PjxSzLG86KionyWt9lsRERE5FqsBcHVtKdcWXba9J133mHQoEG89957tG/fPjfDLDCy056VK1cGoH79+vz++++MHz9eiQVZb9NTp06xbds2duzYwYMPPgik/4gzTRObzcaKFSv417/+lSex51c59VnaokULFixYkNPhFThX055lypShbNmynqQCoHbt2pimyZEjR6hevXquxpzfZedv1DRN3nrrLfr160dAQEBuhlmgXE2bTpo0ieuvv55HH30UgAYNGhAaGsqNN97Is88+m6ejaDTHAggICCAmJoaVK1d6HV+5ciWtWrXyWadly5aXlF+xYgVNmzbFbrfnWqwFwdW0p1zZ1bbp4sWLGTBgAIsWLfLLWMv8Kqf+Rk3TJDU1NafDK5Cy2qbFihXjxx9/JC4uzvMYNmwYNWvWJC4ujubNm+dV6PlWTv2d7tix45oennve1bTn9ddfz9GjRzl9+rTn2N69e7FYLJQrVy5X4y0IsvM3unr1an755RcGDRqUmyEWOFfTpikpKVgs3j/prVYr8M9omjyT59PF86nzS3vNmTPH3LVrlxkbG2uGhoZ6VikYPXq02a9fP0/588vN/uc//zF37dplzpkzR8vNXiCr7Wmaprljxw5zx44dZkxMjNmnTx9zx44d5k8//eSP8POlrLbpokWLTJvNZr7yyiteS/udPHnSX6eQr2S1PWfOnGl+8skn5t69e829e/eab731llmsWDFz3Lhx/jqFfOdq/t1fSKtCXSqrbTpt2jRz6dKl5t69e82dO3eao0ePNgHzgw8+8Ncp5CtZbc9Tp06Z5cqVM2+//Xbzp59+MlevXm1Wr17dHDx4sL9OId+52n/3d999t9m8efO8DrdAyGqbzp0717TZbOasWbPMX3/91Vy3bp3ZtGlTs1mzZnkeuxKLC7zyyitmxYoVzYCAALNJkybm6tWrPc/179/fbN26tVf5VatWmY0bNzYDAgLMSpUqma+++moeR5y/ZbU9gUseFStWzNug87mstGnr1q19tmn//v3zPvB8Kivt+fLLL5t169Y1Q0JCzGLFipmNGzc2Z82aZbpcLj9Enn9l9d/9hZRY+JaVNp08ebJZtWpVMygoyCxRooR5ww03mJ9//rkfos6/svo3unv3brN9+/ZmcHCwWa5cOfORRx4xU1JS8jjq/C2rbXry5EkzODjYfOONN/I40oIjq2368ssvm3Xq1DGDg4PNMmXKmH379jWPHDmSx1GbpmGaed1HIiIiIiIihY3mWIiIiIiISLYpsRARERERkWxTYiEiIiIiItmmxEJERERERLJNiYWIiIiIiGSbEgsREREREck2JRYiIiIiIpJtSixERERERCTblFiIiEiuGD9+PI0aNfLb+z/55JPcd999mSo7atQoRowYkcsRiYgUbrrztoiIZJlhGFd8vn///sycOZPU1FQiIiLyKKp//P7771SvXp0ffviBSpUqZVj++PHjVK1alR9++IHKlSvnfoAiIoWQEgsREcmyhIQEz/Y777zDU089xZ49ezzHgoODCQsL80doAEycOJHVq1ezfPnyTNe57bbbqFatGpMnT87FyERECi8NhRIRkSyLioryPMLCwjAM45JjFw+FGjBgALfeeisTJ04kMjKS4sWLM2HCBJxOJ48++ijh4eGUK1eOt956y+u9fvvtN3r37k2JEiWIiIigR48eHDhw4IrxLVmyhO7du3sde//996lfvz7BwcFERETQvn17zpw543m+e/fuLF68ONttIyJyrVJiISIieeabb77h6NGjrFmzhqlTpzJ+/Hi6detGiRIl2Lx5M8OGDWPYsGEcPnwYgJSUFNq2bUuRIkVYs2YN69ato0iRInTu3Jm0tDSf75GYmMjOnTtp2rSp59ixY8e46667GDhwILt372bVqlX07NmTCzvtmzVrxuHDhzl48GDuNoKISCGlxEJERPJMeHg4L7/8MjVr1mTgwIHUrFmTlJQUxo4dS/Xq1RkzZgwBAQGsX78eSO95sFgsvPnmm9SvX5/atWszd+5cDh06xKpVq3y+x8GDBzFNk+joaM+xY8eO4XQ66dmzJ5UqVaJ+/foMHz6cIkWKeMqULVsWIMPeEBER8c3m7wBEROTaUbduXSyWf65pRUZGUq9ePc++1WolIiKC48ePA7B9+3Z++eUXihYt6vU6586d49dff/X5HmfPngUgKCjIc6xhw4a0a9eO+vXr06lTJzp27Mjtt99OiRIlPGWCg4OB9F4SERHJOiUWIiKSZ+x2u9e+YRg+j7ndbgDcbjcxMTEsXLjwktcqVaqUz/coWbIkkD4k6nwZq9XKypUr2bBhAytWrGDGjBmMGzeOzZs3e1aB+uuvv674uiIicmUaCiUiIvlWkyZN2LdvH6VLl6ZatWpej8utOlW1alWKFSvGrl27vI4bhsH111/PhAkT2LFjBwEBASxdutTz/M6dO7Hb7dStWzdXz0lEpLBSYiEiIvlW3759KVmyJD169GDt2rXEx8ezevVqHn74YY4cOeKzjsVioX379qxbt85zbPPmzUycOJFt27Zx6NAhPvzwQ06cOEHt2rU9ZdauXcuNN97oGRIlIiJZo8RCRETyrZCQENasWUOFChXo2bMntWvXZuDAgZw9e5ZixYpdtt59993HkiVLPEOqihUrxpo1a+jatSs1atTgiSeeYMqUKXTp0sVTZ/HixQwZMiTXz0lEpLDSDfJERKTQMU2TFi1aEBsby1133ZVh+c8//5xHH32UH374AZtN0w9FRK6GeixERKTQMQyDN954A6fTmanyZ86cYe7cuUoqRESyQT0WIiIiIiKSbeqxEBERERGRbFNiISIiIiIi2abEQkREREREsk2JhYiIiIiIZJsSCxERERERyTYlFiIiIiIikm1KLEREREREJNuUWIiIiIiISLYpsRARERERkWxTYiEiIiIiItn2/8H8lUkJCvGEAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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1Rk1ahR79+7l559/BiApKemCROTs66SkpEu2x6U8+eSTXHHFFbRp0+ayj5FTWm5WROQiQgM0fidPqT1F8s7KybDqlazLlWsAPeZ5b5tzBxz8X9Z1E++H5g9cXnx/+uyzzyhRogROp5OMjAy6dOnCpEmTKFu2LDfddBPTpk2jSZMmfPbZZ5w5c4bbbrvtkserX7++53lYWBjh4eEcPnwYgG3bttGsWTOvydeJiYkXPVafPn14+OGHufvuu1m1ahXvv/8+y5cv9yrjcrn4v//7P959911+/fVX0tLSSEtLIywszPOeaWlptG7dOttxlytXDoDDhw9Ts2ZNgAsmjJ/tKb/cieRjx45l7ty5LFmypEDvf6LEQkTEh0C7jaEtMrul73UEZFFasuKyO+DP9nQ77VmUFpEspZ2AE79lXS7yigu3pf6evbppJ3Ie13muv/56Xn31VQICAoiLiyMg4K/P0379+tGzZ08mTJjA9OnTuf322wkNDb3k8c6tD5k/vN3uzHlwOR222rFjR+677z769u3LzTffTHR09AVlxo0bx4QJE5g4cSL16tUjLCyMwYMHk56eDmQO7cqOc+M+myycjTs2NvaCnomzydL5PRnZ8Z///IfRo0fz1VdfeSU0BUGJhYiIiEhRExQO4XFZlwst7XtbduoGhec8rvOEhYVRtWpVn/s6duxIWFgYr776Kl9++SXLli3L1XvVrl2bjz/+2Gvb999/f9Hydrudnj17MnbsWL788kufZZYvX06XLl24++67gcxkYOfOndSqVQuAatWqERISwtdff33B8KfsSkxM5KmnniI9PZ3AwEAAFi1aRFxcHJUqVcrRsV588UWee+45Fi5cSOPGjS8rntxQYiEich5N1s5726lE97TMO28fJIqC/7oTKWaaP3D5w5TOHxrlJ3a7nd69ezNs2DCqVq16yWFL2TFw4EDGjRvHo48+yn333cf69es9cygu5tlnn+Xxxx/32VsBULVqVT788ENWrlxJqVKlGD9+PElJSZ7EIjg4mCeeeIKhQ4cSGBhIixYtOHLkCFu2bKFv377ZirtHjx6MGjWK3r1789RTT7Fz505Gjx7N008/7TUU6uxN9k6ePMmRI0fYtGkTgYGB1K5dG8gc/jRixAjmzJlDpUqVPL0gJUqUoESJEtmKJbc0eVtExAen2/Dh1gw+3JqB0eTtXDvj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V3+H4aEeCxERX4ybtANb/nzayr+xFANBJp0yh48CUKrKCT9HIyIFoX379kyfPh2n08nWrVvp06cPx48fZ+7cuf4OrdDLyMggIKDoDR9Vj4WIiC+WjaC4mgTF1dQcCxGRyxAUFERsbCzly5enbdu23H777SxatMirzPTp06lVqxbBwcHUrFmTKVOmXPR4LpeLvn37Eh8fT0hICDVq1OCll17y7F+2bBkBAQEkJSV51Xvssce47rrrANi7dy8333wzpUqVIiwsjDp16vDFF19c9D2PHTtGr169KFWqFKGhoXTo0IGdO3d69s+YMYOSJUvy8ccfU716dYKDg7nxxhvZv3+/13H++9//kpCQQHBwMJUrV2bUqFE4nX8tZW5ZFq+99hpdunQhLCyM5557LsvzHTlyJDNnzuSTTz7x9A4tWbIEgF9//ZXbb7+dUqVKER0dTZcuXdizZ89FzzOvqMdCRMSHDo71lK2W2WW/36ZrMCJSyKSnX3yfzQYOR/bKWhace2X8YmUDA3MW33l27drFggULvK7CT506lWeeeYbJkyfTqFEjNm7cSP/+/QkLC+Oee+654Bhut5vy5cvz3nvvUbp0aVauXMmAAQMoV64c3bt357rrrqNy5cq88847PP744wA4nU5mzZrF//3f/wFw//33k56ezrJlywgLC2Pr1q2UKFHionH37t2bnTt38umnnxIREcETTzxBx44d2bp1q+dcUlNTef7555k5cyaBgYEMGjSIO+64g++++w6AhQsXcvfdd/Pyyy9z7bXX8ssvvzBgwAAAnnnmGc97PfPMM4wZM4YJEyZgt9uzPN8hQ4awbds2UlJSmD59OgBRUVGkpqZy/fXXc+2117Js2TIcDgfPPfcc7du354cffiAwl/+Wl6LEQkTEh76OL7jatgOAe7nTz9GIiJxn9OiL76tWDe6666/XL74IGRm+y1aqBL17//V64kRITb2w3MiROQ7xs88+o0SJErhcLs6cOQPA+PHjPfufffZZxo0bR7du3QCIj49n69atvP766z4Ti4CAAEaNGuV5HR8fz8qVK3nvvffo3j1zOeu+ffsyffp0T2Lx+eefk5qa6tm/b98+brnlFurVqwdA5cqVLxr/2YTiu+++o3nz5gDMnj2bChUq8PHHH3PbbbcBmcOWJk+eTNOmTQGYOXMmtWrVYs2aNTRp0oTnn3+eJ5980nNOlStX5tlnn2Xo0KFeiUWPHj3o06ePVwyXOt8SJUoQEhJCWloasbGxnnKzZs3CZrPx5ptvenrcp0+fTsmSJVmyZAlt27a96DnnlhILEREpUBpYJvL3cP311/Pqq6+SmprKm2++yY4dO3jwwQcBOHLkCPv376dv377079/fU8fpdBIZGXnRY7722mu8+eab7N27l9OnT5Oenu61YlTv3r3517/+xffff0+zZs2YNm0a3bt3JywsDICHHnqIf/7znyxatIg2bdpwyy23UL9+fZ/vtW3bNhwOhydhAIiOjqZGjRps27bNs83hcNC4cWPP65o1a1KyZEm2bdtGkyZNWL9+PWvXruX555/3lDmbbKWmphIaGgrgdYzsnq8v69ev5+effyY8PNxr+5kzZ/jll18uWTe3lFiIiJzHABkuNyOXZV5hc/W4yJU+yTZzXjph0HqzIrny1FMX33f+8M0/r977dP4cssGDLzuk84WFhVG1alUAXn75Za6//npGjRrFs88+i9ududre1KlTvX64A9jtvpf4fu+993jkkUcYN24ciYmJhIeH8+KLL7J69WpPmbJly3LzzTczffp0KleuzBdffOGZdwDQr18/2rVrx+eff86iRYsYM2YM48aN8yQ85zIXWRfbGHPB3Dtfc/HObnO73YwaNcrTM3Ou4OBgz/OzyU9OztcXt9tNQkICs2fPvmBfmTJlLlk3t5RYiIhkQVfYRaTQyck4+fwqm0PPPPMMHTp04J///CdxcXFcccUV7Nq1i7vOHbZ1CcuXL6d58+YMGjTIs83XFfh+/fpxxx13UL58eapUqUKLFi289leoUIGBAwcycOBAhg0bxtSpU30mFrVr18bpdLJ69WrPUKg//viDHTt2UKtWLU85p9PJunXraNKkCQDbt2/n+PHj1KxZE4CrrrqK7du3e5Ks7MrO+QYGBuJyuby2XXXVVbz77ruULVuWiIiIHL1nbmlGooiIDw6bxePNg3i8eRA2u67B5JbLbofmQdA8CLcmw4v8LbVq1Yo6deow+s/5ISNHjmTMmDG89NJL7Nixgx9//JHp06d7zcM4V9WqVVm3bh0LFy5kx44djBgxgrVr115Qrl27dkRGRvLcc89x7733eu0bPHgwCxcuZPfu3WzYsIFvvvnGK0k4V7Vq1ejSpQv9+/dnxYoV/O9//+Puu+/miiuu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OVKJBTlv0xMnTrBu3To2btzIAw88AGT+iDPG4HA4WLRoETfccEOBxF5Y5dVnabNmzZg1a1Zeh1fkXE57litXjiuuuMKTVADUqlULYwwHDhygWrVq+RpzYZebv1FjDNOmTaNnz54EBgbmZ5hFyuW06ZgxY2jRogWPP/44APXr1ycsLIxrr72W5557rkBH0WiOBRAYGEhCQgKLFy/22r548WKaN2/us05iYuIF5RctWkTjxo0JCAjIt1iLgstpT7m0y23TuXPn0rt3b+bMmeOXsZaFVV79jRpjSEtLy+vwiqSctmlERAQ//vgjmzZt8jwGDhxIjRo12LRpE02bNi2o0AutvPo73bhx4996eO5Zl9OeLVq04LfffuPkyZOebTt27MBms1G+fPl8jbcoyM3f6NKlS/n555/p27dvfoZY5FxOm6ampmKzef+kt9vtwF+jaQpMgU8XL6TOLu311ltvma1bt5rBgwebsLAwzyoFTz75pOnZs6en/NnlZh955BGzdetW89Zbb2m52XPktD2NMWbjxo1m48aNJiEhwfTo0cNs3LjRbNmyxR/hF0o5bdM5c+YYh8NhXnnlFa+l/Y4fP+6vUyhUctqekydPNp9++qnZsWOH2bFjh5k2bZqJiIgww4cP99cpFDqX8//+XFoV6kI5bdMJEyaY+fPnmx07dpjNmzebJ5980gDmww8/9NcpFCo5bc8TJ06Y8uXLm1tvvdVs2bLFLF261FSrVs3069fPX6dQ6Fzu//u7777bNG3atKDDLRJy2qbTp083DofDTJkyxfzyyy9mxYoVpnHjxqZJkyYFHrsSi3O88sor5sorrzSBgYHmqquuMkuXLvXsu+eee0zLli29yi9ZssQ0atTIBAYGmkqVKplXX321gCMu3HLansAFjyuvvLJggy7kctKmLVu29Nmm99xzT8EHXkjlpD1ffvllU6dOHRMaGmoiIiJMo0aNzJQpU4zL5fJD5IVXTv/fn0uJhW85adMXXnjBVKlSxQQHB5tSpUqZa665xnz++ed+iLrwyunf6LZt20ybNm1MSEiIKV++vHn00UdNampqAUdduOW0TY8fP25CQkLMG2+8UcCRFh05bdOXX37Z1K5d24SEhJhy5cqZu+66yxw4cKCAozbGMqag+0hERERERKS40RwLERERERHJNSUWIiIiIiKSa0osREREREQk15RYiIiIiIhIrimxEBERERGRXFNiISIiIiIiuabEQkREREREck2JhYiIiIiI5JoSCxERyRcjR46kYcOGfnv/ESNGMGDAgGyVHTJkCA899FA+RyQiUrzpztsiIpJjlmVdcv8999zD5MmTSUtLIzo6uoCi+suhQ4eoVq0aP/zwA5UqVcqy/OHDh6lSpQo//PAD8fHx+R+giEgxpMRCRERyLCkpyfP83Xff5emnn2b79u2ebSEhIURGRvojNABGjx7N0qVLWbhwYbbr3HLLLVStWpUXXnghHyMTESm+NBRKRERyLDY21vOIjIzEsqwLtp0/FKp379507dqV0aNHExMTQ8mSJRk1ahROp5PHH3+cqKgoypcvz7Rp07ze69dff+X222+nVKlSREdH06VLF/bs2XPJ+ObNm0fnzp29tn3wwQfUq1ePkJAQoqOjadOmDadOnfLs79y5M3Pnzs1124iI/F0psRARkQLzzTff8Ntvv7Fs2TLGjx/PyJEj6dSpE6VKlWL16tUMHDiQgQMHsn//fgBSU1O5/vrrKVGiBMuWLWPFihWUKFGC9u3bk56e7vM9jh07xubNm2ncuLFn28GDB7nzzjvp06cP27ZtY8mSJXTr1o1zO+2bNGnC/v372bt3b/42gohIMaXEQkRECkxUVBQvv/wyNWrUoE+fPtSoUYPU1FSeeuopqlWrxrBhwwgMDOS7774DMnsebDYbb775JvXq1aNWrVpMnz6dffv2sWTJEp/vsXfvXowxxMXFebYdPHgQp9NJt27dqFSpEvXq1WPQoEGUKFHCU+aKK64AyLI3REREfHP4OwAREfn7qFOnDjbbX9e0YmJiqFu3rue13W4nOjqaw4cPA7B+/Xp+/vlnwsPDvY5z5swZfvnlF5/vcfr0aQCCg4M92xo0aEDr1q2pV68e7dq1o23bttx6662UKlXKUyYkJATI7CUREZGcU2IhIiIFJiAgwOu1ZVk+t7ndbgDcbjcJCQnMnj37gmOVKVPG53uULl0ayBwSdbaM3W5n8eLFrFy5kkWLFjFp0iSGDx/O6tWrPatAHT169JLHFRGRS9NQKBERKbSuuuoqdu7cSdmyZalatarX42KrTlWpUoWIiAi2bt3qtd2yLFq0aMGoUaPYuHEjgYGBzJ8/37N/8+bNBAQEUKdOnXw9JxGR4kqJhYiIFFp33XUXpUuXpkuXLixfvpzdu3ezdOlSHn74YQ4cOOCzjs1mo02bNqxYscKzbfXq1YwePZp169axb98+PvroI44cOUKtWrU8ZZYvX861117rGRIlIiI5o8RCREQKrdDQUJYtW0bFihXp1q0btWrVok+fPpw+fZqIiIiL1hswYADz5s3zDKmKiIhg2bJldOzYkerVq/Ovf/2LcePG0aFDB0+duXPn0r9//3w/JxGR4ko3yBMRkWLHGEOzZs0YPHgwd955Z5blP//8cx5//HF++OEHHA5NPxQRuRzqsRARkWLHsizeeOMNnE5ntsqfOnWK6dOnK6kQEckF9ViIiIiIiEiuqcdCRERERERyTYmFiIiIiIjkmhILERERERHJNSUWIiIiIiKSa0osREREREQk15RYiIiIiIhIrimxEBERERGRXFNiISIiIiIiuabEQkREREREck2JhYiIiIiI5Nr/A1Uic8ccNQ2uAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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NYUSEQnA6/WyevVqAN8M3HL6DNOE/WVfzEZL9bAQ+S348MMPiYyM9K1ffvnlvP322359IZo1a8Zf//pXbrvtNl544YXTeh+n04nD4SA8PJzk5OQTlvv55595//33+eqrr+jevTsACxYsIC0tjffee48//elPPPHEE5x//vl+sbRr1w6AvLw8nnnmGZ577jmGDx8OQIsWLbj44osrFa/L5eKFF16gY8eOABw6dIicnBwGDBhAixYtAGjbtm2lzllbKLEQEQnAarVyySWX+JZFRGqTSZMmAWC3233bLrroIrp27Vquvf69995bruwFF1zAeeedV67skR/9x5Y9vlnSqerVqxcvvviibz0iIgKA//73v0ydOpWNGzeSm5uL2+2muLiYgoICX5masGnTJmw2G126dPFti4+Pp3Xr1mzatAkoq7H405/+dMLjS0pK6N27d5XicDgcnHPOOb71uLg4RowYQb9+/bjsssvo06cPQ4YMISUlpUrvEwxqCiUiEoDVaqV379707t1biUUNMFVpIVIlDocDh8Ph11zGarXicDiw2WzVXvZ0RERE0LJlS98rJSWFHTt2cMUVV9C+fXveffdd1q5dy/PPPw/ga45ksVj8+ngcu68qjj/nsduPXG9YWNgJjz/ZPsCXpB37PoHiDgsLK9fMae7cuaxatYru3bvz1ltvcdZZZ/lqzesSJRYiIiIickZ8++23uN1unnrqKbp27cpZZ53F3r17/co0aNCAjIwMvx/oFfXzcDgcFXYyP/vss3G73axZs8a37eDBg2zZssXX9Oicc87h888/D3h8q1atCAsLO+H+IyNepaenn3Lcxzr33HOZOHEiK1eupH379rzxxhunfGxtocRCRCQA0zQDjo4ip2cnqXzqOZ9PPefzsadLxQeISL3UokUL3G43s2bNYuvWrcyfP5+XXnrJr0zPnj3Zv38/TzzxBL/++ivPP/88n3zyyUnP27RpU9asWcP27ds5cOAA3gCDRLRq1YpBgwYxatQovvzyS/73v/9xww030LBhQwYNGgSUdc7+5ptvuP322/nhhx/46aefePHFFzlw4AChoaHcf//93Hffffzf//0fv/76K6tXr+bVV18FoGXLlqSlpTF58mS2bNnCRx99xFNPPVXhPdm2bRsTJ05k1apV7NixgyVLlvglO3WJEgsRkQBcLpdv1JXqqIL/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IiIiUmWqsRARERERkSpTYiEiIiIiIlWmxEJERERERKpMiYWIiIiIiFSZEotjvPDCCzRr1ozQ0FA6d+7MF198cdLyy5cvp3PnzoSGhtK8eXNeeumlMxRp3VCZ+5mens7QoUNp3bo1FouFsWPHnrlA65DK3NN//etfXHbZZTRo0IDo6Gi6devGp59+egajrf0qcz+//PJLLrroIuLj4wkLC6NNmzbMnDnzDEZbN1T27+gRX331FTabjU6dOtVsgHVQZe7psmXLMAyj3Ounn346gxHXbpX9jJaUlPDggw/SpEkTQkJCaNGiBXPmzDlD0dYNlbmnI0aMCPgZbdeu3RmMuPar7Od0wYIFdOzYkfDwcFJSUrjppps4ePDgGYr2GKaYpmmaCxcuNO12uzl79mxz48aN5t13321GRESYO3bsCFh+69atZnh4uHn33XebGzduNGfPnm3a7XbznXfeOcOR106VvZ/btm0z77rrLvO1114zO3XqZN59991nNuA6oLL39O677zanT59ufv311+aWLVvMiRMnmna73fzuu+/OcOS1U2Xv53fffWe+8cYb5vr1681t27aZ8+fPN8PDw81//OMfZzjy2quy9/SI7Oxss3nz5mbfvn3Njh07nplg64jK3tP//ve/JmBu3rzZTE9P973cbvcZjrx2Op3P6MCBA80uXbqYS5cuNbdt22auWbPG/Oqrr85g1LVbZe9pdna232dz165dZlxcnPnoo4+e2cBrscre0y+++MK0WCzmM888Y27dutX84osvzHbt2plXXXXVGY7cNJVYHHbhhReat956q9+2Nm3amA888EDA8vfdd5/Zpk0bv21jxowxu3btWmMx1iWVvZ/H6tGjhxKLAKpyT484++yzzSlTplR3aHVSddzPP/zhD+YNN9xQ3aHVWad7T6+55hrzoYceMh999FElFsep7D09klhkZWWdgejqnsrez08++cR0Op3mwYMHz0R4dVJV/5YuWrTINAzD3L59e02EVydV9p4++eSTZvPmzf22Pfvss2ajRo1qLMYTUVMooLS0lLVr19K3b1+/7X379mXlypUBj1m1alW58v369ePbb7/F5XLVWKx1wencTzm56rinXq+XvLw84uLiaiLEOqU67uf333/PypUr6dGjR02EWOec7j2dO3cuv/76K48++mhNh1jnVOVzeu6555KSkkLv3r3573//W5Nh1hmncz/ff/99zj//fJ544gkaNmzIWWedxYQJEygqKjoTIdd61fG39NVXX6VPnz40adKkJkKsc07nnnbv3p3du3fz8ccfY5om+/bt45133uH3v//9mQjZj+2Mv2MtdODAATweD0lJSX7bk5KSyMjICHhMRkZGwPJut5sDBw6QkpJSY/HWdqdzP+XkquOePvXUUxQUFDBkyJCaCLFOqcr9bNSoEfv378ftdjN58mRuueWWmgy1zjide/rzzz/zwAMP8MUXX2Cz6evoeKdzT1NSUnj55Zfp3LkzJSUlzJ8/n969e7Ns2TIuvfTSMxF2rXU693Pr1q18+eWXhIaGsmjRIg4cOMDtt9/OoUOH1M+Cqn83paen88knn/DGG2/UVIh1zunc0+7du7NgwQKuueYaiouLcbvdDBw4kFmzZp2JkP3oL/kxDMPwWzdNs9y2isoH2v5bVdn7KRU73Xv65ptvMnnyZP7973+TmJhYU+HVOadzP7/44gvy8/NZvXo1DzzwAC1btuS6666ryTDrlFO9px6Ph6FDhzJlyhTOOuusMxVenVSZz2nr1q1p3bq1b71bt27s2rWLv//977/5xOKIytxPr9eLYRgsWLAAp9MJwIwZM/jjH//I888/T1hYWI3HWxec7nfTvHnziImJ4aqrrqqhyOquytzTjRs3ctddd/HII4/Qr18/0tPTuffee7n11lt59dVXz0S4PkosgISEBKxWa7lMMDMzs1zGeERycnLA8jabjfj4+BqLtS44nfspJ1eVe/rWW28xcuRI3n77bfr06VOTYdYZVbmfzZo1A6BDhw7s27ePyZMnK7Gg8vc0Ly+Pb7/9lu+//54///nPQNmPONM0sdlsLFmyhN/97ndnJPbaqrr+lnbt2pXXX3+9usOrc07nfqakpNCwYUNfUgHQtm1bTNNk9+7dtGrVqkZjru2q8hk1TZM5c+YwbNgwHA5HTYZZp5zOPZ02bRoXXXQR9957LwDnnHMOERERXHLJJTz22GNntBWN+lgADoeDzp07s3TpUr/tS5cupXv37gGP6datW7nyS5Ys4fzzz8dut9dYrHXB6dxPObnTvadvvvkmI0aM4I033ghKW8vaqro+o6ZpUlJSUt3h1UmVvafR0dH8+OOPrFu3zve69dZbad26NevWraNLly5nKvRaq7o+p99///1vunnuEadzPy+66CL27t1Lfn6+b9uWLVuwWCw0atSoRuOtC6ryGV2+fDm//PILI0eOrMkQ65zTuaeFhYVYLP4/6a1WK3C0Nc0Zc8a7i9dSR4b2evXVV82NGzeaY8eONSMiInyjFDzwwAPmsGHDfOWPDDd7zz33mBs3bjRfffVVDTd7jMreT9M0ze+//978/vvvzc6dO5tDhw41v//+e3PDhg3BCL9Wquw9feONN0ybzWY+//zzfkP7ZWdnB+sSapXK3s/nnnvOfP/9980tW7aYW7ZsMefMmWNGR0ebDz74YLAuodY5nX/3x9KoUOVV9p7OnDnTXLRokbllyxZz/fr15gMPPGAC5rvvvhusS6hVKns/8/LyzEaNGpl//OMfzQ0bNpjLly83W7VqZd5yyy3BuoRa53T/3d9www1mly5dznS4dUJl7+ncuXNNm81mvvDCC+avv/5qfvnll+b5559vXnjhhWc8diUWx3j++efNJk2amA6HwzzvvPPM5cuX+/YNHz7c7NGjh1/5ZcuWmeeee67pcDjMpk2bmi+++OIZjrh2q+z9BMq9mjRpcmaDruUqc0979OgR8J4OHz78zAdeS1Xmfj777LNmu3btzPDwcDM6Oto899xzzRdeeMH0eDxBiLz2quy/+2MpsQisMvd0+vTpZosWLczQ0FAzNjbWvPjii82PPvooCFHXXpX9jG7atMns06ePGRYWZjZq1MgcN26cWVhYeIajrt0qe0+zs7PNsLAw8+WXXz7DkdYdlb2nzz77rHn22WebYWFhZkpKinn99debu3fvPsNRm6Zhmme6jkREREREROob9bEQEREREZEqU2IhIiIiIiJVpsRCRERERESqTImFiIiIiIhUmRILERERERGpMiUWIiIiIiJSZUosRERERESkypRYiIiIiIhIlSmxEBGRGjF58mQ6deoUtPd/+OGHGT169CmVnTBhAnfddVcNRyQiUr9p5m0REak0wzBOun/48OE899xzlJSUEB8ff4aiOmrfvn20atWKH374gaZNm1ZYPjMzkxYtWvDDDz/QrFmzmg9QRKQeUmIhIiKVlpGR4Vt+6623eOSRR9i8ebNvW1hYGE6nMxihATB16lSWL1/Op59+esrHXH311bRs2ZLp06fXYGQiIvWXmkKJiEilJScn+15OpxPDMMptO74p1IgRI7jqqquYOnUqSUlJxMTEMGXKFNxuN/feey9xcXE0atSIOXPm+L3Xnj17uOaaa4iNjSU+Pp5Bgwaxffv2k8a3cOFCBg4c6LftnXfeoUOHDoSFhREfH0+fPn0oKCjw7R84cCBvvvlmle+NiMhvlRILERE5Y/7zn/+wd+9eVqxYwYwZM5g8eTIDBgwgNjaWNWvWcOutt3Lrrbeya9cuAAoLC+nVqxeRkZGsWLGCL7/8ksjISPr3709paWnA98jKymL9+vWcf/75vm3p6elcd9113HzzzWzatIlly5YxePBgjq20v/DCC9m1axc7duyo2ZsgIlJPKbEQEZEzJi4ujmeffZbWrVtz880307p1awoLC5k0aRKtWrVi4sSJOBwOvvrqK6Cs5sFisfDKK6/QoUMH2rZty9y5c9m5cyfLli0L+B47duzANE1SU1N929LT03G73QwePJimTZvSoUMHbr/9diIjI31lGjZsCFBhbYiIiARmC3YAIiLy29GuXTsslqPPtJKSkmjfvr1v3Wq1Eh8fT2ZmJgBr167ll19+ISoqyu88xcXF/PrrrwHfo6ioCIDQ0FDfto4dO9K7d286dOhAv3796Nu3L3/84x+JjY31lQkLCwPKaklERKTylFiIiMgZY7fb/dYNwwi4zev1AuD1euncuTMLFiwod64GDRoEfI+EhASgrEnUkTJWq5WlS5eycuVKlixZwqxZs3jwwQf5//bullWZIAzj+LVyNBg2idGiCIvNJIjNokWwiU3QqsVmsdj8AhajJk1+AHctC4IgYjEIYhNsYhF82sKB55znZTgg8v/Blpm5FyZe3LOzvu8Ht0Bdr9dv3wsA+B5HoQAALyubzepwOCgejyuVSn16vrp1KplMyrZt7ff7T+OWZSmfz6vf72uz2SgSiWg+nwfzu91O4XBYmUzmR/cEAO+KYAEAeFn1el2xWEyVSkWe5+l4PGq5XKrdbut8Pv+2JhQKqVgsarVaBWO+72swGGi9Xut0Omk2m+lyuchxnGCN53kqFArBkSgAwL8hWAAAXlY0GpXrukokEqpWq3IcR41GQ/f7XbZtf1nXarU0nU6DI1W2bct1XZXLZaXTafV6PQ2HQ5VKpaBmMpmo2Wz++J4A4F3xgzwAwNt5Pp/K5XLqdDqq1Wp/XL9YLNTtdrXdbvXxweeHAPA/6FgAAN6OZVkajUZ6PB5/tf52u2k8HhMqAMAAHQsAAAAAxuhYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADD2C/AdRxVmoSSsAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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2BCQ17/clFiIiPhj2Qip0ZKQGi2xbPqoFBHJqf79+2NZFpZl4XQ6qVatGkOHDuXUqVPZ2r9KlSpMmDAhT2M6k2iULl2a06dP+2z76aefvPFebr/++ivt2rUjJCSEChUq8PLLL/tc1Dp8+DB9+/aldu3a2Gw2hgwZkukYU6ZM4dprr6V06dKULl2ajh078tNPP13Gs1BiISKSiQEa2PfTPiaV9jGp2C//d4yISJHQtWtXDh8+zK5du3j11VeZPHkyQ4cODXRYlChRgrlz5/qs++ijj6hUqdJljyUpKYlOnToRExPDmjVrmDhxIv/6178YN26ct0xqaiply5bl+eefp3Hjxn6Ps3jxYu68805++OEHVq1aRaVKlejcuTMHDx68XKeixEJExJ/nHTP5OOg1Pg56DSfuQIdT6MXyB+1tG2hv20BH21rUu0zkyhAcHEx0dDSxsbH07duXu+66iy+++IIaNWrwr3/9y6fspk2bsNls/P77736PZVkWH3zwAX/7298IDQ2lZs2azJs3z6fMN998Q61atQgJCeH6669nz549fo9177338tFHH3mXU1JSmDNnDvfee69PuT///JM777yTihUrEhoaSsOGDfn44499yng8HsaMGUONGjUIDg6mUqVKvPbaaz5ldu3axfXXX09oaCiNGzdm1apV3m2zZs3i9OnTTJs2jQYNGtC7d2+ee+45xo0b5221qFKlCm+++Sb33HMP4eHhfs9p1qxZDB48mCZNmlCnTh2mTJmCx+Ph+++/91s+PyixEBHxwxhD/CkP8ac8GmORB8pax2hi+50mtt9p+tedykXkyhMSEkJ6ejoDBgxg6tSpPts++ugjrr32WqpXr37B/UeOHEmfPn3YuHEj3bt356677uLYsWMA7N+/n969e9O9e3c2bNjA/fffz7PPPuv3OP369WPZsmXs27cPgM8//5wqVapw1VVX+ZQ7ffo0zZo146uvvmLTpk0MGjSIfv36sXr1am+ZYcOGMWbMGIYPH86WLVuYPXs2UVFRPsd5/vnnGTp0KBs2bKBWrVrceeeduFwZNx9dtWoV7dq1Izj47P2PunTpwqFDhy6YGGVHcnIy6enpRERcvokylFiIiPjh8hgmr0lj8po0PG5XoMMRESn0fvrpJ2bPnk2HDh2477772LZtm3cMQHp6OjNnzmTAgAEXPUb//v258847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X78+ACdOnODNN99k0qRJ3HvvvQBUr16da665JkfxpqenM3nyZBo3bgzAsWPHSExMpEePHlSvXh2AunXr5uiYBUWhSCzS09O544478Hg82XrTGWN8sjx/Gd/5Zc43evRoRo4ceWkBi0iht9FViQ9WHATAU7no9H8VkaJh1KhRADz11FOEhYUBGV14/ve//3HVVVf5jEl94403SE9PZ8iQId5eH2vWrGH+/Pk0bNiQW265xVt2woQJJCcnM3jwYO+F2Q0bNtCsWbMcx3j99dfzzjvveJfPxPnDDz8watQotmzZQlJSEi6Xi9OnT3Pq1ClvmfywdetWHA6HTy+WyMhIateuzdatW4GMc73tttsuuH9qaiodOnTIVRxBQUE0atTIuxwREUH//v3p0qULnTp1omPHjvTp04fy5cvn6nUCocB3hUpPT6dPnz7s3r2bhQsXelsrAKKjo0lLS/P2ezsjPj7e2ywVHR3NH3/8kem4R44cuWjT1bBhw0hMTPQ+9u/fn0dnJCKFwSjXXXzuuY7PPdfhtpyBDqfQO0QUP7ib8IO7CYvcOf+BIiKFT1hYGDVq1PA+ypcvz969e+nevTsNGjTg888/Z+3atbz99tsA3u5INpstU6vmmW25caGW0nMvNoeEhFxw/4ttg4y4z38df3GHhIRkurg9depUVq1aRZs2bfjkk0+oVasWP/7440VfryAq0C0WZ5KKHTt28MMPPxAZGemzvVmzZjidTu8gb4DDhw+zadMmXn/9dQBat25NYmIiP/30Ey1atABg9erVJCYmepvB/AkODiY4ODifzkxECjrL7qTUNXcBYHcoscitw85y3Nf2xUCHIVJkPPfcc0BGl6Uz2rZtS6tWrbw/cM946qmnMpW9+uqrueqqqzKVPdNN6dyy5w+kzo2ff/4Zl8vF2LFjva/96aef+pQpW7YscXFxPj/4N2zYcNHjBgUF+cwI6k+9evVwuVysXr3a+xvwzz//ZPv27d6uR40aNeL777/322ulZs2ahISE8P3333P//fdn2l62bFkg47fomW76WcV9rqZNm9K0aVOGDRtG69atmT17Nq1atcr2/gVBQBOLkydP+sxnvHv3bjZs2EBERAQxMTHceuutrFu3jq+++gq32+0dExEREUFQUBDh4eEMHDiQJ598ksjISCIiIhg6dCgNGzakY8eOQEYfta5du/LAAw/w3nvvATBo0CB69OhB7dq1L/9Ji4iIiORSUFBQpnV2u93vWIi8KJtXqlevjsvlYuLEidx0002sWLGCd99916dM+/btOXLkCK+//jq33nor8+fP59tvv/XptXK+KlWqsHr1avbs2UPx4sWJiIjIlDTVrFmTXr16eX8TlihRgmeffZYKFSrQq1cvIKPHSsOGDRk8eDAPPfQQQUFB/PDDD9x2222UKVOGZ555hqeffpqgoCDatm3LkSNH2Lx5MwMHDqRGjRrExsYyYsQIXn31VXbs2MHYsWOzrJPdu3fz/vvv07NnT2JiYti2bRvbt2/nnnvuuYQaDqyAdoX6+eefvdkZwBNPPEHTpk158cUXOXDgAPPmzePAgQM0adKE8uXLex/nzuY0fvx4br75Zvr06UPbtm0JDQ3lv//9r88/waxZs2jYsCGdO3emc+fONGrUiBkzZlz28xURERG5kjVp0oRx48YxZswYGjRowKxZsxg9erRPmbp16zJ58mTefvttGjduzE8//cTQoUMvetyhQ4dit9upV68eZcuW9d6a4HxTp06lWbNm9OjRg9atW2OM4ZtvvvG20NSqVYsFCxbwyy+/0KJFC1q3bs2XX37pHeA+fPhwnnzySV588UXq1q3L7bffTnx8PJDRyvPxxx/z22+/0bhxY8aMGcOrr76aZZ2Ehoby22+/ccstt1CrVi0GDRrEI488woMPPpjlvgWNZTQ1R7YkJSURHh5OYmLiRTNmESn8pizdRfH5j5G0ZzMAK9tPZ/qg6wIcVeHW+sUv6bRxPgBf17qGyQ91pWW1yCz2EpHTp0+ze/duqlatSrFixQIdjhRRF3uf5eQ3cIEeYyEiEggGQ6z1B4sOHcpY1vWXXKtrdvHy8Y8AqGz/E+ga2IBERCTPKbEQEfHDZllcWynjI3K3rcBPoFfoKFUTESl6lFiIiPhht1l0qJbxETnLlncDF69Uhe/+sSIiklO6DCciIiIiIrmmFgsRET+MMaS5jfe5iIiIXJwSCxERP1wew6gVqQB4KroCHI2IiEjBp65QIiIiIiKSa2qxEBHxw2GzeO7aYAAetOujMrfcdjv8VZ8ej65piYgURfq2FBE5jzFgWRZBtoy5jCxLcxrlmmWB/a96NKpPEZGiSImFiIgfs1038IPVBACPJkvNcxoPLyJZ2bNnD1WrVmX9+vU0adIk0OFINqg9WkTEjy9crRn/eyXG/14JT6CDKQJ2eirxwq9388Kvd/N+2o2BDkdE8ln//v2xLAvLsnA4HFSqVIm///3vJCQkBDq0IqV///7cfPPNgQ7DSy0WIiL+GA+pBzb/9bR9YGMpAtJwUuaPjB8Ux6sVD3A0InI5dO3alalTp+JyudiyZQsDBgzg+PHjfPzxx4EOrcBLT0/H6XQGOowcU4uFiIg/lo3gmDoEx9TRGAsRkUsQHBxMdHQ0FStWpHPnztx+++0sWLDAp8zUqVOpW7cuxYoVo06dOkyePPmCx3O73QwcOJCqVasSEhJC7dq1efPNN73bly5ditPpJC4uzme/J598kuuuuw6AvXv3ctNNN1G6dGnCwsKoX78+33zzzQVfMyEhgXvuuYfSpUsTGhpKt27d2LFjh3f7tGnTKFWqFF988QW1atWiWLFidOrUif379/sc57///S/NmjWjWLFiVKtWjZEjR+JynZ3K3LIs3n33XXr16kVYWBivvvpqluc7YsQIpk+fzpdffultHVq8eDEABw8e5Pbbb6d06dJERkbSq1cv9uzZc8HzzCtqsRAR8aOULQVntRoA2Gz2AEcjInKetLQLb7PZwOHIXlnLgnOvjF+obFBQzuI7z65du5g/f77PVfgpU6bw0ksvMWnSJJo2bcr69et54IEHCAsL49577810DI/HQ8WKFfn0008pU6YMK1euZNCgQZQvX54+ffpw3XXXUa1aNWbMmMFTTz0FgMvlYubMmfzzn/8E4OGHHyYtLY2lS5cSFhbGli1bKF78wq2o/fv3Z8eOHcybN4+SJUvyzDPP0L17d7Zs2eI9l+TkZF577TWmT59OUFAQgwcP5o477mDFihUAfPfdd9x999289dZbXHvttfz+++8MGjQIgJdeesn7Wi+99BKjR49m/Pjx2O32LM936NChbN26laSkJKZOnQpAREQEycnJXH/99Vx77bUsXboUh8PBq6++SteuXdm4cSNBufxbXowSCxERPz4I+hdX27YDcB/zAxxN4RdhEmlk7QLgKms7cG1gAxIp7EaNuvC2mjXhrrvOLr/xBqSn+y9bpQr07392ecIESE7OXG7EiByH+NVXX1G8eHHcbjenT58GYNy4cd7tr7zyCmPHjqV3794AVK1alS1btvDee+/5TSycTicjR470LletWpWVK1fy6aef0qdPHwAGDhzI1KlTvYnF119/TXJysnf7vn37uOWWW2jYsCEA1apVu2D8ZxKKFStW0KZNGwBmzZpFbGwsX3zxBbfddhuQ0W1p0qRJtGzZEoDp06dTt25dfvrpJ1q0aMFrr73Gs88+6z2natWq8corr/D000/7JBZ9+/ZlwIABPjFc7HyLFy9OSEgIqampREdHe8vNnDkTm83GBx984G1xnzp1KqVKlWLx4sV07tz5guecW0osREQk38UQzw329QDstccCAwMbkIjku+uvv5533nmH5ORkPvjgA7Zv386jjz4KwJEjR9i/fz8DBw7kgQce8O7jcrkIDw+/4DHfffddPvjgA/bu3UtKSgppaWk+M0b179+fF154gR9//JFWrVrx0Ucf0adPH8LCwgB47LHH+Pvf/86CBQvo2LEjt9xyC40aNfL7Wlu3bsXhcHgTBoDIyEhq167N1q1bvescDgfNmzf3LtepU4dSpUqxdetWWrRowdq1a1mzZg2vvfaat8yZZCs5OZnQ0FAAn2Nk93z9Wbt2LTt37qREiRI+60+fPs3vv/9+0X1zS4mFiMh5DJDu9jBiacYVNnffC1zpExEJlOeeu/A223lDaP+6eu/X+WPIhgy55JDOFxYWRo0aGV1K33rrLa6//npGjhzJK6+8gseTMd/elClTfH64A9jt/ruffvrpp/zjH/9g7NixtG7dmhIlSvDGG2+wevVqb5ly5cpx0003MXXqVKpVq8Y333zjHXcAcP/999OlSxe+/vprFixYwOjRoxk7dqw34TmXucC82MaYTGPv/I3FO7PO4/EwcuRIb8vMuYoVK+Z9fib5ycn5+uPxeGjWrBmzZs3KtK1s2bIX3Te3lFiIiMhlZ9CNLERyJSf95POrbA699NJLdOvWjb///e/ExMRQoUIFdu3axV3ndtu6iGXLltGmTRsGDx7sXefvCvz999/PHXfcQcWKFalevTpt27b12R4bG8tDDz3EQw89xLBhw5gyZYrfxKJevXq4XC5Wr17t7Qr1559/sn37durWrest53K5+Pnnn2nRogUA27Zt4/jx49SpUweAq666im3btnmTrOzKzvkGBQXhdrt91l111VV88sknlCtXjpIlS+boNXNLs0KJiPjhsFk81SaYp9oEY7frGkxuue12aBMMbYLxnH81VUSuCO3bt6d+/fqM+mt8yIgRIxg9ejRvvvkm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CArK3MVEsi/IYaNltYzxFb3vbwWqTxGRQiEpKYl/v/5s1gW5QrpCBQUF0axZMxYu9O3ztnDhQtq0aROgqESkoHM6nTh11+2843RmPEREpEi6IrpCwdnpZt99911at27N+++/z5QpU9i8eTOVK2e+Wdv5zkw3q65QIiIiInKlyMlv4CuiKxTA7bffzp9//snLL7/M4cOHadCgAd988022kgoREREREbm4K6bFIrfUYiEiIiIiVxq1WIiI5JLL5eKbb74BoHv37rpJXm65XPDJJxnPb78dVJ8iIkWOPtlFRPzweDysW5dxp+OuXbsGOJoiwOOBHTvOPhcRkSJHiYWIiB92u50bbrjB+1xEREQuTomFiIgfdrud6667LtBhiIiIFBpXxH0sREREREQkf6nFQkTED2MMycnJAISGhmJZVoAjEhERKdjUYiEi4kd6ejpvvPEGb7zxBunp6YEOR0REpMBTi0U2nbndR1JSUoAjEZHLIS0tjdTUVCDj/z4oKCjAERVyaWnwV32SlASqTxGRQuHMb9/s3PpON8jLpl27dlG9evVAhyEiIiIictnt37+fihUrXrSMWiyyKSIiAoB9+/YRHh4e4GgKv6SkJGJjY9m/f7/uZJ5HVKd5T3Wat1SfeU91mvdUp3lL9Zn3LnedGmM4ceIEMTExWZZVYpFNNlvGcJTw8HD9Y+ShkiVLqj7zmOo076lO85bqM++pTvOe6jRvqT7z3uWs0+xeVNfgbRERERERyTUlFiIiIiIikmtKLLIpODiYl156ieDg4ECHUiSoPvOe6jTvqU7zluoz76lO857qNG+pPvNeQa5TzQolIiIiIiK5phYLERERERHJNSUWIiIiIiKSa0osREREREQk15RYnGPy5MlUrVqVYsWK0axZM5YtW3bR8kuWLKFZs2YUK1aMatWq8e67716mSAuHnNTn4cOH6du3L7Vr18ZmszFkyJDLF2ghkpM6/c9//kOnTp0oW7YsJUuWpHXr1nz33XeXMdqCLyf1uXz5ctq2bUtkZCQhISHUqVOH8ePHX8ZoC4ecfo6esWLFChwOB02aNMnfAAuhnNTp4sWLsSwr0+O33367jBEXbDl9j6ampvL8889TuXJlgoODqV69Oh999NFlirZwyEmd9u/f3+97tH79+pcx4oIvp+/TWbNm0bhxY0JDQylfvjz33Xcff/7552WK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321atmx5ucMtFHJap1OnTjUOh8NMnjzZ/P7772b58uWmefPmpkWLFpc9diUW53j77bdN5cqVTVBQkLnqqqvMkiVLvNvuvfde065dO5/yixcvNk2bNjVBQUGmSpUq5p133rnMERdsOa1PINOjcuXKlzfoAi4nddquXTu/dXrvvfde/sALqJzU51tvvWXq169vQkNDTcmSJU3Tpk3N5MmTjdvtDkDkBVdO/+/PpcTCv5zU6ZgxY0z16tVNsWLFTOnSpc0111xjvv766wBEXXDl9D26detW07FjRxMSEmIqVqxonnjiCZOcnHyZoy7Yclqnx48fNyEhIeb999+/zJEWHjmt07feesvUq1fPhISEmPLly5u77rrLHDhw4DJHbYxlzOVuIxERERERkaJGYyxERERERCTXlFiIiIiIiEiuKbEQEREREZFcU2IhIiIiIiK5psRCRERERERyTYmFiIiIiIjkmhILERERERHJNSUWIiIiIiKSa0osREQkX4wYMYImTZoE7PWHDx/OoEGDslV26NChPPbYY/kckYhI0aY7b4uISI5ZlnXR7ffeey+TJk0iNTWVyMjIyxTVWX/88Qc1a9Zk48aNVKlSJcvy8fHxVK9enY0bN1K1atX8D1BEpAhSYiEiIjkWFxfnff7JJ5/w4osvsm3bNu+6kJAQwsPDAxEaAKNGjWLJkiV899132d7nlltuoUaNGowZMyYfIxMRKbrUFUpERHIsOjra+wgPD8eyrEzrzu8K1b9/f26++WZGjRpFVFQUpUqVYuTIkbhcLp566ikiIiKoWLEiH330kc9rHTx4kNtvv53SpUsTGRlJr1692LNnz0XjmzNnDj179vRZ9+9//5uGDRsSEhJCZGQkHTt25NSpU97tPXv25OOPP8513YiIXKmUWIiIyGXzv//9j0OHDrF06VLGjRvHiBEj6NGjB6VLl2b16tU89NBDPPTQQ+zfvx+A5ORkrr/+eooXL87SpUtZvnw5xYsXp2vXrqSlpfl9jYSEBDZt2kTz5s296w4fPsydd97JgAED2Lp1K4sXL6Z3796c22jfokUL9u/fz969e/O3EkREiiglFiIictlERETw1ltvUbt2bQYMGEDt2rVJTk7mueeeo2bNmgwbNoygoCBWrFgBZLQ82Gw2PvjgAxo2bEjdunWZOnUq+/btY/HixX5fY+/evRhjiImJ8a47fPgwLpeL3r17U6VKFRo2bMjgwYMpXry4t0yFChUAsmwNERER/xyBDkBERK4c9evXx2Y7e00rKiqKBg0aeJftdjuRkZHEx8cDsHbtWnbu3EmJEiV8jnP69Gl+//13v6+RkpICQLFixbzrGjduTIcOHWjYsCFdunShc+fO3HrrrZQuXdpbJiQkBMhoJRERkZxTYiEiIpeN0+n0WbYsy+86j8cDgMfjoVmzZsyaNSvTscqWLev3NcqUKQNkdIk6U8Zut7Nw4UJWrlzJggULmDhxIs8//zyrV6/2zgJ17Nixix5XREQuTl2hRESkwLrqqqvYsWMH5cqVo0aNGj6PC806Vb16dUqWLMmWLVt81luWRdu2bRk5ciTr168nKCiIuXPnerdv2rQJp9NJ/fr18/WcRESKKiUWIiJSYN11112UKVOGXr16sWzZMnbv3s2SJUt4/PHHOXDggN99bDYbHTt2ZPny5d51q1evZtSoUfz888/s27eP//znPxw5coS6det6yyxbtoxrr73W2yVKRERyRomFiIgUWKGhoSxdupRKlSrRu3dv6taty4ABA0hJSaFkyZIX3G/QoEHMmTPH26WqZMmSLF26lO7du1OrVi1eeOEFxo4dS7du3bz7fPzxxzzwwAP5fk4iIkWVbpAnIiJFjjGGVq1aMWTIEO68884sy3/99dc89dRTbNy4EYdDww9FRC6FWixERKTIsSyL999/H5fLla3yp06dYurUqUoqRERyQS0WIiIiIiKSa2qxEBERERGRXFNiISIiIiIiuabEQkREREREck2JhYiIiIiI5JoSCxERERERyTUlFiIiIiIikmtKLEREREREJNeUWIiIiIiISK4psRARERERkVxTYiEiIiIiIrn2/7EXzxVkz9hkAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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ud5dsexdht2atmr2EndNqhh7vJKKHe5iFKjelMiwwMxWlTH/tqCc+LceA7ZUq4jkZgsXLuSWW24hMTHRM/nGtbR7925iYmJYt24dtWvXvubPf60FdIyFiEhOYrVaadOmDW3atPFqEc1sb/2wjpct7xJsOIgwknnZnMD97yyjvnluVe1lZnVua9GKyc60dX+Omnl5zuzNY00qZllcIiIZ0aVLFwzDwDAMbDYbZcuWpX///pw6dcqn88uUKXPFH5+vxsKFCzEMg8jISM6cOeP12OrVqz3xXmsbN26kUaNGhISEULx4cV588UVM89wvLvHx8XTs2JFKlSphsVguOZHI5MmTuemmm4iMjCQyMpKmTZt6jS++FpRYiIj4yGq1EhsbS2xsbJYlFmccLow9y8lrnPvAW+SqxelTSdxk2QiA07RwslgDejUqy5lbXuT2sI94LGo6Ax9+kLKF82ZJXCIiV6Nly5bEx8ezc+dOXn75ZSZNmkT//v0DHRbh4eF89dVXXsemTJlCqVKlrnksx48fp1mzZkRHR7NmzRreeOMNXnvtNcaOHespk5KSQuHChRk8eDC1atW65HUWLlzIfffdxy+//MKKFSsoVaoUzZs358CBA9fqVpRYiIhkJ7/vO8Z1bPbsP5n6CIOd3chnJFPechCA9WZ5YiuWxjAMHm9SgdlPt2XGI42pWSJ/gKIWEbm04OBgoqKiKFmyJB07duT+++9n9uzZlC9f/qJxrJs2bcJisbBjx45LXsswDN577z3uuOMOQkNDqVChAt98841Xmblz51KxYkVCQkK45ZZb2L179yWv1blzZ6ZMmeLZP336NLNmzfJaRw3gn3/+4b777qNEiRKEhoZSo0YNZs6c6VXG7Xbz6quvUr58eYKDgylVqhTDhw/3KrNz505uueUWQkNDqVWrFitWrPA8Nn36dM6cOcO0adOoXr067du3Z9CgQYwdO9bTalGmTBlef/11HnzwQSIiIi55T9OnT6d3797Url2bypUrM3nyZNxuNz/99NMly2cFJRYiIj4yTZNDhw5x6NAhrybqq3XG4eLHzX/z05a/Sf13wbvVu45S17LVU2aRO+2XqaaW3zzHfnbVoXHlIn4/v4jItRYSEoLD4aBr165MnTrV67EpU6Zw0003Ua5cucueP2zYMDp06MCGDRto1aoV999/P0ePHgVg3759tG/f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5htPFykRkdwoKCiIjh07erbFP26LATVs/25rTRARkZwuw5+MjRs35siRIxw/fpzIyEjP8Z49exIaGpqpwYmIZCcWi4WKFSsGOoxcwcBIWyWvoBUAU4mFiEiOl+F38tOnT5OSkuJJKvbs2cP48ePZunUrRYoUyfQARUQk91PHMhGRnC/DLRbt2rWjffv29OrVi2PHjlGvXj1sNhtHjhxh7NixPPLII1kRp4hIwLndbnbt2gVATEwMFv3K7pfpjiZsiy8GwP7S+mFKRCSny/Cn4m+//cZNN90EwOeff07RokXZs2cPH374IRMmTMj0AEVEsgun08lHH33ERx99hNPpDHQ4Od5udxRFth6hyNYjnDZDAh2OiIj4KcMtFsnJyYSHhwMwf/582rdvj8VioX79+uzZsyfTAxQRyS4MwyAqKsqzLSIiIudkOLEoX748s2fP5o477uCHH37gySefBODQoUPky5cv0wMUEckubDYbvXr1CnQYIiIi2VKGu0K98MIL9O/fnzJlylC3bl3i4uKAtNaLOnXqZHqAIiKSO5UzDlDGSKCMkUBe81SgwxERET9luMXirrvu4sYbbyQ+Pt6zhgVAkyZNuOOOOzI1OBERyb3us/7C7dZlAHxj6vNDRCSnu6oVnqKiooiKimL//v0YhkHx4sW1OJ6I5HoOh4Pp06cDcP/992Oz2QIcUc6lISoiIrlPhrtCud1uXnzxRSIiIihdujSlSpUif/78vPTSS7jd7qyIUUQkWzBNk927d7N7925MUysviIiInC/DLRaDBw/m/fff55VXXqFhw4aYpsmyZcsYOnQoZ86cYfjw4VkRp4hIwAUFBXH33Xd7tsU/bosBVW3/bmtNEBGRnC7Dn4wffPAB7733Hm3btvUcq1WrFsWLF6d3795KLEQk17JYLFSrVi3QYeQehgFFrACYSixERHK8DL+THz16lMqVK190vHLlyhw9ejRTghIRERERkZwlw4lFrVq1mDhx4kXHJ06c6DVLlIhIbuN2u9m7dy979+7VmLLMYJpwyAWHXBiqTxGRHC/DXaFGjRrFbbfdxo8//khcXByGYbB8+XL27dvH3LlzsyJGEZFswel0MmXKFAAGDRqE3W4PcEQ5m8VtwmZH2nZhJRYiIjldhlssGjVqxLZt27jjjjs4duwYR48epX379mzdupWbbropK2IUEckWDMOg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vC0iIiIiInI5arEQEfGRy+ViyZIlANx0001aJM8PRqADEBGRTKfEQkTERy6Xi4ULFwLQoEEDJRZ+Om6GctwMBcChjyMRkRxP7+QiIj6yWCzccMMNnm3xz1vu2/mjSCkANhoVAxyNiIj4S4mFiIiPgoKCuO222wIdRq7hslj5pVxaohZuUeuPiEhOp5/cRERERETEb2qxEBGRwDBNQhxpK28THBbYWERExG9KLEREfJSamsorr7wCwIABA7Db7QGOKGe7k8X0+vVzAAY2eibA0YiIiL+UWIiIZIDb7Q50CLlGRcs+KlgOAJCfEwGORkRE/KXEQkTERzabjaeeesqzLVfP0EIWIiK5jhILEREfGYZBvnz5Ah2GiIhItqRZoURERERExG9qsRAR8ZHL5WLlypUA1K9fXytvZyYz0AGIiIi/lFiIiPjI5XKxYMECAG644QYlFiIiIudRYiEi4iOLxULt2rU92+If0zAgynpuW0REcjQlFiIiPgoKCuL2228PdBi5hmmxQOW02bXcplp/RERyOv3kJiIiIiIiflOLhYiIBIDB766yfOJqBMARW0SA4xEREX8psRAR8VFqaipjx44F4KmnnsJutwc4opztJ+d1VF7xFwD7GxcJcDQiIuIvJRYiIhlw5syZQIcgIiKSLSmxEBHxkc1m4/HHH/dsi4iIyDlKLEREfGQYBgULFgx0GLmS1scTEcn5lFiIiEhA9An6koetXwOwgTpA68AGJCIifgnodLMjR47khhtuIDw8nCJFinD77bezdetWrzKmaTJ06FCio6MJCQmhcePG/PHHH15lUlJSePzxxylUqBBhYWG0bduW/fv3e5VJTEykU6dOREREEBERQadOnTh27JhXmb1799KmTRvCwsIoVKgQffr0ITU1NUvuXURyHpfLxerVq1m9ejUulyvQ4eR4NpzYjbQ/C+5AhyMiIn4KaGKxaNEiHn30UVauXMmCBQtwOp00b96cU6dOecqMGjWKsWPHMnHiRNasWUNUVBTNmjXjxIkTnjJ9+/blq6++YtasWSxdupSTJ0/SunVrrw/+jh07sn79eubNm8e8efNYv349nTp18jzucrm47bbbOHXqFEuXLmXWrFl88cUX9OvX79pUhohkey6Xi7lz5zJ37lwlFiIiIhcIaFeoefPmee1PnTqVIkWKsHbtWm6++WZM02T8+PEM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Pzvd7JNPPmlu3rzZfP/99zXd7HkyWp+maZrr1q0z161bZ8bGxpodO3Y0161bZ/7xxx+BCD/byWh9zpgxwwwKCjLffPNNryn/jh07FqhbyDYyWpcTJ040v/nmG3Pbtm3mtm3bzClTppj58uUzBw8eHKhbyFau5v/6+TQr1DkZrctx48aZX331lblt2zZz06ZN5oABA0zA/OKLLwJ1C9lKRuvzxIkTZokSJcy77rrL/OOPP8xFixaZFSpUMLt37x6oW8g2rvb/+QMPPGDWq1fvWoeb7WW0PqdOnWoGBQWZkyZNMnfs2GEuXbrUvP766826dete89iVWGTAm2++aZYuXdq02+3mddddZy5atMjzWOfOnc1GjRp5lV+4cKFZp04d0263m2XKlDHfeuutaxxx9pbR+gQu+itduvS1DToby0h9NmrU6JL12blz52sfeDaUkbqcMGGCWa1aNTM0NNTMly+fWadOHXPSpEmmy+UKQOTZU0b/r59PiYW3jNTlq6++apYrV87MkyePGRkZad54443mnDlzAhB19pXR1+aWLVvMpk2bmiEhIWaJEiXMp556ykxOTr7GUWdPGa3LY8eOmSEhIea77757jSPNGTJanxMmTDCrVq1qhoSEmMWKFTPvv/9+c//+/dc4atM0TPNat5GIiIiIiEhuozEWIiIiIiLiNyUWIiIiIiLiNyUWIiIiIiLiNyUWIiIiIiLiNyUWIiIiIiLiNyUWIiIiIiLiNyUWIiIiIiLiNyUWIiIiIiLiNyUWIiJyzQwdOpTatWsH7Pmff/55evbs6VPZ/v3706dPnyyOSEQk99DK2yIikikMw7ji4507d2bixImkpKRQsGDBaxTVOX///TcVKlRgw4YNlClTJt3yhw4doly5cmzYsIGYmJisD1BEJIdTYiEiIpkiISHBs/3JJ5/wwgsvsHXrVs+xkJAQIiIiAhEaACNGjGDRokX88MMPPp9z5513Ur58eV599dUsjExEJHdQVygREckUUVFRnr+IiAgMw7jo2IVdobp06cLtt9/OiBEjKFq0KPnz52fYsGE4nU6efvppChQoQIkSJZgyZYrXcx04cIB77rmHyMhIChYsSLt27di9e/cV45s1axZt27b1Ovb5559To0YNQkJCKFiwIE2bNuXUqVOex9u2bcvMmTP9rhsRkf8CJRYiIhJQP//8MwcPHmTx4sWMHTuWoUOH0rp1ayIjI1m1ahW9evWiV69e7Nu3D4Dk5GRuueUW8ubNy+LFi1m6dCl58+alZcuWpKamXvI5EhMT2bRpE9dff73nWHx8PPfddx9du3Zly5YtLFy4kPbt23N+Q37dunXZt28fe/bsydpKEBHJBZRYiIhIQBUoUIAJEyZQqVIlunbtSqVKlUhOTmbQoEFUqFCBgQMHYrfbWbZsGZDW8mCxWHjvvfeoUaMGVapUYerUqezdu5eFCxde8jn27NmDaZpER0d7jsXHx+N0Omnfvj1lypShRo0a9O7dm7x583rKFC9eHCDd1hAREYGgQAcgIiL/bdWqVcNiOfc7V9GiRalevbpn32q1UrBgQQ4dOgTA2rVr+euvvwgPD/e6zpkzZ9ixY8cln+P06dMA5MmTx3OsVq1aNGnShBo1atCiRQuaN2/OXXfdRWRkpKdMSEgIkNZKIiIiV6bEQkREAspms3ntG4ZxyWNutxsAt9tNbGws06dPv+hahQsXvuRzFCpUCEjrEnW2jNVqZcGCBSxfvpz58+fzxhtvMHjwYFatWuWZBero0aNXvK6IiJyjrlAiIpKjXHfddWzfvp0iRYpQvnx5r7/LzTpVrlw58uXLx+bNm72OG4ZBw4YNGTZsGOvWrcNut/PVV195Ht+0aRM2m41q1apl6T2JiOQGSixERCRHuf/++ylUqBDt2rVjyZIl7Nq1i0WLFvHEE0+wf//+S55jsVho2rQpS5cu9RxbtWoVI0aM4Ndff2Xv3r18+eWXHD58mCpVqnjKLFmyhJtuusnTJUpERC5PiYWIiOQooaGhLF68mFKlStG+fXuqVKlC165dOX36NPny5bvseT179mTWrFmeLlX58uVj8eLFtGrViooVK/Lcc88xZswYbr31Vs85M2fOpEePHll+TyIiuYEWyBMRkf8E0zSpX78+ffv25b777ku3/Jw5c3j66afZsGEDQUEakigikh61WIiIyH+CYRi8++67OJ1On8qfOnWKqVOnKqkQEfGRWixERERERMRvarEQERERERG/KbEQERERERG/KbEQERERERG/KbEQERERERG/KbEQERERERG/KbEQERERERG/KbEQERERERG/KbEQERERERG/KbEQERERERG/KbEQERERERG//R8fwV/Pkn9LGgAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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sBmM7d0IwzC4r2t96saXCezLjK7N2GsuBuChng2CjktyRzLp782w2wxu79uNLr5pLDJbAeCy23j62mbcnFyTVVVS+c48PqHfh762rLvwHp7r34pFvpb57vX/PIPpl/Mwh60IHvTcysdmawA+Ny/iMe/NgXL9Hf/jwhWjuGrNbTzkmE1T2xY8lp0/zGg+37iH/6zZEdoPQURKjNTUVAzDwDAMnE4ntWvXZtSoURw6dCik42vWrMm0adOKNKZjiUb58uU5evRo0L5Vq1YF4v2zrV27lssvv5yoqCiqVKnCP/7xj6AvsHbv3k3//v2pX78+NpuN4aeYXHTGjBlceumllC9fnvLly9OpUydWrVr1J96FmkKJiIQsx4hkic3/wfln35VcVcD3nj7Nq3BpvWHsPZDDmEoxOOz+73aiXHb+eUdPnvu8Pq8ezOGai6rSJW+413LRLubd2Y6ZX20h+6iXa1tV5aLq5QP73r+7PR98twufCT2aJuKO8iczie4oPht5OQu+343DbtChfnxgX5vaFfjn7Vfx1qrtOO02BrSrQd34WACmD2jPI/99kUPrP+GoEU2Nll2Y3LsREQ47y38awvRvjnCLPY0sYhjvvYmLewzkkmgnl85NYh/uoPt9w9eFTCuWZ53TsRkWf7Mfn/jPY9l5wDOYq5z+IXDfWrmdvyfXQURKl27dujFz5kw8Hg9ffvklt912G4cOHeKFF14Ia1yxsbHMnz+fG264IbDt1VdfpXr16mzfvv1PjSU7O5vOnTtzxRVXsHr1ajZt2kRqaioxMTGMHDkSgJycHCpVqsSDDz7I1KlTT3mexYsXc8MNN9CuXTsiIyOZOHEiXbp04YcffqBKlSp/zs1YUmSysrIswMrKygp3KCJyHvztn0utGvd/GHjNWbkt3CGdN9lHci2vzwzaZpqmNf+bndbAmSute976xlr+8++Bff/832ar5gPHn02zxz6xlmzaY81Zuc26b8xIy/eI27IeLWtZj5a1djxS27r6galWjfs/CJS/4un//bk3KFJCHDlyxFq/fr115MiRcIdSYAMGDLD69OkTtO22226zEhISrDp16liTJk0K2rd27VrLMAzrp59+sizLsmrUqGFNnTo1sB+wZsyYYV199dVWVFSUVbduXeu9994LOseCBQusevXqWZGRkVaHDh2smTNnWoCVmZlpWZZl/e9/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unWJiIigevXqjBs3DoDFixdjGAb79+8PnCs9PT0onlmzZlGuXDk+/PBDGjZsSEREBNu2bWPx4sW0bt2amJgYypUrR/v27dm2bVvoD7uYUGIhIhIiu93B2A6RjO0QidOuP5+FYRhg2W3QIRI6ROK1a+ZtkYIYP34848eP5/Dhw4FtX331FePHj+ejjz4KKjtp0iTGjx9PVlZWYNvq1asZP3487733XlDZadOmMX78ePbu3RvYlp6eXqSx22w2nn32WdatW8drr73G559/zn33nfvMQM888wzJyckMGjSI3bt3s3v3bqpVq3bKsqmpqXz99de8//77LF++HMuyuOqqq/B4PID/Xjt27EijRo1Yvnw5S5cupVevXoHkavTo0Tz11FM8/PDDrF+/njlz5lC5cuUCxXv48GEmTJjAyy+/zA8//EBcXBxXX301l19+Od9//z3Lly9n8ODBJbJWXE2hRERCZp2wZGi42UKy9ARFSrUPP/yQMmXKBNa7d+/OO++8E9QXolatWjz++OPceeedPP/88+d0HbfbjcvlIjo6moSEhNOW27x5M++//z5fffUV7dq1A/xznVWrVo13332Xa6+9lokTJ9KqVaugWBo1agTAgQMHeOaZZ5g+fToDBvgnS61Tpw6XXHJJgeL1eDw8//zzNGvmH778jz/+ICsri549e1KnTh3AP8FzSaTEQkRERKSEGTNmDABOpzOwrX379rRt2zZfe/3/+7//y1f24osv5qKLLspX9tiH/hPLntwsKVRXXHEFL7zwQmA9JiYGgP/973+MHz+e9evXk52djdfr5ejRoxw6dChQ5nzYsGEDDoeDNm3aBLZVqFCB+vXrs2HDBsBfY3Httdee9vicnBw6duxYqDhcLhdNmx4fVTAuLo7U1FS6du1K586d6dSpE/369SMxMbFQ1wkH1eWLiITI9PlI+8lL2k9efKZmii4swzThBw/84MF2jm24Rf6qXC4XLpcrqLmM3W7H5XLhcDiKvOy5iImJoW7duoFXYmIi27Zt46qrrqJx48bMmzePNWvW8M9//hMg0BzJZrMF9fE4cV9hnHzOE7cfu9+oqKjTHn+mfUAgSTvxOqeKOyoqKl8zp5kzZ7J8+XLatWvH22+/zQUXXMCKFSvOeL3iSImFiEiITMtkxU4vK3Z68Z36/UkKwLCAvT7Y68M4zRu+iJQuX3/9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Ex8fT9myZQt0zcLSqFAiIiGK9+5m8M5HGbzzUSY6Xgh3OCWeYQG7fLDLpzlCRP4COnToQKNGjRif1/F87NixTJgwgWeeeYZNmzaxdu1aZs6cGdQP40R169bl66+/5pNPPmHTpk08/PDDrF69Ol+5rl274na7eeKJJ7jllluC9g0fPpxPPvmELVu28M033/D5558HJQknqlevHn369GHQoEEsXbqU7777jptuuokqVarQp0+fQDmn08nQoUNZuXIl33zzDbfccgtt27YNJBqPPPIIr7/+OmPHjuWHH35gw4YNvP322zz00ENnfF6h3G/NmjX5/vvv+fHHH/n999/xeDzceOONVKxYkT59+vDll1+yZcsWvvjiC+655x527tx5xmsWlhILEZEQGYZBqyQbrZJsOAwNNysiUlAjRoxgxowZ7Nixg9tuu42XX36ZWbNm0aRJEy6//HJmzZpFrVq1TnnsHXfcQd++fbnuuuto06YN+/btO+W8GDabjdTUVHw+HzfffHPQPp/Px1133UWDBg3o1q0b9evXP+MQtzNnzqRly5b07NmT5ORkLMvio48+ChrdKjo6mvvvv5/+/fuTnJxMVFQUc+fODezv2rUrH374IYsWLeLiiy+mbdu2TJkyhRo1apzxWYVyv4MGDaJ+/fq0atWKSpUq8dVXXxEdHc2SJUuoXr06ffv2pUGDBtx6660cOXLk/NdgWGE0fvx4q1WrVlaZMmWsSpUqWX369LE2btwYVGbAgAEWEPRq06ZNUJmjR49ad999t1WhQgUrOjra6tWrl7Vjx46gMn/88Yd10003WWXLlrXKli1r3XTTTVZmZmZQmW3btlk9e/a0oqOjrQoVKlhDhw61cnJyQr6frKwsC7CysrIK9iBEpERIeXquZT1a1rIeLWvNf6ib9X76r+EOqcRa9EOGNfeBXpZ1ucuyLndZqY//K9whiRRLR44csdavX28dOXIk3KGUKLfddpvVq1ev836dmTNnWm63+7xf53w70+9ZQT7fhrXG4osvvuCuu+5ixYoVLFq0CK/XS5cuXTh06FBQuWMzNx57nTyRyfDhw5k/fz5z585l6dKlHDx4kJ49ewa1Oevfvz/p6emkpaWRlpZGeno6KSkpgf0+n48ePXpw6NAhli5dyty5c5k3bx4jR448vw9BREqOk5rraCCjoqNHKSJFISsri08/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UJUqVSI9PZ169eoFbU9PTz/lB/jCODa0LUDjxo1p1aoVNWrUYMGCBfTt2/e0xx0b+vaYU401fy5lTjZ69GhGjBgRWM/OzlZyIVKK2e32QHNPu10zRReWZRhQ2/82ZNrOqcufiIgUIwVOLAYNGsTgwYP55ZdfaNeuHYZhsHTpUp566qnz3tk5MTGRGjVqsHnzZgASEhLIzc0lMzMzqNZiz549tGvXLlDmt99+y3euvXv3BmopEhISWLlyZdD+zMxMPB5PvpqME0VERBAREVHo+xKRksFut9O+fftwh1FqWDYbVHccXxYRkRKtwInFww8/TGxsLJMnT2b06NGAv1/D2LFjgybNOx/27dvHjh07SExMBKBly5Y4nU4WLVpEv379ANi9ezfr1q1j4sSJACQnJ5OVlcWqVato3bo1ACtXriQrKyuQfCQnJzNu3Dh2794dOPfChQuJiIigZcuW5/WeRET+qhb42rLZqgrAzojTf4kjIiIlg2FZ1jn3nztw4AAAsbGx53T8wYMH+emnnwBo0aIFU6ZM4YorriAuLo64uDjGjh3LNddcQ2JiIlu3bmXMmDFs376dDRs2BK5555138uGHHzJr1izi4uIYNWoU+/btY82aNYGmCt27d2fXrl289NJLAAwePJgaNWrwwQcfAP7hZps3b07lypWZNGkSf/zxB6mpqVx99dWBWXZDkZ2djdvtJisrSyNJiZRClmUF/d07U1NJObPlP++j/7+WEX/wDwC88QmsebRrmKMSEZGTFeTzbYHrnq+88srA5HKxsbGBD/jZ2dlceeWVBTrX119/TYsWLWjRogUAI0aMoEWLFjzyyCPY7XbWrl1Lnz59uOCCCxgwYAAXXHABy5cvD0pkpk6dytVXX02/fv1o37490dHRfPDBB0Htn2fPnk2TJk3o0qULXbp0oWnTprzxxhuB/Xa7nQULFhAZGUn79u3p168fV199NU8//XRBH4+IlGIej4cpU6YwZcoUPB5PuMMp8Rymjxu++4QbvvsEu+k7+wEiIlKsFbgp1OLFi085I/XRo0f58ssvC3SuDh06cKYKk08++eSs54iMjOS55547Y81CXFwcb7755hnPU716dT788MOzXk9E/tps6gtQJFTZIyJS+oScWHz//feB5fXr1wcNw+rz+UhLS6NKlSpFG52ISDHicrl45JFHwh1GqVGRLOLw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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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GEjlypV58803r1KkhUNO7ueRI0fo168f1atXx2KxMHLkyKsXaCGSk3v6ySef0KFDB0qXLk1YWBjNmzfn66+/vorRFnw5uZ/r1q2jZcuWlCxZkqCgIGrUqMG0adOuYrSFQ05/j571/fffY7PZaNCgQf4GWAjl5J6uWrUKwzAyvX755ZerGHHBltOf0bS0NJ544gmuu+46AgICuP7663n33XevUrSFQ07u6aBBg7L8Ga1du/ZVjLjgy+nP6YIFC6hfvz7BwcGULVuWe++9l2PHjl2laM9jimmaprl48WLTbrebs2bNMnfu3Gk+8sgjZkhIiHngwIEsy+/du9cMDg42H3nkEXPnzp3mrFmzTLvdbn700UdXOfKCKaf3c9++feaIESPM9957z2zQoIH5yCOPXN2AC4Gc3tNHHnnEfPHFF83Nmzebv/76qzlu3DjTbrebP/zww1WOvGDK6f384YcfzIULF5o7duww9+3bZ86bN88MDg4233rrrascecGV03t61okTJ8zKlSubHTt2NOvXr391gi0kcnpPv/vuOxMwd+/ebR45csT7crlcVznygulKfkZ79OhhNm3a1FyxYoW5b98+c9OmTeb3339/FaMu2HJ6T0+cOOHzs3no0CGzRIkS5jPPPHN1Ay/AcnpP165da1osFvOVV14x9+7da65du9asXbu22atXr6scuWkqsTijSZMm5oMPPuhzrEaNGubYsWOzLP/YY4+ZNWrU8Dn2wAMPmM2aNcu3GAuTnN7P87Vu3VqJRRZyc0/PqlWrljlx4sS8Dq1Qyov7edttt5l33313XodWaF3pPb3zzjvNJ5980nzmmWeUWFwgp/f0bGKRkJBwFaIrfHJ6P7/66iszPDzcPHbs2NUIr1DK7e/SJUuWmIZhmPv378+P8AqlnN7Tl156yaxcubLPsVdffdUsX758vsV4MeoKBaSnp7Nt2zY6duzoc7xjx46sX78+yzobNmzIVL5Tp05s3boVp9OZb7EWBldyP+XS8uKeejweTp48SYkSJfIjxEIlL+7n9u3bWb9+Pa1bt86PEAudK72nc+bM4ffff+eZZ57J7xALndz8nDZs2JCyZcvSrl07vvvuu/wMs9C4kvv52WefccMNNzBlyhTKlStHtWrVGDNmDCkpKVcj5AIvL36Xzp49m/bt23PdddflR4iFzpXc0xYtWnD48GGW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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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LjnZvq/5r7/+mh49elCvXj27+RERESXWFY4NGzaMYcOG+TsMISqMqtNzd3trr1D/Nalu/R61UXZSQ8kB4IR2pRejCzwmnZ55bfrYnlea33fhc+WOY+HiQHjn1y+tnOLj0arLjNXha5Z/wuxOyEGUc5UrEPfVFyOJl19OcfMzem65G3EFxKW3VatWERsba0sqADp37kxsbCwrV650ahtHjhzh+++/Z/jw4SWWzZ07l8TERJo3b87YsWM5fdrzgeWEEMElR4njbdO1vG26lhWWlm5t4179t7wXMpX3QqYSpUlvX0IIIYJLQNyxyM7OpkaNGiXm16hRo8TAZGX54IMPiI6O5oYbbrCbf/vtt1O3bl2Sk5P5+++/GTduHBs3bmTx4sVlbuv86M3n5eXlObknQohAdVyJ52XTrbbntzpYVwghhKiK/JpYPPvss0yYMMHhOr///jtQel07TdOcrm85e/Zsbr/9dsLCwuzmjxw50jbdokULGjZsSIcOHfjjjz9o165dqduaNGlSuXELIYKLxWzi9MZFAES1+Jefowl8erOJoX98D8CH7a7xczRCCCG8wa+JxQMPPFBuD0zp6els2rSJI0eOlFh27Ngxp3ojWrFiBdu2bWP+/PnlrtuuXTsMBgM7duwoM7EYN26c3RgFeXl5pKamlrttIUQA0zTMp4+df+LXUIKBgkZMUb5tWgghKpp883ifXxOLxMREEhMTy10vIyOD3Nxc1q5dS8eOHQFYs2YNubm5dOnSpdzys2bNon379rRu3brcdf/55x+MRiMpKSllrhMaGkpoaGi52xJCBA9VUYht2hUAnZut0+QEWgghAks5Q1CJSwRE4+2mTZvSp08fRo4cyerVq1m9ejUjR46kf//+dj1CNWnShC+//NKubF5eHp999hkjRowosd1du3bx3HPPsW7dOvbu3cuCBQu4+eabadu2LV27dvX5fgkhAkcD5QD7aj3JvlpP8rzhQ5fLy2+TYwHYyYsQoooob4BbXwq0HrACIrEAa89NLVu2pFevXvTq1YtWrVrx3//+126dbdu2kZubazfvk08+QdM0br21ZFPLkJAQlixZQu/evWncuDEPPvggvXr14qeffkKn0/l0f6q6YcOGcf311/s7DCFEJSGJV9XhcCSGcs6iKuM5lrv74+oJo3UQX/fiCDbujAFSGmcSBm+d2Lv73jnz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eP7553nsscd45plnbK932223cdddd9nF4Gh/o6KiCA8Pp6ioiOTkZNt6H330Eaqq8v7779u+EObMmUNcXBxLly6lV69eZe6z8J37/tWEnccf4os/D9nmpZ7eyGPvHuKlu2+kfvWq3X2sq4w6A+92usnfYQghhPAiSSxEmXr06MHbb79NQUEB77//Ptu3b+f//u//ADh27BgHDhxg+PDhjBw50lbGZDIRGxtb5jbfeecd3n//ffbt28fZs2cpLi6mTZs2tuXDhg1j/PjxrF69ms6dOzN79mwGDhxIZGQkAA8++CD33XcfixYtomfPntx44420atWq1NfasmULer3eljAAJCQk0LhxY7Zs2WKbp9fr6dChg+15kyZNiIuLY8uWLXTs2JH169fz+++/292hOJ9sFRQUEBERAWC3DWf3tzTr169n586dREdH280vLCxk165dDssK31EUhUk3tuRATgG/783hMmUrmSEvc6Y4nAffKuCB22/m8oaJ/g5TCCGE8JuASSxefPFFvv/+ezZs2EBISAinTp0qt4ymaUyYMIH33nuPnJwcOnXqxFtvvUXz5s1t6xQVFTF27FjmzZvH2bNnueqqq5gxYwa1a9f24d4ATz5Z9jL1khpq567el+rS+1SjR7sd0qUiIyNp0MA6UvDrr79Ojx49mDBhAs8//zwWi3Xk4ZkzZ9qduAPodLpSt/fpp5/y8MMP89prr5GRkUF0dDSvvPIKa9assa1To0YNrr32WubMmUO9evVYsGCBrd0BwIgRI+jduzfff/89ixYtYtKkSbz22mu2hOdiWhmVIjVNK3FrsLRbhefnWSwWJkyYYLszc7GwsDDb9Pnkx5X9LY3FYqF9+/bMnTu3xLLq1as7LCt8K1Sv4/2hlzFk1mrGHvuUSKWISIrI1J5mfOZelmUM5cGejYgOq9qjagshhKiaAiaxKC4u5uabbyYjI4NZs2Y5VWby5MlMmTKFzMxMGjVqxAsvvMDVV1/Ntm3bbFeDR48ezbfffssnn3xCQkICjzzyCP3792f9+vVlniB7RUiI/9d10TPPPEPfvn257777qFmzJrVq1WL37t3cfnG1LQdWrFhBly5d7HqWKu0K/IgRI7jllluoXbs29evXp2vXrnbLU1NTuffe/2/v3uOirPM9gH/mys1muMgdFQUEBLxhirqGJYprrRj5yiNamVly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bm6uyM3NFdHR0SIpKUnk5uaKkydPWiN8m2NoPrds2SLkcrlYs2aNzlR/FRUV1joEm2JoPt9//33x5Zdfivz8fJGfny82btwoVCqVWLRokbUOwaZ05P1+L84KpcvQfK5cuVLs3LlT5OfnixMnTogFCxYIAOKzzz6z1iHYDENzeePGDREQECDGjx8vTp48KTIzM0VISIiYNm2atQ7BpnT0vT558mQxaNAgS4dr8wzNZ3p6upDL5WLt2rXi3LlzIjs7WwwYMEAMHDjQ4rGzsGinNWvWiG7dugmlUin69+8vMjMztfuee+45ERsbq9P+wIEDol+/fkKpVIrAwEDxwQcfWDhi22VoLgE0u3Xr1s2yQdswQ/IZGxurN5/PPfec5QO3UYbkc9WqVSIiIkI4OzsLlUol+vXrJ9auXSvq6+utELltMvT9fi8WFs0Zks8VK1aIoKAg4ejoKNzc3MQf/vAH8dVXX1khattk6Ll56tQpERcXJ5ycnERAQICYM2eOqK6utnDUtsvQfFZUVAgnJyexbt06C0dqHwzN56pVq0SvXr2Ek5OT8PX1FZMmTRLFxcUWjloIiRCWvkZCRERERET3G46xICIiIiIio7GwICIiIiIio7GwICIiIiIio7GwICIiIiIio7GwICIiIiIio7GwICIiIiIio7GwICIiIiIio7GwICIiIiIio7GwICIii0hNTUXfvn2t9vqvv/46XnrppXa1nTt3LmbOnGnmiIiI7i9ceZuIiIwmkUha3f/cc8/h/fffR21tLTw8PCwU1V2XL19GSEgIjh07hsDAwDbbl5WVISgoCMeOHUP37t3NHyAR0X2AhQURERmttLRU+/P27duxZMkSnD59WrvNyckJarXaGqEBAJYtW4bMzEzs2bOn3Y956qmnEBwcjBUrVpgxMiKi+we7QhERkdF8fHy0N7VaDYlE0mzb77tCTZkyBePGjcOyZcvg7e0NV1dXpKWlQaPRYN68eXB3d0dAQAA2btyo81q//vorJkyYADc3N3h4eCAhIQHnz59vNb5t27Zh7NixOtt27NiBqKgoODk5wcPDA3Fxcbh165Z2/9ixY7F161ajc0NE9KBgYUFERFbzzTff4NKlSzh48CDeffddpKam4oknnoCbmxuOHDmC5ORkJCcno6ioCABQXV2NRx99FJ06dcLBgweRnZ2NTp06YfTo0airq9P7GtevX8eJEycwYMAA7baSkhJMnDgRU6dOxalTp3DgwAEkJibi3ov4AwcORFFRES5cuGDeJBAR3SdYWBARkdW4u7tj1apVCA0NxdSpUxEaGorq6mq89tprCAkJwcKFC6FUKnHo0CEAjVcepFIpPvroI0RFRSE8PBzp6em4ePEiDhw4oPc1Lly4ACEE/Pz8tNtKSkqg0WiQmJiIwMBAREVFYcaMGejUqZO2jb+/PwC0eTWEiIgaya0dABERPbgiIiIgld79G5e3tzciIyO192UyGTw8PFBWVgYA+PHHH3H27Fk89NBDOs9z+/ZtnDt3Tu9r1NTUAAAcHR212/r06YMRI0YgKioK8fHxGDVqFMaPHw83NzdtGycnJwCNV0mIiKhtLCyIiMhqFAqFzn2JRKJ3W0NDAwCgoaEB0dHR2Lx5c7Pn8vT01PsanTt3BtDYJaqpjUwmw759+3D48GHs3bsXq1evxqJFi3DkyBHtLFDXrl1r9XmJiEgXu0IREZHd6N+/P86cOQMvLy8EBwfr3FqadSooKAgqlQp5eXk62yUSCYYOHYq0tDTk5uZCqVRi586d2v0nTpyAQqFARESEWY+JiOh+wcKCiIjsxqRJk9C5c2ckJCQgKysLhYWFyMzMxKxZs1BcXKz3MVKpFHFxccjOztZuO3LkCJYtW4acnBxcvHgRn3/+Oa5cuYLw8HBtm6ysLAwbNkzbJYqIiFrHwoKIiOyGs7MzDh48iK5duyIxMRHh4eGYOnUqampqoFKpWnzcSy+9hG3btmm7VKlUKhw8eBBjxoxBz549sXjxYrzzzjv44x//qH3M1q1b8eKLL5r9mIiI7hdcII+IiO57QgjExMQgJSUFEydObLP9V199hXnz5uHYsWOQyzkckYioPXjFgoiI7nsSiQTr1q2DRqNpV/tbt24hPT2dRQURkQF4xYKIiIiIiIzGKxZERERERGQ0FhZERERERGQ0FhZERERERGQ0FhZERERERGQ0FhZERERERGQ0FhZERERERGQ0FhZERERERGQ0FhZERERERGQ0FhZERERERGQ0FhZERERERGS0/wfLZToFJGx5fgAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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eQE+pMJCRET8S5eY+Ubmmbf3LM+9X2Q5aDvM86ipeyyk5KlatSozZ87Msm7evHmUKlUqIHn69+/P+vXr+f777/n+++9Zv349AwYMyNLHNE0GDhxIv379cnyOpUuX0rRpUz799FN+++03Bg4cyB133MFXX31VGC/BZ1RYiEhQczgc/Oc//+E///mPZrT1JZcJK9M9D4duivcJI9NILTWuzr1fyn745C7PY/XM3PtJseN2mxw9lRbQh9t9cTPAd+zYkQcffJARI0ZQtmxZ4uPjGTNmTJY+W7du5eqrryYsLIyGDRuyYMGCHJ/rzjvv5IMPPuDMmTPedTNmzODOO+/M1veJJ56gbt26REREULNmTZ5++uls7/VffvklLVu2JCwsjHLlytG3b98s21NTUxk4cCBRUVFUq1aNt956y7vtzz//5Pvvv+ftt9+mbdu2tG3blunTp/P111+zefNmb7/XXnuNYcOGUbNmzRxf06hRo3juuedo164dtWrV4sEHH6R79+7Mmzcv5wNaROnmbREJaqZpsn37dm9bfCjV7fmvjquITxxPTafF8/8NaIY1T3UmtlToRe07e/ZsHnnkEVasWMGyZctITEykffv2dOnSBbfbTd++fSlXrhzLly8nJSWF4cOH5/g8LVq0oEaNGnz66afcfvvt7Nmzh8WLF/PGG2/w3HPPZekbFRXFrFmzqFSpEr///juDBw8mKiqKESNGAPDNN9/Qt29fRo8ezbvvvkt6ejrffPNNlud45ZVXeO655xg1ahSffPIJ9913H1dffTX169dn2bJlxMTE0Lp1a2//Nm3aEBMTw9KlS6lXr95FHSuA5ORkGjRocNH7B4IKi0wWbEgiotTpQMcQkULkdrup0OQqEmIjsdn0ligi4i9Nmzbl2WefBaBOnTpMmTKFH3/8kS5duvDf//6XP//8k507d1KlShUAxo0bx7XXXpvjc911113MmDGD22+/nZkzZ9KjRw/vCH+ZPfXUU952QkICjz76KB9++KG3sHjhhRe45ZZbGDt2rLffZZddluU5evTowdChQwHPGZBJkyaxcOFC6tevT1JSEhUqVMj2cytUqEBSUlJBDk8Wn3zyCatWreLf//73RT9HIOhTNJPUeQ9A6IVHhZnt7MpvZi3vcg3jAPfbPs/X849yDCKNjNFRultW0sW6Js/9drjjmeL6vyzrhts+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KLfccgu//PILAD/88AO33347r732GldddRXbtm3jnnvuAeDZZ5/1/qxnn32W8ePHM2nSJKxWa56v97HHHuPPP/8kJSWFmTNnAlC2bFlSU1O55ppruOqqq1i8eDE2m43nn3+e7t2789tvvxFyif+WF6J3exER8bnnnQO87X62mAAmESmhxo3LfVudOnDbbRnLL78MuV1ak5AAiYkZy5MnQ2pq9n5jxhQ44tdff02pUqVwuVycPXsWgIkTJ3q3P/fcc7zyyiv07dsXgBo1arBx40b+/e9/51hY2O12xo4d612uUaMGS5cu5aOPPuLmm28GYNCgQcycOdNbWHzzzTekpqZ6t+/evZsbbriBJk08c7zUrFkz1/znC4pffvmFdu3aATBnzhyqVq3K559/zk033QR4LluaMmUKrVu3BmD27Nk0aNCAlStX0qpVK1544QWefPJJ72uqWbMmzz33HCNGjMhSWPTv35+BAwdmyXCh11uqVCnCw8NJS0sjPj7e2++9997DYrHw9ttvYxgG4DkzVLp0aRYuXEjXrv4bKU6FhYgENZfL5f1WqX379pp520esbhdX71gLgKV5xQCnEZFAuOaaa5g2bRqpqam8/fbbbNmyhQceeACAw4cPs2fPHgYNGsTgwYO9+zidTmJicv8y4s033+Ttt99m165dnDlzhvT0dJo1a+bdnpiYyFNPPcXy5ctp06YNM2bM4Oabb/aemX7wwQe57777mD9/Pp07d+aGG26gadOmOf6sP//8E5vN5i0YAGJjY6lXrx5//vmnd53NZqNly5be5fr161O6dGn+/PNPWrVqxZo1a1i1ahUvvPCCt8/5Yis1NZWIiAiALM+R39ebkzVr1vDXX38RFRWVZf3Zs2fZtm3bBfe9VCosRCSouVwu/ve//wHQpk0bFRY+YjHdXHZgCwCH3GYevUWkwEaNyn2b5W+30J779j5H577R9ho+/KIj/V1kZCS1a9cG4LXXXuOaa65h7NixPPfcc7jPzSQ/ffr0LH+4A7m+D3/00Uc8/PDDvPLKK7Rt25aoqChefvllVqxY4e1ToUIFrr/+embOnEnNmjX59ttvvfcdANx9991069aNb775hvnz5zN+/HheeeUVb8GTmWnm/N5lmqb3TMB5f1/OvM7tdjN27FjvmZnMwsLCvO2/X5abn9ebE7fbTYsWLZgzZ062beXLl7/gvpdKhYWIBDWLxcLll1/ubYsUWaHRGe1S8bn3s2oQgqBQkOvk/dW3gJ599lmuvfZa7rvvPipVqkTlypXZvn07t2W+bOsClixZQrt27bKMLJXTN/B33303t9xyC1WqVKFWrVq0b98+y/aqVasyZMgQhgwZwsiRI5k+fXqOhUXDhg1xOp2sWLHCeynU0aNH2bJlCw0aNPD2czqdrF69mlatWgGwefNmTpw4Qf369QG4/PLL2bx5s7fIyq/8vN6QkBBcLleWdZdffjkffvghFSpUIDo6msKkT1ERCWo2m41evXrRq1cvbDZ91+Irw22fcKN1MTdaFxPuSg50nJLBYgXOfSsaUzn3fjFVM9qth/g1kkhBdOzYkUaNGjHu3P0hY8aMYfz48bz66qts2bKF33//nZkzZ2a5DyOz2rVrs3r1an744Qe2bNnC008/zapVq7L169atGzExMTz//PPcddddWbYNHz6cH374gR07drB27Vr+97//ZSkSMqtTpw69e/dm8ODB/Pzzz/z666/cfvvtVK5cmd69e3v72e12HnjgAVasWMHatWu56667aNOmjbfQeOaZZ/jPf/7DmDFj2LBhA3/++ScffvghTz311AWPV35eb0JCAr/99hubN2/myJEjOBwObrvtNsqVK0fv3r1ZsmQJO3bsYNGiRTz00EPs3XuBEeV8QIWFiIj4XB1jH1WMw1QxDmM1NfypiHg88sgjTJ8+nT179nD33Xfz9ttvM2vWLJo0aUKHDh2YNWsWNWrUyHHfIUOG0LdvX/r160fr1q05evRojvNiWCwWEhMTcblc3HHHHVm2uVwuhg0bRoMGDejevTv16tW74BC3M2fOpEWLFvTs2ZO2bdtimibffvttltGtIiIieOKJJ+jfvz9t27YlPDycDz74wLu9W7dufP311yxYsIArrriCNm3aMHHiRKpXr37BY5Wf1zt48GDq1atHy5YtKV++PL/88gsREREsXryYatWq0bdvXxo0aMDAgQM5c+aM389gGGZuF5AFkZSUFGJiYkhOTi70U0YiIiXNP7/aSJtlw+i6dCkA4wZ9yagB3QKcqoQYUxowoXILGPy/nPvsXQNv/8PTbj0Erp1QWOnED86ePcuOHTuoUaNGluvx5cIGDx7MwYMH+fLLL/36c2bNmsXw4cM5ceKEX3+Ov13o96wgfyfrvL+IBLX0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AYSEhPgzZrFyMcd0/PjxtG/fnscffxyApk2bEhkZyVVXXcXzzz9f6FfZlPh7LEJCQmjRogULFizIsn7BggW0a9cux33atm2brf/8+fNp2bIldrvdb1mLi4s5ppK3iz2u77//PomJicydOzdg11QWZb76fTVNk7S0NF/HK5YKekyjo6P5/fffWb9+vfcxZMgQ6tWrx/r162ndunVhRS/SfPW7um7duqC/ZDezizmu7du3Z//+/Zw6dcq7bsuWLVgsFqpUqeLXvMXFpfy+Llq0iL/++otBgwb5M2KxczHHNDU1FYsl65/zVqsVyLjaplAV/v3ihe/80F3vvPOOuXHjRnP48OFmZGSkdxSCJ5980hwwYIC3//nhZh9++GFz48aN5jvvvKPhZv+moMfUNE1z3bp15rp168wWLVqY/fv3N9etW2du2LAhEPGLrIIe17lz55o2m8184403sgzhd+LEiUC9hCKpoMd1ypQp5pdffmlu2bLF3LJlizljxgwzOjraHD16dKBeQpFzMe8BmWlUqJwV9LhOmjTJnDdvnrllyxbzjz/+MJ988kkTMD/99NNAvYQiqaDH9eTJk2aVKlXMG2+80dywYYO5aNEis06dOubdd98dqJdQJF3s+8Dtt99utm7durDjFgsFPaYzZ840bTabOXXqVHPbtm3mzz//bLZs2dJs1apVQPIHRWFhmqb5xhtvmNWrVzdDQkLMyy+/3Fy0aJF325133ml26NAhS/+FCxeazZs3N0NCQsyEhARz2rRphZy46CvoMQWyPapXr164oYuBghzXDh065Hhc77zzzsIPXsQV5Li+9tprZqNGjcyIiAgzOjrabN68uTl16lTT5XIFIHnRVdD3gMxUWOSuIMd1woQJZq1atcywsDCzTJky5pVXXml+8803AUhd9BX09/XPP/80O3fubIaHh5tVqlQxH3nkETM1NbWQUxd9BT2uJ06cMMPDw8233nqrkJMWHwU9pq+99prZsGFDMzw83KxYsaJ52223mXv37i3k1B6GaQbiPImIiIiIiJQkJf4eCxERERER8T8VFiIiIiIicslUWIiIiIiIyCVTYSEiIiIiIpdMhYWIiIiIiFwyFRYiIiIiInLJVFiIiIiIiMglU2EhIiIiIiKXTIWFiIj4xJgxY2jWrFnAfv7TTz/NPffck6++jz32GA8++KCfE4mIBBfNvC0iInkyDOOC2++8806mTJlCWloasbGxhZQqw8GDB6lTpw6//fYbCQkJefY/dOgQtWrV4rfffqNGjRr+DygiEgRUWIiISJ6SkpK87Q8//JBnnnmGzZs3e9eFh4cTExMTiGgAjBs3jkWLFvHDDz/ke58bbriB2rVrM2HCBD8mExEJHroUSkRE8hQfH+99xMTEYBhGtnV/vxQqMTGRPn36MG7cOOLi4ihdujRjx47F6XTy+OOPU7ZsWapUqcKMGTOy/Kx9+/bRr18/ypQpQ2xsLL1792bnzp0XzPfBBx/Qq1evLOs++eQTmjRpQnh4OLGxsXTu3JnTp097t/fq1Yv333//ko+NiIh4qLAQERG/+d///sf+/ftZvHgxEydOZMyYMfTs2ZMyZcqwYsUKhgwZwpAhQ9izZw8AqampXHPNNZQqVYrFixfz888/U6pUKbp37056enqOP+P48eP88ccftGzZ0rvuwIED3HrrrQwcOJA///yThQsX0rdvXzKfpG/VqhV79uxh165d/j0IIiJBQoWFiIj4TdmyZXnttdeoV68eAwcOpF69eqSmpjJq1Cjq1KnDyJEjCQkJ4ZdffgE8Zx4sFgtvv/02TZo0oUGDBsycOZPdu3ezcOHCHH/Grl27ME2TSpUqedcdOHAAp9NJ3759SUhIoEmTJgwdOpRSpUp5+1SuXBkgz7MhIiKSP7ZABxARkZKrUaNGWCwZ32HFxcXRuHFj77LVaiU2NpZDhw4BsGbNGv766y+ioqKyPM/Zs2fZtm1bjj/jzJkzAISFhXnXXXbZZXTq1IkmTZrQrVs3unbtyo033kiZMmW8fcLDwwHPWRIREbl0KixERMRv7HZ7lmXDMHJc53a7AXC73bRo0YI5c+Zke67y5cvn+DPKlSsHeC6JOt/HarWyYMECli5dyvz583n99dcZPXo0K1as8I4CdezYsQs+r4iIFIwuhRIRkSLj8ssvZ+vWrVSoUIHatWtneeQ26lStWrWIjo5m48aNWdYbhkH79u0ZO3Ys69atIyQkhHnz5nm3//HHH9jtdho1auTX1yQiEixUWIiISJFx2223Ua5cOXr37s2SJUvYsWMHixYt4qGHHmLv3r057mOxWOjcuTM///yzd92KFSsYN24cq1evZvfu3Xz22WccPnyYBg0aePssWbKEq666yntJlIiIXBoVFiIiUmRERESwePFiqlWrRt++fWnQoAEDBw7kzJkzREdH57rfPffcwwcffOC9pCo6OprFixfTo0cP6taty1NPPcUrr7zCtdde693n/fffZ/DgwX5/TSIiwUIT5ImISLFnmiZt2rRh+PDh3HrrrXn2/+abb3j88cf57bffsNl0u6GIiC/ojIWIiBR7hmHw1ltv4XQ689X/9OnTzJw5U0WFiIgP6YyFiIiIiIhcMp2xEBERERGRS6bCQkRERERELpkKCxERERERuWQqLERERERE5JKpsBARERERkUumwkJERERERC6ZCgsREREREblkKixEREREROSSqbAQEREREZFLpsJCREREREQu2f8DXwrB20xCe1cAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "for ch_pas, ch_py in zip(channels_pas, channels_py):\n", " plt.figure(figsize=(8,4))\n", " plt.plot(time, outputs_pas[ch_pas], label='IndMach012', lw=3)\n", " plt.plot(time, outputs_py[ch_py], label='PyIndMach012', ls='--', lw=2)\n", + " if outputs_cpp:\n", + " plt.plot(time, outputs_cpp[ch_py], label='CppIndMach012', ls='-.', lw=2)\n", + "\n", " plt.axvline(0.3, linestyle=':', color='k', alpha=0.5, label='Fault occurs')\n", " plt.axvline(0.4, linestyle='--', color='r', alpha=0.5, label='Relays operate')\n", " plt.legend()\n", @@ -829,7 +410,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/dss/UserModels/wrappers.py b/dss/UserModels/wrappers.py index 7b06b3e6..a97ef6ad 100644 --- a/dss/UserModels/wrappers.py +++ b/dss/UserModels/wrappers.py @@ -1,7 +1,10 @@ import re from dss_python_backend import ( # _dss_CapUserControl, - _dss_GenUserModel, + _dss_GenUserModel_AltDSS, + _dss_GenUserModel_OpenDSS_v7, + _dss_GenUserModel_OpenDSS_v8v9, + _dss_GenUserModel_OpenDSS_v10, # _dss_PVSystemUserModel, # _dss_StoreDynaModel, # _dss_StoreUserModel @@ -17,7 +20,7 @@ def __init__(self): self.ffi = self.cffi_module.ffi self.lib = self.cffi_module.lib self.models = [] - self.model_classes = {} + # self.model_classes = {} -- this is now initialized in the subclasses prefix = self.function_prefix for fname in self.function_names: @@ -26,10 +29,10 @@ def __init__(self): if self.Base is not None: self.Base.ffi = self.ffi - - def register(self, cls): - self.model_classes[cls.__name__.lower()] = cls - return cls + @classmethod + def register(this_class, model_cls): + this_class.model_classes[model_cls.__name__.lower()] = model_cls + return model_cls def Delete(self, ID): ID = ID[0] @@ -167,6 +170,7 @@ def Restore(self): # class CapUserControlWrapper(CommonWrapper): # Base = bases.CapUserControlBase +# model_classes = {} # cffi_module = _dss_CapUserControl # function_prefix = 'pyCapUserControl' # function_names = ( @@ -190,7 +194,8 @@ def Restore(self): class GenUserModelWrapper(DynamicsWrapper, SaveRestoreMixin): Base = bases.GenUserModelBase - cffi_module = _dss_GenUserModel + model_classes = {} + function_prefix = 'pyGenUserModel' function_names = ( 'New', @@ -210,6 +215,10 @@ class GenUserModelWrapper(DynamicsWrapper, SaveRestoreMixin): 'Restore' ) + def __init__(self, cffi_module): + self.cffi_module = cffi_module + CommonWrapper.__init__(self) + def New(self, GenData, DynaData, CallBacks): # Create a base instance to be replaced in Edit self.active_instance = self.Base(GenData, DynaData, CallBacks) @@ -219,6 +228,7 @@ def New(self, GenData, DynaData, CallBacks): # class PVSystemUserModelWrapper(DynamicsWrapper, SaveRestoreMixin): # Base = bases.PVSystemUserModelBase +# model_classes = {} # cffi_module = _dss_PVSystemUserModel # function_prefix = 'pyPVSystemUserModel' # function_names = ( @@ -248,6 +258,7 @@ def New(self, GenData, DynaData, CallBacks): # class StoreUserModelWrapper(DynamicsWrapper, SaveRestoreMixin): # Base = bases.StoreUserModelBase +# model_classes = {} # cffi_module = _dss_StoreUserModel # function_prefix = 'pyStoreUserModel' # function_names = ( @@ -277,6 +288,7 @@ def New(self, GenData, DynaData, CallBacks): # class StoreDynaModelWrapper(DynamicsWrapper): # Base = bases.StoreDynaModelBase +# model_classes = {} # cffi_module = _dss_StoreDynaModel # function_prefix = 'pyStoreDynaModel' # function_names = ( @@ -304,7 +316,34 @@ def New(self, GenData, DynaData, CallBacks): # Instantiate the wrappers to link the DLLs to the the Python code # CapUserControl = CapUserControlWrapper() -GenUserModel = GenUserModelWrapper() +GenUserModels = { + 'AltDSS': GenUserModelWrapper(_dss_GenUserModel_AltDSS), + 'OpenDSS_v7': GenUserModelWrapper(_dss_GenUserModel_OpenDSS_v7), + 'OpenDSS_v8v9': GenUserModelWrapper(_dss_GenUserModel_OpenDSS_v8v9), + 'OpenDSS_v10': GenUserModelWrapper(_dss_GenUserModel_OpenDSS_v10), +} + +def GenUserModel(DSS): + ''' + Select the approapriate GenUserModel instance according to the DSS engine provided. + + If the version is not recognized, defaults to OpenDSS v10.0. + ''' + ver = DSS.Version + if 'DSS C-API Library' in ver: + return GenUserModels['AltDSS'] + elif ver.startswith('Version 9.') or ver.startswith('Version 8.'): + return GenUserModels['OpenDSS_v8v9'] + elif ver.startswith('Version 7.'): + return GenUserModels['OpenDSS_v7'] + else: + # Assuming compatibility with OpenDSS v10.0 + return GenUserModels['OpenDSS_v10'] + +# To keep backwards compatibility, add `Base` and register` to GenUserModel +GenUserModel.Base = GenUserModelWrapper.Base +GenUserModel.register = GenUserModelWrapper.register + # PVSystemUserModel = PVSystemUserModelWrapper() # StoreDynaModel = StoreDynaModelWrapper() # StoreUserModel = StoreUserModelWrapper() From e5ec7e532a0ded83e89f509af330cdff5984ad7c Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 12 Jul 2024 19:41:03 -0300 Subject: [PATCH 02/82] Update Storages and add new WindGens --- dss/ICircuit.py | 6 +- dss/IStorages.py | 263 ++++++++++++++++++++++++++++++ dss/IWindGens.py | 414 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 682 insertions(+), 1 deletion(-) create mode 100644 dss/IWindGens.py diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 0f2e1811..a091f371 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -45,6 +45,7 @@ from .IReduceCkt import IReduceCkt from .IStorages import IStorages from .IGICSources import IGICSources +from .IWindGens import IWindGens from ._types import Float64Array, Int32Array, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex from .enums import DSSJSONFlags, DSSSaveFlags @@ -95,6 +96,7 @@ class ICircuit(Base): 'ReduceCkt', 'Storages', 'GICSources', + 'WindGens', ] _columns = [ @@ -165,6 +167,7 @@ class ICircuit(Base): Reactors: IReactors ReduceCkt: IReduceCkt Storages: IStorages + WindGens: IWindGens GICSources: IGICSources Parallel: IParallel @@ -213,8 +216,9 @@ def __init__(self, api_util): self.TSData = ITSData(api_util) if not api_util._is_odd else None self.Reactors = IReactors(api_util) if not api_util._is_odd else None self.ReduceCkt = IReduceCkt(api_util) #: Circuit Reduction Interface - self.Storages = IStorages(api_util) if not api_util._is_odd else None + self.Storages = IStorages(api_util) self.GICSources = IGICSources(api_util) + self.WindGens = IWindGens(api_util) if hasattr(api_util.lib, 'Parallel_CreateActor'): self.Parallel = IParallel(api_util) diff --git a/dss/IStorages.py b/dss/IStorages.py index c7752e69..a2de257c 100644 --- a/dss/IStorages.py +++ b/dss/IStorages.py @@ -19,6 +19,28 @@ class IStorages(Iterable): 'RegisterValues', 'puSOC', 'State', + 'AmpLimit', + 'AmpLimitGain', + 'ChargeTrigger', + 'ControlMode', + 'DischargeTrigger', + 'EffCharge', + 'EffDischarge', + 'Kp', + 'kV', + 'kVA', + 'kvar', + 'kVDC', + 'kW', + 'kWhRated', + 'kWRated', + 'LimitCurrent', + 'PF', + 'PITol', + 'SafeMode', + 'SafeVoltage', + 'TimeChargeTrig', + 'VarFollowInverter', ] @@ -57,3 +79,244 @@ def RegisterValues(self) -> Float64Array: self._check_for_error(self._lib.Storages_Get_RegisterValues_GR()) return self._get_float64_gr_array() + @property + def AmpLimit(self) -> float: + ''' + Current limit per phase for the IBR when operating in GFM mode. + ''' + return self._check_for_error(self._lib.Storages_Get_AmpLimit()) + + @AmpLimit.setter + def AmpLimit(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_AmpLimit(Value)) + + @property + def AmpLimitGain(self) -> float: + ''' + Use it for fine tuning the current limiter when active. + ''' + return self._check_for_error(self._lib.Storages_Get_AmpLimitGain()) + + @AmpLimitGain.setter + def AmpLimitGain(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_AmpLimitGain(Value)) + + @property + def ChargeTrigger(self) -> float: + ''' + Dispatch trigger value for charging the Storage. + ''' + return self._check_for_error(self._lib.Storages_Get_ChargeTrigger()) + + @ChargeTrigger.setter + def ChargeTrigger(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_ChargeTrigger(Value)) + + @property + def ControlMode(self) -> int: + ''' + Control mode for the inverter. It can be one of {GFM = 1 | GFL* = 0}. + ''' + return self._check_for_error(self._lib.Storages_Get_ControlMode()) + + @ControlMode.setter + def ControlMode(self, Value: int) -> None: + self._check_for_error(self._lib.Storages_Set_ControlMode(Value)) + + @property + def DischargeTrigger(self) -> float: + ''' + Dispatch trigger value for discharging the Storage. + ''' + return self._check_for_error(self._lib.Storages_Get_DischargeTrigger()) + + @DischargeTrigger.setter + def DischargeTrigger(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_DischargeTrigger(Value)) + + @property + def EffCharge(self) -> float: + ''' + Percentage efficiency for CHARGING the Storage element. + ''' + return self._check_for_error(self._lib.Storages_Get_EffCharge()) + + @EffCharge.setter + def EffCharge(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_EffCharge(Value)) + + @property + def EffDischarge(self) -> float: + ''' + Percentage efficiency for DISCHARGING the Storage element. + ''' + return self._check_for_error(self._lib.Storages_Get_EffDischarge()) + + @EffDischarge.setter + def EffDischarge(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_EffDischarge(Value)) + + @property + def Kp(self) -> float: + ''' + Proportional gain for the PI controller within the inverter. + Use it to modify the controller response in dynamics simulation mode. + ''' + return self._check_for_error(self._lib.Storages_Get_Kp()) + + @Kp.setter + def Kp(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_Kp(Value)) + + @property + def kV(self) -> float: + ''' + Nominal rated (1.0 per unit) voltage, kV, for Storage element. + ''' + return self._check_for_error(self._lib.Storages_Get_kV()) + + @kV.setter + def kV(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_kV(Value)) + + @property + def kVA(self) -> float: + ''' + Inverter nameplate capability (in kVA). Used as the base for Dynamics mode and Harmonics mode values. + ''' + return self._check_for_error(self._lib.Storages_Get_kVA()) + + @kVA.setter + def kVA(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_kVA(Value)) + + @property + def kvar(self) -> float: + ''' + Get/set the requested kvar value. Final kvar is subjected to the inverter ratings. Sets inverter to operate in constant kvar mode. + ''' + return self._check_for_error(self._lib.Storages_Get_kvar()) + + @kvar.setter + def kvar(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_kvar(Value)) + + @property + def kVDC(self) -> float: + ''' + Rated voltage (kV) at the input of the inverter while the storage is discharging + ''' + return self._check_for_error(self._lib.Storages_Get_kVDC()) + + @kVDC.setter + def kVDC(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_kVDC(Value)) + + @property + def kW(self) -> float: + ''' + Get/set the requested kW value. Final kW is subjected to the inverter ratings. + ''' + return self._check_for_error(self._lib.Storages_Get_kW()) + + @kW.setter + def kW(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_kW(Value)) + + @property + def kWhRated(self) -> float: + ''' + Rated Storage capacity in kWh. + ''' + return self._check_for_error(self._lib.Storages_Get_kWhRated()) + + @kWhRated.setter + def kWhRated(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_kWhRated(Value)) + + @property + def kWRated(self) -> float: + ''' + kW rating of power output. Base for Loadshapes when DispMode=Follow. Sets kVA property if it has not been specified yet. + ''' + return self._check_for_error(self._lib.Storages_Get_kWRated()) + + @kWRated.setter + def kWRated(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_kWRated(Value)) + + @property + def LimitCurrent(self) -> bool: + ''' + Limits current magnitude to Vminpu value for both 1-phase and 3-phase Storage similar to Generator Model 7. + For 3-phase, limits the positive-sequence current but not the negative-sequence." + ''' + return self._check_for_error(self._lib.Storages_Get_LimitCurrent()) != 0 + + @LimitCurrent.setter + def LimitCurrent(self, Value: bool) -> None: + self._check_for_error(self._lib.Storages_Set_LimitCurrent(Value)) + + @property + def PF(self) -> float: + ''' + Get/set the requested PF value. + ''' + return self._check_for_error(self._lib.Storages_Get_PF()) + + @PF.setter + def PF(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_PF(Value)) + + @property + def PITol(self) -> float: + ''' + Tolerance (%) for the closed loop controller of the inverter + ''' + return self._check_for_error(self._lib.Storages_Get_PITol()) + + @PITol.setter + def PITol(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_PITol(Value)) + + @property + def SafeMode(self) -> int: + ''' + (Read only) Indicates whether the inverter entered (Yes) or not (No) into Safe Mode. + ''' + return self._check_for_error(self._lib.Storages_Get_SafeMode()) + + @property + def SafeVoltage(self) -> float: + ''' + Indicates the voltage level (%) respect to the base voltage level for which the Inverter will operate. + ''' + return self._check_for_error(self._lib.Storages_Get_SafeVoltage()) + + @SafeVoltage.setter + def SafeVoltage(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_SafeVoltage(Value)) + + @property + def TimeChargeTrig(self) -> float: + ''' + Time of day in fractional hours (0230 = 2.5) at which Storage element will automatically go into charge state. + ''' + return self._check_for_error(self._lib.Storages_Get_TimeChargeTrig()) + + @TimeChargeTrig.setter + def TimeChargeTrig(self, Value: float) -> None: + self._check_for_error(self._lib.Storages_Set_TimeChargeTrig(Value)) + + @property + def VarFollowInverter(self) -> int: + ''' + Indicates if the reactive power generation/absorption does not respect the inverter status + ''' + return self._check_for_error(self._lib.Storages_Get_VarFollowInverter()) + + @VarFollowInverter.setter + def VarFollowInverter(self, Value: int) -> None: + self._check_for_error(self._lib.Storages_Set_VarFollowInverter(Value)) + + diff --git a/dss/IWindGens.py b/dss/IWindGens.py new file mode 100644 index 00000000..c233d998 --- /dev/null +++ b/dss/IWindGens.py @@ -0,0 +1,414 @@ +# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. +# Copyright (c) 2024 Paulo Meira +# Copyright (c) 2024 DSS-Extensions contributors +from ._cffi_api_util import Iterable +from ._types import Float64Array +from typing import List, Union, AnyStr +from .enums import StorageStates + +class IWindGens(Iterable): + '''WindGen objects''' + + __slots__ = [] + _is_circuit_element = True + + _columns = [ + 'Name', + 'idx', + 'RegisterNames', + 'RegisterValues', + 'Ag', + 'Bus1', + 'Class', + 'Cp', + 'daily', + 'duty', + 'IsDelta', + 'kV', + 'kVA', + 'kvar', + 'kW', + 'Lamda', + 'N_WTG', + 'NPoles', + 'pd', + 'PF', + 'Phases', + 'PSS', + 'QFlag', + 'QMode', + 'QSS', + 'Rad', + 'RThev', + 'VCutIn', + 'VCutOut', + 'Vss', + 'WindSpeed', + 'XThev', + 'Yearly', + ] + + @property + def RegisterNames(self) -> List[str]: + ''' + Array of Storage energy meter register names + + See also the enum `GeneratorRegisters`. + ''' + return self._check_for_error(self._get_string_array(self._lib.WindGens_Get_RegisterNames)) + + @property + def RegisterValues(self) -> Float64Array: + '''Array of values in Storage registers.''' + self._check_for_error(self._lib.WindGens_Get_RegisterValues_GR()) + return self._get_float64_gr_array() + + @property + def kV(self) -> float: + ''' + Nominal rated (1.0 per unit) voltage for the active WindGen, in kV. + ''' + return self._check_for_error(self._lib.WindGens_Get_kV()) + + @kV.setter + def kV(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_kV(Value)) + + @property + def kvar(self) -> float: + ''' + Base kvar for the active WindGen. + ''' + return self._check_for_error(self._lib.WindGens_Get_kvar()) + + @kvar.setter + def kvar(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_kvar(Value)) + + @property + def kW(self) -> float: + ''' + Total base kW for the active WindGen. + ''' + return self._check_for_error(self._lib.WindGens_Get_kW()) + + @kW.setter + def kW(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_kW(Value)) + + @property + def PF(self) -> float: + ''' + WindGen power factor. Power factor (pos. = producing vars). + ''' + return self._check_for_error(self._lib.WindGens_Get_PF()) + + @PF.setter + def PF(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_PF(Value)) + + @property + def kVA(self) -> float: + ''' + KVA rating of the electrical machine in the WindGen. + ''' + return self._check_for_error(self._lib.WindGens_Get_kVA()) + + @kVA.setter + def kVA(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_kVA(Value)) + + @property + def Ag(self) -> float: + ''' + Gearbox ratio + ''' + return self._check_for_error(self._lib.WindGens_Get_Ag()) + + @Ag.setter + def Ag(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_Ag(Value)) + + @property + def Cp(self) -> float: + ''' + Turbine performance coefficient. + ''' + return self._check_for_error(self._lib.WindGens_Get_Cp()) + + @Cp.setter + def Cp(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_Cp(Value)) + + @property + def Lamda(self) -> float: + ''' + Tip speed ratio + ''' + return self._check_for_error(self._lib.WindGens_Get_Lamda()) + + @Lamda.setter + def Lamda(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_Lamda(Value)) + + @property + def N_WTG(self) -> int: + ''' + Number of WTG in aggregation + ''' + return self._check_for_error(self._lib.WindGens_Get_N_WTG()) + + @N_WTG.setter + def N_WTG(self, Value: int) -> None: + self._check_for_error(self._lib.WindGens_Set_N_WTG(Value)) + + @property + def NPoles(self) -> int: + ''' + Number of pole pairs of the induction generator + ''' + return self._check_for_error(self._lib.WindGens_Get_NPoles()) + + @NPoles.setter + def NPoles(self, Value: int) -> None: + self._check_for_error(self._lib.WindGens_Set_NPoles(Value)) + + @property + def pd(self) -> float: + ''' + Air density in kg/m3 + ''' + return self._check_for_error(self._lib.WindGens_Get_pd()) + + @pd.setter + def pd(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_pd(Value)) + + @property + def PSS(self) -> float: + ''' + Steady state output real power. + ''' + return self._check_for_error(self._lib.WindGens_Get_PSS()) + + @PSS.setter + def PSS(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_PSS(Value)) + + @property + def QFlag(self) -> int: + ''' + Non-zero values enable reactive power and voltage control in the dynamic model. + ''' + return self._check_for_error(self._lib.WindGens_Get_QFlag()) + + @QFlag.setter + def QFlag(self, Value: int) -> None: + self._check_for_error(self._lib.WindGens_Set_QFlag(Value)) + + @property + def QMode(self) -> int: + ''' + Q control mode (0:Q, 1:PF, 2:VV). + ''' + return self._check_for_error(self._lib.WindGens_Get_QMode()) + + @QMode.setter + def QMode(self, Value: int) -> None: + self._check_for_error(self._lib.WindGens_Set_QMode(Value)) + + @property + def QSS(self) -> float: + ''' + Steady state output reactive power. + ''' + return self._check_for_error(self._lib.WindGens_Get_QSS()) + + @QSS.setter + def QSS(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_QSS(Value)) + + @property + def Rad(self) -> float: + ''' + Rotor radius in meters + ''' + return self._check_for_error(self._lib.WindGens_Get_Rad()) + + @Rad.setter + def Rad(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_Rad(Value)) + + @property + def RThev(self) -> float: + ''' + Per unit Thevenin equivalent resistance (R). + ''' + return self._check_for_error(self._lib.WindGens_Get_RThev()) + + @RThev.setter + def RThev(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_RThev(Value)) + + @property + def VCutIn(self) -> float: + ''' + Cut-in speed for the wind generator + ''' + return self._check_for_error(self._lib.WindGens_Get_VCutIn()) + + @VCutIn.setter + def VCutIn(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_VCutIn(Value)) + + @property + def VCutOut(self) -> float: + ''' + Cut-out speed for the wind generator + ''' + return self._check_for_error(self._lib.WindGens_Get_VCutOut()) + + @VCutOut.setter + def VCutOut(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_VCutOut(Value)) + + @property + def Vss(self) -> float: + ''' + Steady state voltage magnitude. + ''' + return self._check_for_error(self._lib.WindGens_Get_Vss()) + + @Vss.setter + def Vss(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_Vss(Value)) + + @property + def WindSpeed(self) -> float: + ''' + Wind speed in m/s + ''' + return self._check_for_error(self._lib.WindGens_Get_WindSpeed()) + + @WindSpeed.setter + def WindSpeed(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_WindSpeed(Value)) + + @property + def XThev(self) -> float: + ''' + Per unit Thevenin equivalent reactance (X). + ''' + return self._check_for_error(self._lib.WindGens_Get_XThev()) + + @XThev.setter + def XThev(self, Value: float) -> None: + self._check_for_error(self._lib.WindGens_Set_XThev(Value)) + + @property + def Phases(self) -> int: + ''' + Number of phases + + (API Extension) + ''' + return self._check_for_error(self._lib.WindGens_Get_Phases()) + + @Phases.setter + def Phases(self, Value: int) -> None: + ''' + Number of phases + + (API Extension) + ''' + self._check_for_error(self._lib.WindGens_Set_Phases(Value)) + + @property + def daily(self) -> str: + ''' + Name of the loadshape for daily wind speed + + (API Extension) + ''' + return self._get_string(self._check_for_error(self._lib.WindGens_Get_daily())) + + @daily.setter + def daily(self, Value: AnyStr) -> None: + if not isinstance(Value, bytes): + Value = Value.encode(self._api_util.codec) + + self._check_for_error(self._lib.WindGens_Set_daily(Value)) + + @property + def duty(self) -> str: + ''' + Name of the loadshape for a duty cycle simulation. + + (API Extension) + ''' + return self._get_string(self._check_for_error(self._lib.WindGens_Get_duty())) + + @duty.setter + def duty(self, Value: AnyStr) -> None: + if not isinstance(Value, bytes): + Value = Value.encode(self._api_util.codec) + + self._check_for_error(self._lib.WindGens_Set_duty(Value)) + + @property + def Yearly(self) -> str: + ''' + Name of yearly loadshape + + (API Extension) + ''' + return self._get_string(self._check_for_error(self._lib.WindGens_Get_Yearly())) + + @Yearly.setter + def Yearly(self, Value: AnyStr) -> None: + if not isinstance(Value, bytes): + Value = Value.encode(self._api_util.codec) + + self._check_for_error(self._lib.WindGens_Set_Yearly(Value)) + + @property + def IsDelta(self) -> bool: + ''' + WindGen connection. True/1 if delta connection, False/0 if wye. + + (API Extension) + ''' + return self._check_for_error(self._lib.WindGens_Get_IsDelta()) != 0 + + @IsDelta.setter + def IsDelta(self, Value: bool) -> None: + self._check_for_error(self._lib.WindGens_Set_IsDelta(Value)) + + @property + def Class(self) -> int: + ''' + An arbitrary integer number representing the class of WindGen so that WindGen values may be segregated by class. + + (API Extension) + ''' + return self._check_for_error(self._lib.WindGens_Get_Class_()) + + @Class.setter + def Class(self, Value: int) -> None: + self._check_for_error(self._lib.WindGens_Set_Class_(Value)) + + @property + def Bus1(self) -> str: + ''' + Bus to which the WindGen is connected. May include specific node specification. + + (API Extension) + ''' + return self._get_string(self._check_for_error(self._lib.WindGens_Get_Bus1())) + + @Bus1.setter + def Bus1(self, Value: AnyStr): + if not isinstance(Value, bytes): + Value = Value.encode(self._api_util.codec) + + self._check_for_error(self._lib.WindGens_Set_Bus1(Value)) From 02ae5b6364a4448174124e1b4980800d36e4708a Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:56:43 -0300 Subject: [PATCH 03/82] Tests: add NCIM samples --- tests/_settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/_settings.py b/tests/_settings.py index f448eec7..1b476760 100644 --- a/tests/_settings.py +++ b/tests/_settings.py @@ -44,6 +44,8 @@ #"L!Distrib/IEEETestCases/4wire-Delta/Kersting4wireIndMotor.dss", test_filenames = ''' +Version8/Distrib/Examples/NCIM/Xmission_System_Kundur2Area/Master.dss +Version8/Distrib/IEEETestCases/IEEE118Bus/master_file.dss Version8/Distrib/Examples/MemoryMappingLoadShapes/ckt24/master_ckt24-mm-csv-p.dss Version8/Distrib/Examples/MemoryMappingLoadShapes/ckt24/master_ckt24-mm-csv-pq.dss Version8/Distrib/Examples/MemoryMappingLoadShapes/ckt24/master_ckt24-mm-dbl-p.dss From 0d1657468f28cee14bd63168a7499d1e8f5cba27 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:34:45 -0300 Subject: [PATCH 04/82] Tests: add WindGen samples --- tests/_settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/_settings.py b/tests/_settings.py index 1b476760..89c658d3 100644 --- a/tests/_settings.py +++ b/tests/_settings.py @@ -44,6 +44,8 @@ #"L!Distrib/IEEETestCases/4wire-Delta/Kersting4wireIndMotor.dss", test_filenames = ''' +Version8/Distrib/Examples/WindGenerator/WindGen_QSTS/Run_IEEE123Bus_GFLDaily.DSS +Version8/Distrib/Examples/WindGenerator/WindGen_GFL_Dynamics/Run_IEEE123Bus_GFLDaily.DSS Version8/Distrib/Examples/NCIM/Xmission_System_Kundur2Area/Master.dss Version8/Distrib/IEEETestCases/IEEE118Bus/master_file.dss Version8/Distrib/Examples/MemoryMappingLoadShapes/ckt24/master_ckt24-mm-csv-p.dss From 53085298a01ea22272f72e0a826f4a70f3946b1c Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:54:32 -0300 Subject: [PATCH 05/82] FastDSS backend integration; some more AltDSS-Python functions. --- dss/IActiveClass.py | 40 +++-- dss/IBus.py | 135 ++++++++-------- dss/ICNData.py | 72 ++++----- dss/ICapControls.py | 64 ++++---- dss/ICapacitors.py | 31 ++-- dss/ICircuit.py | 128 ++++++--------- dss/ICktElement.py | 138 +++++++--------- dss/ICtrlQueue.py | 26 +-- dss/IDSS.py | 82 +++++----- dss/IDSSElement.py | 25 ++- dss/IDSSProgress.py | 11 +- dss/IDSSProperty.py | 18 +-- dss/IDSS_Executive.py | 16 +- dss/IDSSimComs.py | 6 +- dss/IError.py | 11 +- dss/IFuses.py | 55 +++---- dss/IGICSources.py | 36 ++--- dss/IGenerators.py | 89 +++++------ dss/IISources.py | 12 +- dss/ILineCodes.py | 57 ++++--- dss/ILineGeometries.py | 53 +++---- dss/ILineSpacings.py | 22 ++- dss/ILines.py | 128 +++++++-------- dss/ILoadShapes.py | 54 +++---- dss/ILoads.py | 161 +++++++++---------- dss/IMeters.py | 100 ++++++------ dss/IMonitors.py | 68 ++++---- dss/IPDElements.py | 86 +++++----- dss/IPVSystems.py | 73 ++++----- dss/IParallel.py | 32 ++-- dss/IParser.py | 58 +++---- dss/IReactors.py | 96 +++++------- dss/IReclosers.py | 63 ++++---- dss/IReduceCkt.py | 50 +++--- dss/IRegControls.py | 92 +++++------ dss/IRelays.py | 36 ++--- dss/ISensors.py | 58 +++---- dss/ISettings.py | 89 +++++------ dss/ISolution.py | 198 +++++++++++------------ dss/IStorages.py | 99 ++++++------ dss/ISwtControls.py | 33 ++-- dss/ITSData.py | 68 ++++---- dss/IText.py | 16 +- dss/ITopology.py | 46 +++--- dss/ITransformers.py | 89 +++++------ dss/IVsources.py | 20 +-- dss/IWindGens.py | 133 +++++++--------- dss/IWireData.py | 44 +++--- dss/IXYCurves.py | 38 +++-- dss/IYMatrix.py | 56 +++---- dss/IZIP.py | 30 ++-- dss/_cffi_api_util.py | 347 ++++++++++++++++++++++++++++++++++------- 52 files changed, 1753 insertions(+), 1835 deletions(-) diff --git a/dss/IActiveClass.py b/dss/IActiveClass.py index 75bce1a8..6c0c0762 100644 --- a/dss/IActiveClass.py +++ b/dss/IActiveClass.py @@ -4,7 +4,7 @@ from __future__ import annotations from ._cffi_api_util import Base from .enums import DSSJSONFlags -from typing import AnyStr, List, Iterator +from typing import AnyStr, List, Iterator, Optional class IActiveClass(Base): __slots__ = [] @@ -23,7 +23,7 @@ def ActiveClassName(self) -> str: Original COM help: https://opendss.epri.com/ActiveClassName.html ''' - return self._get_string(self._check_for_error(self._lib.ActiveClass_Get_ActiveClassName())) + return self._lib.ActiveClass_Get_ActiveClassName() @property def AllNames(self) -> List[str]: @@ -32,7 +32,7 @@ def AllNames(self) -> List[str]: Original COM help: https://opendss.epri.com/AllNames.html ''' - return self._check_for_error(self._get_string_array(self._lib.ActiveClass_Get_AllNames)) + return self._lib.ActiveClass_Get_AllNames() @property def Count(self) -> int: @@ -41,10 +41,10 @@ def Count(self) -> int: Original COM help: https://opendss.epri.com/Count.html ''' - return self._check_for_error(self._lib.ActiveClass_Get_Count()) + return self._lib.ActiveClass_Get_Count() def __len__(self) -> int: - return self._check_for_error(self._lib.ActiveClass_Get_Count()) + return self._lib.ActiveClass_Get_Count() def __iter__(self) -> Iterator[IActiveClass]: n = self.First @@ -62,7 +62,7 @@ def First(self) -> int: Original COM help: https://opendss.epri.com/First.html ''' - return self._check_for_error(self._lib.ActiveClass_Get_First()) + return self._lib.ActiveClass_Get_First() @property def Name(self) -> str: @@ -71,14 +71,11 @@ def Name(self) -> str: Original COM help: https://opendss.epri.com/Name.html ''' - return self._get_string(self._check_for_error(self._lib.ActiveClass_Get_Name())) + return self._lib.ActiveClass_Get_Name() @Name.setter def Name(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.ActiveClass_Set_Name(Value)) + self._lib.ActiveClass_Set_Name(Value) @property def Next(self) -> int: @@ -90,7 +87,7 @@ def Next(self) -> int: Original COM help: https://opendss.epri.com/Next.html ''' - return self._check_for_error(self._lib.ActiveClass_Get_Next()) + return self._lib.ActiveClass_Get_Next() @property def NumElements(self) -> int: @@ -99,7 +96,7 @@ def NumElements(self) -> int: Original COM help: https://opendss.epri.com/NumElements.html ''' - return self._check_for_error(self._lib.ActiveClass_Get_NumElements()) + return self._lib.ActiveClass_Get_NumElements() @property def ActiveClassParent(self) -> str: @@ -108,7 +105,7 @@ def ActiveClassParent(self) -> str: Original COM help: https://opendss.epri.com/ActiveClassParent.html ''' - return self._get_string(self._check_for_error(self._lib.ActiveClass_Get_ActiveClassParent())) + return self._lib.ActiveClass_Get_ActiveClassParent() def ToJSON(self, options: DSSJSONFlags = 0) -> str: ''' @@ -121,4 +118,17 @@ def ToJSON(self, options: DSSJSONFlags = 0) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.ActiveClass_ToJSON(options))) + return self._lib.ActiveClass_ToJSON(options) + + def to_altdss(self) -> Optional[DSSObject]: + ''' + Returns a Python object for the current active DSS object in this interface. + + Requires AltDSS-Python. + + *Available only for the AltDSS engine.* + + **(API Extension)** + ''' + ptr = self._lib.ActiveClass_Get_Pointer() + return self._api_util.get_dss_obj(ptr) diff --git a/dss/IBus.py b/dss/IBus.py index 3f1b6104..20989c83 100644 --- a/dss/IBus.py +++ b/dss/IBus.py @@ -4,7 +4,14 @@ from __future__ import annotations from ._cffi_api_util import Base from ._types import Float64Array, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex, Int32Array -from typing import List, Union, Iterator +from typing import List, Union, Iterator, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + try: + from altdss import Bus as AltBus + except: + pass + class IBus(Base): __slots__ = [] @@ -52,7 +59,7 @@ def GetUniqueNodeNumber(self, StartNumber: int) -> int: Original COM help: https://opendss.epri.com/GetUniqueNodeNumber.html ''' - return self._check_for_error(self._lib.Bus_GetUniqueNodeNumber(StartNumber)) + return self._lib.Bus_GetUniqueNodeNumber(StartNumber) def ZscRefresh(self) -> bool: ''' @@ -60,7 +67,7 @@ def ZscRefresh(self) -> bool: Original COM help: https://opendss.epri.com/ZscRefresh.html ''' - return self._check_for_error(self._lib.Bus_ZscRefresh()) != 0 + return self._lib.Bus_ZscRefresh() @property def Coorddefined(self) -> bool: @@ -69,7 +76,7 @@ def Coorddefined(self) -> bool: Original COM help: https://opendss.epri.com/Coorddefined.html ''' - return self._check_for_error(self._lib.Bus_Get_Coorddefined()) != 0 + return self._lib.Bus_Get_Coorddefined() @property def CplxSeqVoltages(self) -> Float64ArrayOrComplexArray: @@ -78,8 +85,7 @@ def CplxSeqVoltages(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/CplxSeqVoltages.html ''' - self._check_for_error(self._lib.Bus_Get_CplxSeqVoltages_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_CplxSeqVoltages_GR() @property def Cust_Duration(self) -> float: @@ -90,7 +96,7 @@ def Cust_Duration(self) -> float: Original COM help: https://opendss.epri.com/Cust_Duration.html ''' - return self._check_for_error(self._lib.Bus_Get_Cust_Duration()) + return self._lib.Bus_Get_Cust_Duration() @property def Cust_Interrupts(self) -> float: @@ -101,7 +107,7 @@ def Cust_Interrupts(self) -> float: Original COM help: https://opendss.epri.com/Cust_Interrupts.html ''' - return self._check_for_error(self._lib.Bus_Get_Cust_Interrupts()) + return self._lib.Bus_Get_Cust_Interrupts() @property def Distance(self) -> float: @@ -112,7 +118,7 @@ def Distance(self) -> float: Original COM help: https://opendss.epri.com/Distance.html ''' - return self._check_for_error(self._lib.Bus_Get_Distance()) + return self._lib.Bus_Get_Distance() @property def Int_Duration(self) -> float: @@ -123,7 +129,7 @@ def Int_Duration(self) -> float: Original COM help: https://opendss.epri.com/Int_Duration.html ''' - return self._check_for_error(self._lib.Bus_Get_Int_Duration()) + return self._lib.Bus_Get_Int_Duration() @property def Isc(self) -> Float64ArrayOrComplexArray: @@ -134,8 +140,7 @@ def Isc(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Isc.html ''' - self._check_for_error(self._lib.Bus_Get_Isc_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_Isc_GR() @property def Lambda(self) -> float: @@ -146,7 +151,7 @@ def Lambda(self) -> float: Original COM help: https://opendss.epri.com/Lambda.html ''' - return self._check_for_error(self._lib.Bus_Get_Lambda()) + return self._lib.Bus_Get_Lambda() @property def N_Customers(self) -> int: @@ -157,7 +162,7 @@ def N_Customers(self) -> int: Original COM help: https://opendss.epri.com/N_Customers.html ''' - return self._check_for_error(self._lib.Bus_Get_N_Customers()) + return self._lib.Bus_Get_N_Customers() @property def N_interrupts(self) -> float: @@ -168,7 +173,7 @@ def N_interrupts(self) -> float: Original COM help: https://opendss.epri.com/N_interrupts.html ''' - return self._check_for_error(self._lib.Bus_Get_N_interrupts()) + return self._lib.Bus_Get_N_interrupts() @property def Name(self) -> str: @@ -177,7 +182,7 @@ def Name(self) -> str: Original COM help: https://opendss.epri.com/Name1.html ''' - return self._get_string(self._check_for_error(self._lib.Bus_Get_Name())) + return self._lib.Bus_Get_Name() @property def Nodes(self) -> Int32Array: @@ -186,8 +191,7 @@ def Nodes(self) -> Int32Array: Original COM help: https://opendss.epri.com/Nodes.html ''' - self._check_for_error(self._lib.Bus_Get_Nodes_GR()) - return self._get_int32_gr_array() + return self._lib.Bus_Get_Nodes_GR() @property def NumNodes(self) -> int: @@ -196,7 +200,7 @@ def NumNodes(self) -> int: Original COM help: https://opendss.epri.com/NumNodes.html ''' - return self._check_for_error(self._lib.Bus_Get_NumNodes()) + return self._lib.Bus_Get_NumNodes() @property def SectionID(self) -> int: @@ -207,7 +211,7 @@ def SectionID(self) -> int: Original COM help: https://opendss.epri.com/SectionID.html ''' - return self._check_for_error(self._lib.Bus_Get_SectionID()) + return self._lib.Bus_Get_SectionID() @property def SeqVoltages(self) -> Float64Array: @@ -216,8 +220,7 @@ def SeqVoltages(self) -> Float64Array: Original COM help: https://opendss.epri.com/SeqVoltages.html ''' - self._check_for_error(self._lib.Bus_Get_SeqVoltages_GR()) - return self._get_float64_gr_array() + return self._lib.Bus_Get_SeqVoltages_GR() @property def TotalMiles(self) -> float: @@ -228,7 +231,7 @@ def TotalMiles(self) -> float: Original COM help: https://opendss.epri.com/TotalMiles.html ''' - return self._check_for_error(self._lib.Bus_Get_TotalMiles()) + return self._lib.Bus_Get_TotalMiles() @property def VLL(self) -> Float64ArrayOrComplexArray: @@ -237,8 +240,7 @@ def VLL(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/VLL.html ''' - self._check_for_error(self._lib.Bus_Get_VLL_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_VLL_GR() @property def VMagAngle(self) -> Float64Array: @@ -247,8 +249,7 @@ def VMagAngle(self) -> Float64Array: Original COM help: https://opendss.epri.com/VMagAngle.html ''' - self._check_for_error(self._lib.Bus_Get_VMagAngle_GR()) - return self._get_float64_gr_array() + return self._lib.Bus_Get_VMagAngle_GR() @property def Voc(self) -> Float64ArrayOrComplexArray: @@ -259,8 +260,7 @@ def Voc(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Voc.html ''' - self._check_for_error(self._lib.Bus_Get_Voc_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_Voc_GR() @property def Voltages(self) -> Float64ArrayOrComplexArray: @@ -269,8 +269,7 @@ def Voltages(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Voltages.html ''' - self._check_for_error(self._lib.Bus_Get_Voltages_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_Voltages_GR() @property def YscMatrix(self) -> Float64ArrayOrComplexArray: @@ -281,8 +280,7 @@ def YscMatrix(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/YscMatrix.html ''' - self._check_for_error(self._lib.Bus_Get_YscMatrix_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_YscMatrix_GR() @property def Zsc0(self) -> Float64ArrayOrSimpleComplex: @@ -293,8 +291,7 @@ def Zsc0(self) -> Float64ArrayOrSimpleComplex: Original COM help: https://opendss.epri.com/Zsc0.html ''' - self._check_for_error(self._lib.Bus_Get_Zsc0_GR()) - return self._get_complex128_gr_simple() + return self._lib.Bus_Get_Zsc0_GR() @property def Zsc1(self) -> Float64ArrayOrSimpleComplex: @@ -305,8 +302,7 @@ def Zsc1(self) -> Float64ArrayOrSimpleComplex: Original COM help: https://opendss.epri.com/Zsc1.html ''' - self._check_for_error(self._lib.Bus_Get_Zsc1_GR()) - return self._get_complex128_gr_simple() + return self._lib.Bus_Get_Zsc1_GR() @property def ZscMatrix(self) -> Float64ArrayOrComplexArray: @@ -317,8 +313,7 @@ def ZscMatrix(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/ZscMatrix.html ''' - self._check_for_error(self._lib.Bus_Get_ZscMatrix_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_ZscMatrix_GR() @property def kVBase(self) -> float: @@ -327,7 +322,7 @@ def kVBase(self) -> float: Original COM help: https://opendss.epri.com/kVBase.html ''' - return self._check_for_error(self._lib.Bus_Get_kVBase()) + return self._lib.Bus_Get_kVBase() @property def puVLL(self) -> Float64ArrayOrComplexArray: @@ -336,8 +331,7 @@ def puVLL(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/puVLL.html ''' - self._check_for_error(self._lib.Bus_Get_puVLL_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_puVLL_GR() @property def puVmagAngle(self) -> Float64Array: @@ -346,8 +340,7 @@ def puVmagAngle(self) -> Float64Array: Original COM help: https://opendss.epri.com/puVmagAngle.html ''' - self._check_for_error(self._lib.Bus_Get_puVmagAngle_GR()) - return self._get_float64_gr_array() + return self._lib.Bus_Get_puVmagAngle_GR() @property def puVoltages(self) -> Float64ArrayOrComplexArray: @@ -356,8 +349,7 @@ def puVoltages(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/puVoltages.html ''' - self._check_for_error(self._lib.Bus_Get_puVoltages_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_puVoltages_GR() @property def ZSC012Matrix(self) -> Float64ArrayOrComplexArray: @@ -370,8 +362,7 @@ def ZSC012Matrix(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/ZSC012Matrix.html ''' - self._check_for_error(self._lib.Bus_Get_ZSC012Matrix_GR()) - return self._get_complex128_gr_array() + return self._lib.Bus_Get_ZSC012Matrix_GR() @property def x(self) -> float: @@ -380,11 +371,11 @@ def x(self) -> float: Original COM help: https://opendss.epri.com/x.html ''' - return self._check_for_error(self._lib.Bus_Get_x()) + return self._lib.Bus_Get_x() @x.setter def x(self, Value: float): - self._check_for_error(self._lib.Bus_Set_x(Value)) + self._lib.Bus_Set_x(Value) @property def y(self) -> float: @@ -393,11 +384,11 @@ def y(self) -> float: Original COM help: https://opendss.epri.com/y.html ''' - return self._check_for_error(self._lib.Bus_Get_y()) + return self._lib.Bus_Get_y() @y.setter def y(self, Value: float): - self._check_for_error(self._lib.Bus_Set_y(Value)) + self._lib.Bus_Set_y(Value) @property def LoadList(self) -> List[str]: @@ -406,7 +397,7 @@ def LoadList(self) -> List[str]: Original COM help: https://opendss.epri.com/LoadList.html ''' - return self._check_for_error(self._get_string_array(self._lib.Bus_Get_LoadList)) + return self._lib.Bus_Get_LoadList() @property def LineList(self) -> List[str]: @@ -415,7 +406,7 @@ def LineList(self) -> List[str]: Original COM help: https://opendss.epri.com/LineList.html ''' - return self._check_for_error(self._get_string_array(self._lib.Bus_Get_LineList)) + return self._lib.Bus_Get_LineList() @property def AllPCEatBus(self) -> List[str]: @@ -424,7 +415,7 @@ def AllPCEatBus(self) -> List[str]: Original COM help: https://opendss.epri.com/AllPCEatBus.html ''' - result = self._check_for_error(self._get_string_array(self._lib.Bus_Get_AllPCEatBus)) + result = self._lib.Bus_Get_AllPCEatBus() if result: result.append('') #TODO: remove this -- added for full compatibility with COM else: @@ -439,7 +430,7 @@ def AllPDEatBus(self) -> List[str]: Original COM help: https://opendss.epri.com/AllPDEatBus1.html ''' - result = self._check_for_error(self._get_string_array(self._lib.Bus_Get_AllPDEatBus)) + result = self._lib.Bus_Get_AllPDEatBus() if result: result.append('') #TODO: remove this -- added for full compatibility with COM else: @@ -450,12 +441,9 @@ def AllPDEatBus(self) -> List[str]: def __getitem__(self, index: Union[int, str]) -> IBus: if isinstance(index, int): # bus index is zero based, pass it directly - self._check_for_error(self._lib.Circuit_SetActiveBusi(index)) + self._lib.Circuit_SetActiveBusi(index) else: - if not isinstance(index, bytes): - index = index.encode(self._api_util.codec) - - self._check_for_error(self._lib.Circuit_SetActiveBus(index)) + self._lib.Circuit_SetActiveBus(index) return self @@ -465,16 +453,33 @@ def __call__(self, index: Union[int, str]) -> IBus: def __iter__(self) -> Iterator[IBus]: if self._api_util._is_odd: for i in range(self._lib.Circuit_Get_NumBuses()): - self._check_for_error(self._lib.Circuit_SetActiveBusi(i)) + self._lib.Circuit_SetActiveBusi(i) yield self return - n = self._check_for_error(self._lib.Circuit_SetActiveBusi(0)) + n = self._lib.Circuit_SetActiveBusi(0) while n == 0: yield self - n = self._check_for_error(self._lib.Bus_Get_Next()) + n = self._lib.Bus_Get_Next() def __len__(self) -> int: '''Total number of Buses in the circuit.''' - return self._check_for_error(self._lib.Circuit_Get_NumBuses()) + return self._lib.Circuit_Get_NumBuses() + + def to_altdss(self) -> Optional[AltBus]: + ''' + Returns a Python object for the current active bus in the circuit (if any). + + Requires AltDSS-Python. + + *Available only for the AltDSS engine.* + + **(API Extension)** + + ''' + idx = self.lib.Bus_Get_idx() + if idx < 0: + return None + + return self._api_util.get_bus_obj(self.lib.Alt_Bus_GetByIndex(idx)) diff --git a/dss/ICNData.py b/dss/ICNData.py index 61f35511..fbf32c11 100644 --- a/dss/ICNData.py +++ b/dss/ICNData.py @@ -39,147 +39,147 @@ class ICNData(Iterable): @property def EmergAmps(self) -> float: '''Emergency ampere rating''' - return self._check_for_error(self._lib.CNData_Get_EmergAmps()) + return self._lib.CNData_Get_EmergAmps() @EmergAmps.setter def EmergAmps(self, Value: float): - self._check_for_error(self._lib.CNData_Set_EmergAmps(Value)) + self._lib.CNData_Set_EmergAmps(Value) @property def NormAmps(self) -> float: '''Normal Ampere rating''' - return self._check_for_error(self._lib.CNData_Get_NormAmps()) + return self._lib.CNData_Get_NormAmps() @NormAmps.setter def NormAmps(self, Value: float): - self._check_for_error(self._lib.CNData_Set_NormAmps(Value)) + self._lib.CNData_Set_NormAmps(Value) @property def Rdc(self) -> float: - return self._check_for_error(self._lib.CNData_Get_Rdc()) + return self._lib.CNData_Get_Rdc() @Rdc.setter def Rdc(self, Value: float): - self._check_for_error(self._lib.CNData_Set_Rdc(Value)) + self._lib.CNData_Set_Rdc(Value) @property def Rac(self) -> float: - return self._check_for_error(self._lib.CNData_Get_Rac()) + return self._lib.CNData_Get_Rac() @Rac.setter def Rac(self, Value: float): - self._check_for_error(self._lib.CNData_Set_Rac(Value)) + self._lib.CNData_Set_Rac(Value) @property def GMRac(self) -> float: - return self._check_for_error(self._lib.CNData_Get_GMRac()) + return self._lib.CNData_Get_GMRac() @GMRac.setter def GMRac(self, Value: float): - self._check_for_error(self._lib.CNData_Set_GMRac(Value)) + self._lib.CNData_Set_GMRac(Value) @property def GMRUnits(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.CNData_Get_GMRUnits())) + return LineUnits(self._lib.CNData_Get_GMRUnits()) @GMRUnits.setter def GMRUnits(self, Value: int): - self._check_for_error(self._lib.CNData_Set_GMRUnits(Value)) + self._lib.CNData_Set_GMRUnits(Value) @property def Radius(self) -> float: - return self._check_for_error(self._lib.CNData_Get_Radius()) + return self._lib.CNData_Get_Radius() @Radius.setter def Radius(self, Value: float): - self._check_for_error(self._lib.CNData_Set_Radius(Value)) + self._lib.CNData_Set_Radius(Value) @property def RadiusUnits(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.CNData_Get_RadiusUnits())) + return LineUnits(self._lib.CNData_Get_RadiusUnits()) @RadiusUnits.setter def RadiusUnits(self, Value: Union[int, LineUnits]): - self._check_for_error(self._lib.CNData_Set_RadiusUnits(Value)) + self._lib.CNData_Set_RadiusUnits(Value) @property def ResistanceUnits(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.CNData_Get_ResistanceUnits())) + return LineUnits(self._lib.CNData_Get_ResistanceUnits()) @ResistanceUnits.setter def ResistanceUnits(self, Value: Union[int, LineUnits]): - self._check_for_error(self._lib.CNData_Set_ResistanceUnits(Value)) + self._lib.CNData_Set_ResistanceUnits(Value) @property def Diameter(self) -> float: - return self._check_for_error(self._lib.CNData_Get_Diameter()) + return self._lib.CNData_Get_Diameter() @Diameter.setter def Diameter(self, Value: float): - self._check_for_error(self._lib.CNData_Set_Diameter(Value)) + self._lib.CNData_Set_Diameter(Value) @property def EpsR(self) -> float: - return self._check_for_error(self._lib.CNData_Get_EpsR()) + return self._lib.CNData_Get_EpsR() @EpsR.setter def EpsR(self, Value: float): - self._check_for_error(self._lib.CNData_Set_EpsR(Value)) + self._lib.CNData_Set_EpsR(Value) @property def InsLayer(self) -> float: - return self._check_for_error(self._lib.CNData_Get_InsLayer()) + return self._lib.CNData_Get_InsLayer() @InsLayer.setter def InsLayer(self, Value: float): - self._check_for_error(self._lib.CNData_Set_InsLayer(Value)) + self._lib.CNData_Set_InsLayer(Value) @property def DiaIns(self) -> float: - return self._check_for_error(self._lib.CNData_Get_DiaIns()) + return self._lib.CNData_Get_DiaIns() @DiaIns.setter def DiaIns(self, Value: float): - self._check_for_error(self._lib.CNData_Set_DiaIns(Value)) + self._lib.CNData_Set_DiaIns(Value) @property def DiaCable(self) -> float: - return self._check_for_error(self._lib.CNData_Get_DiaCable()) + return self._lib.CNData_Get_DiaCable() @DiaCable.setter def DiaCable(self, Value: float): - self._check_for_error(self._lib.CNData_Set_DiaCable(Value)) + self._lib.CNData_Set_DiaCable(Value) @property def k(self) -> int: - return self._check_for_error(self._lib.CNData_Get_k()) + return self._lib.CNData_Get_k() @k.setter def k(self, Value: int): - self._check_for_error(self._lib.CNData_Set_k(Value)) + self._lib.CNData_Set_k(Value) @property def DiaStrand(self) -> float: - return self._check_for_error(self._lib.CNData_Get_DiaStrand()) + return self._lib.CNData_Get_DiaStrand() @DiaStrand.setter def DiaStrand(self, Value: float): - self._check_for_error(self._lib.CNData_Set_DiaStrand(Value)) + self._lib.CNData_Set_DiaStrand(Value) @property def GmrStrand(self) -> float: - return self._check_for_error(self._lib.CNData_Get_GmrStrand()) + return self._lib.CNData_Get_GmrStrand() @GmrStrand.setter def GmrStrand(self, Value: float): - self._check_for_error(self._lib.CNData_Set_GmrStrand(Value)) + self._lib.CNData_Set_GmrStrand(Value) @property def RStrand(self) -> float: - return self._check_for_error(self._lib.CNData_Get_RStrand()) + return self._lib.CNData_Get_RStrand() @RStrand.setter def RStrand(self, Value: float): - self._check_for_error(self._lib.CNData_Set_RStrand(Value)) + self._lib.CNData_Set_RStrand(Value) diff --git a/dss/ICapControls.py b/dss/ICapControls.py index 2d8f4f47..b712b1e1 100644 --- a/dss/ICapControls.py +++ b/dss/ICapControls.py @@ -33,7 +33,7 @@ def Reset(self): Original COM help: https://opendss.epri.com/Reset.html ''' - self._check_for_error(self._lib.CapControls_Reset()) + self._lib.CapControls_Reset() @property def CTratio(self) -> float: @@ -42,11 +42,11 @@ def CTratio(self) -> float: Original COM help: https://opendss.epri.com/CTratio.html ''' - return self._check_for_error(self._lib.CapControls_Get_CTratio()) + return self._lib.CapControls_Get_CTratio() @CTratio.setter def CTratio(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_CTratio(Value)) + self._lib.CapControls_Set_CTratio(Value) @property def Capacitor(self) -> str: @@ -55,14 +55,11 @@ def Capacitor(self) -> str: Original COM help: https://opendss.epri.com/Capacitor.html ''' - return self._get_string(self._check_for_error(self._lib.CapControls_Get_Capacitor())) + return self._lib.CapControls_Get_Capacitor() @Capacitor.setter def Capacitor(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.CapControls_Set_Capacitor(Value)) + self._lib.CapControls_Set_Capacitor(Value) @property def DeadTime(self) -> float: @@ -73,11 +70,11 @@ def DeadTime(self) -> float: Original COM help: https://opendss.epri.com/DeadTime.html ''' - return self._check_for_error(self._lib.CapControls_Get_DeadTime()) + return self._lib.CapControls_Get_DeadTime() @DeadTime.setter def DeadTime(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_DeadTime(Value)) + self._lib.CapControls_Set_DeadTime(Value) @property def Delay(self) -> float: @@ -86,11 +83,11 @@ def Delay(self) -> float: Original COM help: https://opendss.epri.com/Delay.html ''' - return self._check_for_error(self._lib.CapControls_Get_Delay()) + return self._lib.CapControls_Get_Delay() @Delay.setter def Delay(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_Delay(Value)) + self._lib.CapControls_Set_Delay(Value) @property def DelayOff(self) -> float: @@ -99,11 +96,11 @@ def DelayOff(self) -> float: Original COM help: https://opendss.epri.com/DelayOff.html ''' - return self._check_for_error(self._lib.CapControls_Get_DelayOff()) + return self._lib.CapControls_Get_DelayOff() @DelayOff.setter def DelayOff(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_DelayOff(Value)) + self._lib.CapControls_Set_DelayOff(Value) @property def Mode(self) -> CapControlModes: @@ -112,11 +109,11 @@ def Mode(self) -> CapControlModes: Original COM help: https://opendss.epri.com/Mode.html ''' - return CapControlModes(self._check_for_error(self._lib.CapControls_Get_Mode())) + return CapControlModes(self._lib.CapControls_Get_Mode()) @Mode.setter def Mode(self, Value: Union[CapControlModes, int]): - self._check_for_error(self._lib.CapControls_Set_Mode(Value)) + self._lib.CapControls_Set_Mode(Value) @property def MonitoredObj(self) -> int: @@ -125,14 +122,11 @@ def MonitoredObj(self) -> int: Original COM help: https://opendss.epri.com/MonitoredObj.html ''' - return self._get_string(self._check_for_error(self._lib.CapControls_Get_MonitoredObj())) + return self._lib.CapControls_Get_MonitoredObj() @MonitoredObj.setter def MonitoredObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.CapControls_Set_MonitoredObj(Value)) + self._lib.CapControls_Set_MonitoredObj(Value) @property def MonitoredTerm(self) -> int: @@ -141,11 +135,11 @@ def MonitoredTerm(self) -> int: Original COM help: https://opendss.epri.com/MonitoredTerm.html ''' - return self._check_for_error(self._lib.CapControls_Get_MonitoredTerm()) + return self._lib.CapControls_Get_MonitoredTerm() @MonitoredTerm.setter def MonitoredTerm(self, Value: int): - self._check_for_error(self._lib.CapControls_Set_MonitoredTerm(Value)) + self._lib.CapControls_Set_MonitoredTerm(Value) @property def OFFSetting(self) -> float: @@ -154,11 +148,11 @@ def OFFSetting(self) -> float: Original COM help: https://opendss.epri.com/OFFSetting.html ''' - return self._check_for_error(self._lib.CapControls_Get_OFFSetting()) + return self._lib.CapControls_Get_OFFSetting() @OFFSetting.setter def OFFSetting(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_OFFSetting(Value)) + self._lib.CapControls_Set_OFFSetting(Value) @property def ONSetting(self) -> float: @@ -167,11 +161,11 @@ def ONSetting(self) -> float: Original COM help: https://opendss.epri.com/ONSetting.html ''' - return self._check_for_error(self._lib.CapControls_Get_ONSetting()) + return self._lib.CapControls_Get_ONSetting() @ONSetting.setter def ONSetting(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_ONSetting(Value)) + self._lib.CapControls_Set_ONSetting(Value) @property def PTratio(self) -> float: @@ -180,11 +174,11 @@ def PTratio(self) -> float: Original COM help: https://opendss.epri.com/PTratio.html ''' - return self._check_for_error(self._lib.CapControls_Get_PTratio()) + return self._lib.CapControls_Get_PTratio() @PTratio.setter def PTratio(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_PTratio(Value)) + self._lib.CapControls_Set_PTratio(Value) @property def UseVoltOverride(self) -> float: @@ -193,11 +187,11 @@ def UseVoltOverride(self) -> float: Original COM help: https://opendss.epri.com/UseVoltOverride.html ''' - return self._check_for_error(self._lib.CapControls_Get_UseVoltOverride()) != 0 + return self._lib.CapControls_Get_UseVoltOverride() @UseVoltOverride.setter def UseVoltOverride(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_UseVoltOverride(Value)) + self._lib.CapControls_Set_UseVoltOverride(Value) @property def Vmax(self) -> float: @@ -206,11 +200,11 @@ def Vmax(self) -> float: Original COM help: https://opendss.epri.com/Vmax.html ''' - return self._check_for_error(self._lib.CapControls_Get_Vmax()) + return self._lib.CapControls_Get_Vmax() @Vmax.setter def Vmax(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_Vmax(Value)) + self._lib.CapControls_Set_Vmax(Value) @property def Vmin(self) -> float: @@ -219,8 +213,8 @@ def Vmin(self) -> float: Original COM help: https://opendss.epri.com/Vmin.html ''' - return self._check_for_error(self._lib.CapControls_Get_Vmin()) + return self._lib.CapControls_Get_Vmin() @Vmin.setter def Vmin(self, Value: float): - self._check_for_error(self._lib.CapControls_Set_Vmin(Value)) + self._lib.CapControls_Set_Vmin(Value) diff --git a/dss/ICapacitors.py b/dss/ICapacitors.py index 0a5201de..c58f8a3e 100644 --- a/dss/ICapacitors.py +++ b/dss/ICapacitors.py @@ -20,16 +20,16 @@ class ICapacitors(Iterable): ] def AddStep(self) -> bool: - return self._check_for_error(self._lib.Capacitors_AddStep()) != 0 + return self._lib.Capacitors_AddStep() def Close(self): - self._check_for_error(self._lib.Capacitors_Close()) + self._lib.Capacitors_Close() def Open(self): - self._check_for_error(self._lib.Capacitors_Open()) + self._lib.Capacitors_Open() def SubtractStep(self) -> bool: - return self._check_for_error(self._lib.Capacitors_SubtractStep()) != 0 + return self._lib.Capacitors_SubtractStep() @property def AvailableSteps(self) -> int: @@ -38,7 +38,7 @@ def AvailableSteps(self) -> int: Original COM help: https://opendss.epri.com/AvailableSteps.html ''' - return self._check_for_error(self._lib.Capacitors_Get_AvailableSteps()) + return self._lib.Capacitors_Get_AvailableSteps() @property def IsDelta(self) -> bool: @@ -47,11 +47,11 @@ def IsDelta(self) -> bool: Original COM help: https://opendss.epri.com/IsDelta.html ''' - return self._check_for_error(self._lib.Capacitors_Get_IsDelta()) != 0 + return self._lib.Capacitors_Get_IsDelta() @IsDelta.setter def IsDelta(self, Value: bool): - self._check_for_error(self._lib.Capacitors_Set_IsDelta(Value)) + self._lib.Capacitors_Set_IsDelta(Value) @property def NumSteps(self) -> int: @@ -60,11 +60,11 @@ def NumSteps(self) -> int: Original COM help: https://opendss.epri.com/NumSteps.html ''' - return self._check_for_error(self._lib.Capacitors_Get_NumSteps()) + return self._lib.Capacitors_Get_NumSteps() @NumSteps.setter def NumSteps(self, Value: int): - self._check_for_error(self._lib.Capacitors_Set_NumSteps(Value)) + self._lib.Capacitors_Set_NumSteps(Value) @property def States(self) -> Int32Array: @@ -73,13 +73,12 @@ def States(self) -> Int32Array: Original COM help: https://opendss.epri.com/States.html ''' - self._check_for_error(self._lib.Capacitors_Get_States_GR()) - return self._get_int32_gr_array() + return self._lib.Capacitors_Get_States_GR() @States.setter def States(self, Value: Int32Array): Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) - self._check_for_error(self._lib.Capacitors_Set_States(ValuePtr, ValueCount)) + self._lib.Capacitors_Set_States(ValuePtr, ValueCount) @property def kV(self) -> float: @@ -88,17 +87,17 @@ def kV(self) -> float: Original COM help: https://opendss.epri.com/kV.html ''' - return self._check_for_error(self._lib.Capacitors_Get_kV()) + return self._lib.Capacitors_Get_kV() @kV.setter def kV(self, Value): - self._check_for_error(self._lib.Capacitors_Set_kV(Value)) + self._lib.Capacitors_Set_kV(Value) @property def kvar(self) -> float: '''Total bank KVAR, distributed equally among phases and steps.''' - return self._check_for_error(self._lib.Capacitors_Get_kvar()) + return self._lib.Capacitors_Get_kvar() @kvar.setter def kvar(self, Value: float): - self._check_for_error(self._lib.Capacitors_Set_kvar(Value)) + self._lib.Capacitors_Set_kvar(Value) diff --git a/dss/ICircuit.py b/dss/ICircuit.py index a091f371..14003fe4 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -243,7 +243,7 @@ def Capacity(self, Start: float, Increment: float) -> float: Original COM help: https://opendss.epri.com/Capacity1.html ''' - return self._check_for_error(self._lib.Circuit_Capacity(Start, Increment)) + return self._lib.Circuit_Capacity(Start, Increment) def Disable(self, Name: AnyStr): ''' @@ -251,10 +251,7 @@ def Disable(self, Name: AnyStr): Original COM help: https://opendss.epri.com/Disable.html ''' - if not isinstance(Name, bytes): - Name = Name.encode(self._api_util.codec) - - self._check_for_error(self._lib.Circuit_Disable(Name)) + self._lib.Circuit_Disable(Name) def Enable(self, Name: AnyStr): ''' @@ -262,10 +259,7 @@ def Enable(self, Name: AnyStr): Original COM help: https://opendss.epri.com/Enable.html ''' - if not isinstance(Name, bytes): - Name = Name.encode(self._api_util.codec) - - self._check_for_error(self._lib.Circuit_Enable(Name)) + self._lib.Circuit_Enable(Name) def EndOfTimeStepUpdate(self): ''' @@ -273,7 +267,7 @@ def EndOfTimeStepUpdate(self): Original COM help: https://opendss.epri.com/EndOfTimeStepUpdate.html ''' - self._check_for_error(self._lib.Circuit_EndOfTimeStepUpdate()) + self._lib.Circuit_EndOfTimeStepUpdate() def FirstElement(self) -> int: ''' @@ -283,7 +277,7 @@ def FirstElement(self) -> int: Original COM help: https://opendss.epri.com/FirstElement.html ''' - return self._check_for_error(self._lib.Circuit_FirstElement()) + return self._lib.Circuit_FirstElement() def FirstPCElement(self) -> int: ''' @@ -293,7 +287,7 @@ def FirstPCElement(self) -> int: Original COM help: https://opendss.epri.com/FirstPCElement.html ''' - return self._check_for_error(self._lib.Circuit_FirstPCElement()) + return self._lib.Circuit_FirstPCElement() def FirstPDElement(self) -> int: ''' @@ -303,26 +297,23 @@ def FirstPDElement(self) -> int: Original COM help: https://opendss.epri.com/FirstPDElement.html ''' - return self._check_for_error(self._lib.Circuit_FirstPDElement()) + return self._lib.Circuit_FirstPDElement() def AllNodeDistancesByPhase(self, Phase: int) -> Float64Array: '''Returns an array of doubles representing the distances to parent EnergyMeter. Sequence of array corresponds to other node ByPhase properties.''' - self._check_for_error(self._lib.Circuit_Get_AllNodeDistancesByPhase_GR(Phase)) - return self._get_float64_gr_array() + return self._lib.Circuit_Get_AllNodeDistancesByPhase_GR(Phase) def AllNodeNamesByPhase(self, Phase: int) -> List[str]: '''Return array of strings of the node names for the By Phase criteria. Sequence corresponds to other ByPhase properties.''' - return self._check_for_error(self._get_string_array(self._lib.Circuit_Get_AllNodeNamesByPhase, Phase)) + return self._lib.Circuit_Get_AllNodeNamesByPhase(Phase) def AllNodeVmagByPhase(self, Phase: int) -> Float64Array: '''Returns Array of doubles represent voltage magnitudes for nodes on the specified phase.''' - self._check_for_error(self._lib.Circuit_Get_AllNodeVmagByPhase_GR(Phase)) - return self._get_float64_gr_array() + return self._lib.Circuit_Get_AllNodeVmagByPhase_GR(Phase) def AllNodeVmagPUByPhase(self, Phase: int) -> Float64Array: '''Returns array of per unit voltage magnitudes for each node by phase''' - self._check_for_error(self._lib.Circuit_Get_AllNodeVmagPUByPhase_GR(Phase)) - return self._get_float64_gr_array() + return self._lib.Circuit_Get_AllNodeVmagPUByPhase_GR(Phase) def NextElement(self) -> int: ''' @@ -331,7 +322,7 @@ def NextElement(self) -> int: Original COM help: https://opendss.epri.com/NextElement.html ''' - return self._check_for_error(self._lib.Circuit_NextElement()) + return self._lib.Circuit_NextElement() def NextPCElement(self) -> int: ''' @@ -339,7 +330,7 @@ def NextPCElement(self) -> int: Original COM help: https://opendss.epri.com/NextPCElement.html ''' - return self._check_for_error(self._lib.Circuit_NextPCElement()) + return self._lib.Circuit_NextPCElement() def NextPDElement(self) -> int: ''' @@ -347,7 +338,7 @@ def NextPDElement(self) -> int: Original COM help: https://opendss.epri.com/NextPDElement.html ''' - return self._check_for_error(self._lib.Circuit_NextPDElement()) + return self._lib.Circuit_NextPDElement() def Sample(self): ''' @@ -355,7 +346,7 @@ def Sample(self): Original COM help: https://opendss.epri.com/Sample.html ''' - self._check_for_error(self._lib.Circuit_Sample()) + self._lib.Circuit_Sample() def SaveSample(self): ''' @@ -363,7 +354,7 @@ def SaveSample(self): Original COM help: https://opendss.epri.com/SaveSample.html ''' - self._check_for_error(self._lib.Circuit_SaveSample()) + self._lib.Circuit_SaveSample() def SetActiveBus(self, BusName: AnyStr) -> int: ''' @@ -373,10 +364,7 @@ def SetActiveBus(self, BusName: AnyStr) -> int: Original COM help: https://opendss.epri.com/SetActiveBus.html ''' - if not isinstance(BusName, bytes): - BusName = BusName.encode(self._api_util.codec) - - return self._check_for_error(self._lib.Circuit_SetActiveBus(BusName)) + return self._lib.Circuit_SetActiveBus(BusName) def SetActiveBusi(self, BusIndex: int) -> int: ''' @@ -387,7 +375,7 @@ def SetActiveBusi(self, BusIndex: int) -> int: Original COM help: https://opendss.epri.com/SetActiveBusi.html ''' - return self._check_for_error(self._lib.Circuit_SetActiveBusi(BusIndex)) + return self._lib.Circuit_SetActiveBusi(BusIndex) def SetActiveClass(self, ClassName: AnyStr) -> int: ''' @@ -397,10 +385,7 @@ def SetActiveClass(self, ClassName: AnyStr) -> int: Original COM help: https://opendss.epri.com/SetActiveClass.html ''' - if not isinstance(ClassName, bytes): - ClassName = ClassName.encode(self._api_util.codec) - - return self._check_for_error(self._lib.Circuit_SetActiveClass(ClassName)) + return self._lib.Circuit_SetActiveClass(ClassName) def SetActiveElement(self, FullName: AnyStr) -> int: ''' @@ -410,10 +395,7 @@ def SetActiveElement(self, FullName: AnyStr) -> int: Original COM help: https://opendss.epri.com/SetActiveElement.html ''' - if not isinstance(FullName, bytes): - FullName = FullName.encode(self._api_util.codec) - - return self._check_for_error(self._lib.Circuit_SetActiveElement(FullName)) + return self._lib.Circuit_SetActiveElement(FullName) def UpdateStorage(self): ''' @@ -423,7 +405,7 @@ def UpdateStorage(self): Original COM help: https://opendss.epri.com/UpdateStorage.html ''' - self._check_for_error(self._lib.Circuit_UpdateStorage()) + self._lib.Circuit_UpdateStorage() @property def AllBusDistances(self) -> Float64Array: @@ -432,8 +414,7 @@ def AllBusDistances(self) -> Float64Array: Original COM help: https://opendss.epri.com/AllBusDistances.html ''' - self._check_for_error(self._lib.Circuit_Get_AllBusDistances_GR()) - return self._get_float64_gr_array() + return self._lib.Circuit_Get_AllBusDistances_GR() @property def AllBusNames(self) -> List[str]: @@ -442,7 +423,7 @@ def AllBusNames(self) -> List[str]: Original COM help: https://opendss.epri.com/AllBusNames.html ''' - return self._check_for_error(self._get_string_array(self._lib.Circuit_Get_AllBusNames)) + return self._lib.Circuit_Get_AllBusNames() @property def AllBusVmag(self) -> Float64Array: @@ -451,8 +432,7 @@ def AllBusVmag(self) -> Float64Array: Original COM help: https://opendss.epri.com/AllBusVmag.html ''' - self._check_for_error(self._lib.Circuit_Get_AllBusVmag_GR()) - return self._get_float64_gr_array() + return self._lib.Circuit_Get_AllBusVmag_GR() @property def AllBusVmagPu(self) -> Float64Array: @@ -461,8 +441,7 @@ def AllBusVmagPu(self) -> Float64Array: Original COM help: https://opendss.epri.com/AllBusVmagPu.html ''' - self._check_for_error(self._lib.Circuit_Get_AllBusVmagPu_GR()) - return self._get_float64_gr_array() + return self._lib.Circuit_Get_AllBusVmagPu_GR() @property def AllBusVolts(self) -> Float64ArrayOrComplexArray: @@ -471,8 +450,7 @@ def AllBusVolts(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/AllBusVolts.html ''' - self._check_for_error(self._lib.Circuit_Get_AllBusVolts_GR()) - return self._get_complex128_gr_array() + return self._lib.Circuit_Get_AllBusVolts_GR() @property def AllElementLosses(self) -> Float64ArrayOrComplexArray: @@ -481,8 +459,7 @@ def AllElementLosses(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/AllElementLosses.html ''' - self._check_for_error(self._lib.Circuit_Get_AllElementLosses_GR()) - return self._get_complex128_gr_array() + return self._lib.Circuit_Get_AllElementLosses_GR() @property def AllElementNames(self) -> List[str]: @@ -491,7 +468,7 @@ def AllElementNames(self) -> List[str]: Original COM help: https://opendss.epri.com/AllElementNames.html ''' - return self._check_for_error(self._get_string_array(self._lib.Circuit_Get_AllElementNames)) + return self._lib.Circuit_Get_AllElementNames() @property def AllNodeDistances(self) -> Float64Array: @@ -500,8 +477,7 @@ def AllNodeDistances(self) -> Float64Array: Original COM help: https://opendss.epri.com/AllNodeDistances.html ''' - self._check_for_error(self._lib.Circuit_Get_AllNodeDistances_GR()) - return self._get_float64_gr_array() + return self._lib.Circuit_Get_AllNodeDistances_GR() @property def AllNodeNames(self) -> List[str]: @@ -510,7 +486,7 @@ def AllNodeNames(self) -> List[str]: Original COM help: https://opendss.epri.com/AllNodeNames.html ''' - return self._check_for_error(self._get_string_array(self._lib.Circuit_Get_AllNodeNames)) + return self._lib.Circuit_Get_AllNodeNames() @property def LineLosses(self) -> Float64ArrayOrSimpleComplex: @@ -519,8 +495,7 @@ def LineLosses(self) -> Float64ArrayOrSimpleComplex: Original COM help: https://opendss.epri.com/LineLosses.html ''' - self._check_for_error(self._lib.Circuit_Get_LineLosses_GR()) - return self._get_complex128_gr_simple() + return self._lib.Circuit_Get_LineLosses_GR() @property def Losses(self) -> Float64ArrayOrSimpleComplex: @@ -529,13 +504,12 @@ def Losses(self) -> Float64ArrayOrSimpleComplex: Original COM help: https://opendss.epri.com/Losses.html ''' - self._check_for_error(self._lib.Circuit_Get_Losses_GR()) - return self._get_complex128_gr_simple() + return self._lib.Circuit_Get_Losses_GR() @property def Name(self) -> str: '''Name of the active circuit.''' - return self._get_string(self._check_for_error(self._lib.Circuit_Get_Name())) + return self._lib.Circuit_Get_Name() @property def NumBuses(self) -> int: @@ -544,7 +518,7 @@ def NumBuses(self) -> int: Original COM help: https://opendss.epri.com/NumBuses.html ''' - return self._check_for_error(self._lib.Circuit_Get_NumBuses()) + return self._lib.Circuit_Get_NumBuses() @property def NumCktElements(self) -> int: @@ -553,7 +527,7 @@ def NumCktElements(self) -> int: Original COM help: https://opendss.epri.com/NumCktElements.html ''' - return self._check_for_error(self._lib.Circuit_Get_NumCktElements()) + return self._lib.Circuit_Get_NumCktElements() @property def NumNodes(self) -> int: @@ -562,7 +536,7 @@ def NumNodes(self) -> int: Original COM help: https://opendss.epri.com/NumNodes1.html ''' - return self._check_for_error(self._lib.Circuit_Get_NumNodes()) + return self._lib.Circuit_Get_NumNodes() @property def ParentPDElement(self) -> int: @@ -571,7 +545,7 @@ def ParentPDElement(self) -> int: Original COM help: https://opendss.epri.com/ParentPDElement.html ''' - return self._check_for_error(self._lib.Circuit_Get_ParentPDElement()) + return self._lib.Circuit_Get_ParentPDElement() @property def SubstationLosses(self) -> Float64ArrayOrSimpleComplex: @@ -580,8 +554,7 @@ def SubstationLosses(self) -> Float64ArrayOrSimpleComplex: Original COM help: https://opendss.epri.com/SubstationLosses.html ''' - self._check_for_error(self._lib.Circuit_Get_SubstationLosses_GR()) - return self._get_complex128_gr_simple() + return self._lib.Circuit_Get_SubstationLosses_GR() @property def SystemY(self) -> Float64ArrayOrComplexArray: @@ -592,8 +565,7 @@ def SystemY(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/SystemY.html ''' - self._check_for_error(self._lib.Circuit_Get_SystemY_GR()) - return self._get_complex128_gr_array() + return self._lib.Circuit_Get_SystemY_GR() @property def TotalPower(self) -> Float64ArrayOrSimpleComplex: @@ -602,8 +574,7 @@ def TotalPower(self) -> Float64ArrayOrSimpleComplex: Original COM help: https://opendss.epri.com/TotalPower.html ''' - self._check_for_error(self._lib.Circuit_Get_TotalPower_GR()) - return self._get_complex128_gr_simple() + return self._lib.Circuit_Get_TotalPower_GR() @property def YCurrents(self) -> Float64ArrayOrComplexArray: @@ -612,8 +583,7 @@ def YCurrents(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/YCurrents.html ''' - self._check_for_error(self._lib.Circuit_Get_YCurrents_GR()) - return self._get_complex128_gr_array() + return self._lib.Circuit_Get_YCurrents_GR() @property def YNodeOrder(self) -> List[str]: @@ -622,7 +592,7 @@ def YNodeOrder(self) -> List[str]: Original COM help: https://opendss.epri.com/YNodeOrder.html ''' - return self._check_for_error(self._get_string_array(self._lib.Circuit_Get_YNodeOrder)) + return self._lib.Circuit_Get_YNodeOrder() @property def YNodeVarray(self) -> Float64ArrayOrComplexArray: @@ -631,8 +601,7 @@ def YNodeVarray(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/YNodeVarray.html ''' - self._check_for_error(self._lib.Circuit_Get_YNodeVarray_GR()) - return self._get_complex128_gr_array() + return self._lib.Circuit_Get_YNodeVarray_GR() def ElementLosses(self, Value: Int32Array) -> Float64ArrayOrComplexArray: ''' @@ -642,8 +611,7 @@ def ElementLosses(self, Value: Int32Array) -> Float64ArrayOrComplexArray: **(API Extension)** ''' Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) - self._check_for_error(self._lib.Circuit_Get_ElementLosses_GR(ValuePtr, ValueCount)) - return self._get_complex128_gr_array() + return self._lib.Circuit_Get_ElementLosses_GR(ValuePtr, ValueCount) def ToJSON(self, options: DSSJSONFlags = 0) -> str: ''' @@ -657,7 +625,7 @@ def ToJSON(self, options: DSSJSONFlags = 0) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.Circuit_ToJSON(options))) + return self._lib.Circuit_ToJSON(options) def FromJSON(self, data: Union[AnyStr, dict], options: DSSJSONFlags = 0): ''' @@ -679,7 +647,6 @@ def FromJSON(self, data: Union[AnyStr, dict], options: DSSJSONFlags = 0): self._lib.Circuit_FromJSON(data, options) - self._check_for_error() def Save(self, dirOrFilePath: AnyStr, options: DSSSaveFlags) -> str: ''' @@ -703,9 +670,6 @@ def Save(self, dirOrFilePath: AnyStr, options: DSSSaveFlags) -> str: **(API Extension)** ''' - if not isinstance(dirOrFilePath, bytes): - dirOrFilePath = dirOrFilePath.encode() - - return self._check_for_error(self._get_string(self._lib.Circuit_Save(dirOrFilePath, options))) + return self._get_string(self._lib.Circuit_Save(dirOrFilePath, options)) diff --git a/dss/ICktElement.py b/dss/ICktElement.py index d38a5edc..98040081 100644 --- a/dss/ICktElement.py +++ b/dss/ICktElement.py @@ -68,11 +68,11 @@ def Close(self, Term: int, Phs: int): Original COM help: https://opendss.epri.com/Close1.html ''' - self._check_for_error(self._lib.CktElement_Close(Term, Phs)) + self._lib.CktElement_Close(Term, Phs) def Controller(self, idx: int) -> str: '''Full name of the i-th controller attached to this element. Ex: str = Controller(2). See NumControls to determine valid index range''' - return self._get_string(self._check_for_error(self._lib.CktElement_Get_Controller(idx))) + return self._lib.CktElement_Get_Controller(idx) def Variable(self, MyVarName: AnyStr) -> Tuple[float, int]: ''' @@ -84,7 +84,7 @@ def Variable(self, MyVarName: AnyStr) -> Tuple[float, int]: MyVarName = MyVarName.encode(self._api_util.codec) Code = self._api_util.ffi.new('int32_t*') - result = self._check_for_error(self._lib.CktElement_Get_Variable(MyVarName, Code)) + result = self._lib.CktElement_Get_Variable(MyVarName, Code) # if Code[0] == 1: # raise DssException('No variable by this name or not a PCelement') return result, Code[0] @@ -97,7 +97,7 @@ def Variablei(self, Idx: int) -> Tuple[float, int]: Original COM help: https://opendss.epri.com/Variablei.html ''' Code = self._api_util.ffi.new('int32_t*') - result = self._check_for_error(self._lib.CktElement_Get_Variablei(Idx, Code)) + result = self._lib.CktElement_Get_Variablei(Idx, Code) # if Code[0] == 1: # raise DssException('Invalid variable index or not a PCelement') return result, Code[0] @@ -108,20 +108,20 @@ def Variablei(self, Idx: int) -> Tuple[float, int]: def setVariableByIndex(self, Idx: int, Value: float) -> int: Code = self._api_util.ffi.new('int32_t*') - self._check_for_error(self._lib.CktElement_Set_Variablei(Idx, Code, Value)) + self._lib.CktElement_Set_Variablei(Idx, Code, Value) # if Code[0] == 1: # raise DSSException('Invalid variable index or not a PCelement') return Code[0] def setVariableByName(self, Idx: AnyStr, Value: float) -> int: Code = self._api_util.ffi.new('int32_t*') - self._check_for_error(self._lib.CktElement_Set_Variable(Idx, Code, Value)) + self._lib.CktElement_Set_Variable(Idx, Code, Value) # if Code[0] == 1: # raise DSSException('Invalid variable index or not a PCelement') return Code[0] def IsOpen(self, Term: int, Phs: int) -> bool: - return self._check_for_error(self._lib.CktElement_IsOpen(Term, Phs)) != 0 + return self._lib.CktElement_IsOpen(Term, Phs) def Open(self, Term: int, Phs: int): ''' @@ -129,7 +129,7 @@ def Open(self, Term: int, Phs: int): Original COM help: https://opendss.epri.com/Open1.html ''' - self._check_for_error(self._lib.CktElement_Open(Term, Phs)) + self._lib.CktElement_Open(Term, Phs) @property def AllPropertyNames(self) -> List[str]: @@ -138,7 +138,7 @@ def AllPropertyNames(self) -> List[str]: Original COM help: https://opendss.epri.com/AllPropertyNames.html ''' - return self._check_for_error(self._get_string_array(self._lib.CktElement_Get_AllPropertyNames)) + return self._lib.CktElement_Get_AllPropertyNames() @property def AllVariableNames(self) -> List[str]: @@ -148,7 +148,7 @@ def AllVariableNames(self) -> List[str]: Original COM help: https://opendss.epri.com/AllVariableNames.html ''' - return self._check_for_error(self._get_string_array(self._lib.CktElement_Get_AllVariableNames)) + return self._lib.CktElement_Get_AllVariableNames() @property def AllVariableValues(self) -> Float64Array: @@ -158,8 +158,7 @@ def AllVariableValues(self) -> Float64Array: Original COM help: https://opendss.epri.com/AllVariableValues.html ''' - self._check_for_error(self._lib.CktElement_Get_AllVariableValues_GR()) - return self._get_float64_gr_array() + return self._lib.CktElement_Get_AllVariableValues_GR() @property def BusNames(self) -> List[str]: @@ -168,11 +167,11 @@ def BusNames(self) -> List[str]: Original COM help: https://opendss.epri.com/BusNames.html ''' - return self._check_for_error(self._get_string_array(self._lib.CktElement_Get_BusNames)) + return self._lib.CktElement_Get_BusNames() @BusNames.setter def BusNames(self, Value: List[AnyStr]): - self._check_for_error(self._set_string_array(self._lib.CktElement_Set_BusNames, Value)) + self._set_string_array(self._lib.CktElement_Set_BusNames, Value) @property def CplxSeqCurrents(self) -> Float64ArrayOrComplexArray: @@ -181,8 +180,7 @@ def CplxSeqCurrents(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/CplxSeqCurrents.html ''' - self._check_for_error(self._lib.CktElement_Get_CplxSeqCurrents_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_CplxSeqCurrents_GR() @property def CplxSeqVoltages(self) -> Float64ArrayOrComplexArray: @@ -191,8 +189,7 @@ def CplxSeqVoltages(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/CplxSeqVoltages1.html ''' - self._check_for_error(self._lib.CktElement_Get_CplxSeqVoltages_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_CplxSeqVoltages_GR() @property def Currents(self) -> Float64ArrayOrComplexArray: @@ -201,8 +198,7 @@ def Currents(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Currents1.html ''' - self._check_for_error(self._lib.CktElement_Get_Currents_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_Currents_GR() @property def CurrentsMagAng(self) -> Float64Array: @@ -211,8 +207,7 @@ def CurrentsMagAng(self) -> Float64Array: Original COM help: https://opendss.epri.com/CurrentsMagAng.html ''' - self._check_for_error(self._lib.CktElement_Get_CurrentsMagAng_GR()) - return self._get_float64_gr_array() + return self._lib.CktElement_Get_CurrentsMagAng_GR() @property def DisplayName(self) -> str: @@ -221,14 +216,11 @@ def DisplayName(self) -> str: Original COM help: https://opendss.epri.com/DisplayName.html ''' - return self._get_string(self._check_for_error(self._lib.CktElement_Get_DisplayName())) + return self._lib.CktElement_Get_DisplayName() @DisplayName.setter def DisplayName(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.CktElement_Set_DisplayName(Value)) + self._lib.CktElement_Set_DisplayName(Value) @property def EmergAmps(self) -> float: @@ -237,11 +229,11 @@ def EmergAmps(self) -> float: Original COM help: https://opendss.epri.com/EmergAmps.html ''' - return self._check_for_error(self._lib.CktElement_Get_EmergAmps()) + return self._lib.CktElement_Get_EmergAmps() @EmergAmps.setter def EmergAmps(self, Value: float): - self._check_for_error(self._lib.CktElement_Set_EmergAmps(Value)) + self._lib.CktElement_Set_EmergAmps(Value) @property def Enabled(self) -> bool: @@ -250,11 +242,11 @@ def Enabled(self) -> bool: Original COM help: https://opendss.epri.com/Enabled.html ''' - return self._check_for_error(self._lib.CktElement_Get_Enabled()) != 0 + return self._lib.CktElement_Get_Enabled() @Enabled.setter def Enabled(self, Value: bool): - self._check_for_error(self._lib.CktElement_Set_Enabled(Value)) + self._lib.CktElement_Set_Enabled(Value) @property def EnergyMeter(self) -> str: @@ -265,7 +257,7 @@ def EnergyMeter(self) -> str: Original COM help: https://opendss.epri.com/EnergyMeter.html ''' - return self._get_string(self._check_for_error(self._lib.CktElement_Get_EnergyMeter())) + return self._lib.CktElement_Get_EnergyMeter() @property def GUID(self) -> str: @@ -274,7 +266,7 @@ def GUID(self) -> str: Original COM help: https://opendss.epri.com/GUID.html ''' - return self._get_string(self._check_for_error(self._lib.CktElement_Get_GUID())) + return self._lib.CktElement_Get_GUID() @property def Handle(self) -> int: @@ -283,7 +275,7 @@ def Handle(self) -> int: Original COM help: https://opendss.epri.com/Handle.html ''' - return self._check_for_error(self._lib.CktElement_Get_Handle()) + return self._lib.CktElement_Get_Handle() @property def HasOCPDevice(self) -> bool: @@ -292,7 +284,7 @@ def HasOCPDevice(self) -> bool: Original COM help: https://opendss.epri.com/HasOCPDevice.html ''' - return self._check_for_error(self._lib.CktElement_Get_HasOCPDevice()) != 0 + return self._lib.CktElement_Get_HasOCPDevice() @property def HasSwitchControl(self) -> bool: @@ -301,7 +293,7 @@ def HasSwitchControl(self) -> bool: Original COM help: https://opendss.epri.com/HasSwitchControl.html ''' - return self._check_for_error(self._lib.CktElement_Get_HasSwitchControl()) != 0 + return self._lib.CktElement_Get_HasSwitchControl() @property def HasVoltControl(self) -> bool: @@ -310,7 +302,7 @@ def HasVoltControl(self) -> bool: Original COM help: https://opendss.epri.com/HasVoltControl.html ''' - return self._check_for_error(self._lib.CktElement_Get_HasVoltControl()) != 0 + return self._lib.CktElement_Get_HasVoltControl() @property def Losses(self) -> Float64ArrayOrSimpleComplex: @@ -319,8 +311,7 @@ def Losses(self) -> Float64ArrayOrSimpleComplex: Original COM help: https://opendss.epri.com/Losses1.html ''' - self._check_for_error(self._lib.CktElement_Get_Losses_GR()) - return self._get_complex128_gr_simple() + return self._lib.CktElement_Get_Losses_GR() @property def Name(self) -> str: @@ -329,7 +320,7 @@ def Name(self) -> str: Original COM help: https://opendss.epri.com/Name4.html ''' - return self._get_string(self._check_for_error(self._lib.CktElement_Get_Name())) + return self._lib.CktElement_Get_Name() @property def NodeOrder(self) -> Int32Array: @@ -340,8 +331,7 @@ def NodeOrder(self) -> Int32Array: Original COM help: https://opendss.epri.com/NodeOrder.html ''' - self._check_for_error(self._lib.CktElement_Get_NodeOrder_GR()) - return self._get_int32_gr_array() + return self._lib.CktElement_Get_NodeOrder_GR() @property def NormalAmps(self) -> float: @@ -350,11 +340,11 @@ def NormalAmps(self) -> float: Original COM help: https://opendss.epri.com/NormalAmps.html ''' - return self._check_for_error(self._lib.CktElement_Get_NormalAmps()) + return self._lib.CktElement_Get_NormalAmps() @NormalAmps.setter def NormalAmps(self, Value: float): - self._check_for_error(self._lib.CktElement_Set_NormalAmps(Value)) + self._lib.CktElement_Set_NormalAmps(Value) @property def NumConductors(self) -> int: @@ -363,7 +353,7 @@ def NumConductors(self) -> int: Original COM help: https://opendss.epri.com/NumConductors.html ''' - return self._check_for_error(self._lib.CktElement_Get_NumConductors()) + return self._lib.CktElement_Get_NumConductors() @property def NumControls(self) -> int: @@ -373,7 +363,7 @@ def NumControls(self) -> int: Original COM help: https://opendss.epri.com/NumControls.html ''' - return self._check_for_error(self._lib.CktElement_Get_NumControls()) + return self._lib.CktElement_Get_NumControls() @property def NumPhases(self) -> int: @@ -382,7 +372,7 @@ def NumPhases(self) -> int: Original COM help: https://opendss.epri.com/NumPhases.html ''' - return self._check_for_error(self._lib.CktElement_Get_NumPhases()) + return self._lib.CktElement_Get_NumPhases() @property def NumProperties(self) -> int: @@ -391,7 +381,7 @@ def NumProperties(self) -> int: Original COM help: https://opendss.epri.com/NumProperties.html ''' - return self._check_for_error(self._lib.CktElement_Get_NumProperties()) + return self._lib.CktElement_Get_NumProperties() @property def NumTerminals(self) -> int: @@ -400,7 +390,7 @@ def NumTerminals(self) -> int: Original COM help: https://opendss.epri.com/NumTerminals.html ''' - return self._check_for_error(self._lib.CktElement_Get_NumTerminals()) + return self._lib.CktElement_Get_NumTerminals() @property def OCPDevIndex(self) -> int: @@ -409,7 +399,7 @@ def OCPDevIndex(self) -> int: Original COM help: https://opendss.epri.com/OCPDevIndex.html ''' - return self._check_for_error(self._lib.CktElement_Get_OCPDevIndex()) + return self._lib.CktElement_Get_OCPDevIndex() @property def OCPDevType(self) -> OCPDevTypeEnum: @@ -418,7 +408,7 @@ def OCPDevType(self) -> OCPDevTypeEnum: Original COM help: https://opendss.epri.com/OCPDevType.html ''' - return OCPDevTypeEnum(self._check_for_error(self._lib.CktElement_Get_OCPDevType())) + return OCPDevTypeEnum(self._lib.CktElement_Get_OCPDevType()) @property def PhaseLosses(self) -> Float64ArrayOrComplexArray: @@ -427,8 +417,7 @@ def PhaseLosses(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/PhaseLosses.html ''' - self._check_for_error(self._lib.CktElement_Get_PhaseLosses_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_PhaseLosses_GR() @property def Powers(self) -> Float64ArrayOrComplexArray: @@ -437,8 +426,7 @@ def Powers(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Powers.html ''' - self._check_for_error(self._lib.CktElement_Get_Powers_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_Powers_GR() @property def Residuals(self) -> Float64Array: @@ -447,8 +435,7 @@ def Residuals(self) -> Float64Array: Original COM help: https://opendss.epri.com/Residuals.html ''' - self._check_for_error(self._lib.CktElement_Get_Residuals_GR()) - return self._get_float64_gr_array() + return self._lib.CktElement_Get_Residuals_GR() @property def SeqCurrents(self) -> Float64Array: @@ -457,8 +444,7 @@ def SeqCurrents(self) -> Float64Array: Original COM help: https://opendss.epri.com/SeqCurrents.html ''' - self._check_for_error(self._lib.CktElement_Get_SeqCurrents_GR()) - return self._get_float64_gr_array() + return self._lib.CktElement_Get_SeqCurrents_GR() @property def SeqPowers(self) -> Float64ArrayOrComplexArray: @@ -467,8 +453,7 @@ def SeqPowers(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/SeqPowers.html ''' - self._check_for_error(self._lib.CktElement_Get_SeqPowers_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_SeqPowers_GR() @property def SeqVoltages(self) -> Float64Array: @@ -477,8 +462,7 @@ def SeqVoltages(self) -> Float64Array: Original COM help: https://opendss.epri.com/SeqVoltages1.html ''' - self._check_for_error(self._lib.CktElement_Get_SeqVoltages_GR()) - return self._get_float64_gr_array() + return self._lib.CktElement_Get_SeqVoltages_GR() @property def Voltages(self) -> Float64ArrayOrComplexArray: @@ -487,8 +471,7 @@ def Voltages(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Voltages1.html ''' - self._check_for_error(self._lib.CktElement_Get_Voltages_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_Voltages_GR() @property def VoltagesMagAng(self) -> Float64Array: @@ -497,8 +480,7 @@ def VoltagesMagAng(self) -> Float64Array: Original COM help: https://opendss.epri.com/VoltagesMagAng.html ''' - self._check_for_error(self._lib.CktElement_Get_VoltagesMagAng_GR()) - return self._get_float64_gr_array() + return self._lib.CktElement_Get_VoltagesMagAng_GR() @property def Yprim(self) -> Float64ArrayOrComplexArray: @@ -507,8 +489,7 @@ def Yprim(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Yprim.html ''' - self._check_for_error(self._lib.CktElement_Get_Yprim_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_Yprim_GR() @property def IsIsolated(self) -> bool: @@ -518,7 +499,7 @@ def IsIsolated(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.CktElement_Get_IsIsolated()) != 0 + return self._lib.CktElement_Get_IsIsolated() @property def TotalPowers(self) -> Float64ArrayOrComplexArray: @@ -527,8 +508,7 @@ def TotalPowers(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/TotalPowers.html ''' - self._check_for_error(self._lib.CktElement_Get_TotalPowers_GR()) - return self._get_complex128_gr_array() + return self._lib.CktElement_Get_TotalPowers_GR() @property def NodeRef(self) -> Int32Array: @@ -539,23 +519,19 @@ def NodeRef(self) -> Int32Array: **(API Extension)** ''' - self._lib.CktElement_Get_NodeRef_GR() - return self._get_int32_gr_array() + return self._lib.CktElement_Get_NodeRef_GR() def __iter__(self) -> Iterator[ICktElement]: - for index in range(self._check_for_error(self._lib.Circuit_Get_NumCktElements())): - self._check_for_error(self._lib.Circuit_SetCktElementIndex(index)) + for index in range(self._lib.Circuit_Get_NumCktElements()): + self._lib.Circuit_SetCktElementIndex(index) yield self def __getitem__(self, index) -> ICktElement: if isinstance(index, int): # index is zero based, pass it directly - self._check_for_error(self._lib.Circuit_SetCktElementIndex(index)) + self._lib.Circuit_SetCktElementIndex(index) else: - if not isinstance(index, bytes): - index = index.encode(self._api_util.codec) - - self._check_for_error(self._lib.Circuit_SetCktElementName(index)) + self._lib.Circuit_SetCktElementName(index) return self diff --git a/dss/ICtrlQueue.py b/dss/ICtrlQueue.py index 583aa5ad..4f77df5f 100644 --- a/dss/ICtrlQueue.py +++ b/dss/ICtrlQueue.py @@ -21,7 +21,7 @@ def ClearActions(self): Original COM help: https://opendss.epri.com/ClearActions.html ''' - self._check_for_error(self._lib.CtrlQueue_ClearActions()) + self._lib.CtrlQueue_ClearActions() def ClearQueue(self): ''' @@ -29,7 +29,7 @@ def ClearQueue(self): Original COM help: https://opendss.epri.com/ClearQueue.html ''' - self._check_for_error(self._lib.CtrlQueue_ClearQueue()) + self._lib.CtrlQueue_ClearQueue() def Delete(self, ActionHandle): ''' @@ -39,7 +39,7 @@ def Delete(self, ActionHandle): Original COM help: https://opendss.epri.com/Delete.html ''' - self._check_for_error(self._lib.CtrlQueue_Delete(ActionHandle)) + self._lib.CtrlQueue_Delete(ActionHandle) def DoAllQueue(self): ''' @@ -49,7 +49,7 @@ def DoAllQueue(self): Original COM help: https://opendss.epri.com/DoAllQueue.html ''' - self._check_for_error(self._lib.CtrlQueue_DoAllQueue()) + self._lib.CtrlQueue_DoAllQueue() def Show(self): ''' @@ -57,7 +57,7 @@ def Show(self): Original COM help: https://opendss.epri.com/Show.html ''' - self._check_for_error(self._lib.CtrlQueue_Show()) + self._lib.CtrlQueue_Show() @property def ActionCode(self) -> int: @@ -69,7 +69,7 @@ def ActionCode(self) -> int: Original COM help: https://opendss.epri.com/ActionCode.html ''' - return self._check_for_error(self._lib.CtrlQueue_Get_ActionCode()) + return self._lib.CtrlQueue_Get_ActionCode() @property def DeviceHandle(self) -> int: @@ -83,7 +83,7 @@ def DeviceHandle(self) -> int: Original COM help: https://opendss.epri.com/DeviceHandle.html ''' - return self._check_for_error(self._lib.CtrlQueue_Get_DeviceHandle()) + return self._lib.CtrlQueue_Get_DeviceHandle() @property def NumActions(self) -> int: @@ -92,7 +92,7 @@ def NumActions(self) -> int: Original COM help: https://opendss.epri.com/NumActions.html ''' - return self._check_for_error(self._lib.CtrlQueue_Get_NumActions()) + return self._lib.CtrlQueue_Get_NumActions() def Push(self, Hour: int, Seconds: float, ActionCode: int, DeviceHandle: int): ''' @@ -100,7 +100,7 @@ def Push(self, Hour: int, Seconds: float, ActionCode: int, DeviceHandle: int): Original COM help: https://opendss.epri.com/Push.html ''' - return self._check_for_error(self._lib.CtrlQueue_Push(Hour, Seconds, ActionCode, DeviceHandle)) + return self._lib.CtrlQueue_Push(Hour, Seconds, ActionCode, DeviceHandle) @property def PopAction(self) -> int: @@ -109,7 +109,7 @@ def PopAction(self) -> int: Original COM help: https://opendss.epri.com/PopAction.html ''' - return self._check_for_error(self._lib.CtrlQueue_Get_PopAction()) + return self._lib.CtrlQueue_Get_PopAction() @property def Queue(self) -> List[str]: @@ -118,7 +118,7 @@ def Queue(self) -> List[str]: Original COM help: https://opendss.epri.com/Queue.html ''' - return self._check_for_error(self._get_string_array(self._lib.CtrlQueue_Get_Queue)) + return self._lib.CtrlQueue_Get_Queue() @property def QueueSize(self) -> int: @@ -127,7 +127,7 @@ def QueueSize(self) -> int: Original COM help: https://opendss.epri.com/QueueSize.html ''' - return self._check_for_error(self._lib.CtrlQueue_Get_QueueSize()) + return self._lib.CtrlQueue_Get_QueueSize() @property def Action(self) -> int: @@ -140,5 +140,5 @@ def Action(self) -> int: @Action.setter def Action(self, Param1: int): - self._check_for_error(self._lib.CtrlQueue_Set_Action(Param1)) + self._lib.CtrlQueue_Set_Action(Param1) diff --git a/dss/IDSS.py b/dss/IDSS.py index 201a4826..6cc944c1 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -22,12 +22,12 @@ try: from altdss import AltDSS except: - AltDSS = None + pass try: from opendssdirect.OpenDSSDirect import OpenDSSDirect except: - OpenDSSDirect = None + pass class IDSS(Base): ''' @@ -106,6 +106,9 @@ def __init__(self, api_util): if api_util.ctx not in IDSS._ctx_to_dss: IDSS._ctx_to_dss[api_util.ctx] = self + if api_util._dss_python is None: + api_util._dss_python = self + self._version = None #: Provides access to the circuit attributes and objects in general. @@ -190,7 +193,7 @@ def to_opendssdirect(self) -> OpenDSSDirect: return OpenDSSDirect._get_instance(ctx=self._api_util.ctx, api_util=self._api_util) def ClearAll(self): - self._check_for_error(self._lib.DSS_ClearAll()) + self._lib.DSS_ClearAll() def Reset(self): ''' @@ -198,13 +201,10 @@ def Reset(self): Original COM help: https://opendss.epri.com/Reset1.html ''' - self._check_for_error(self._lib.DSS_Reset()) + self._lib.DSS_Reset() def SetActiveClass(self, ClassName: AnyStr) -> int: - if not isinstance(ClassName, bytes): - ClassName = ClassName.encode(self._api_util.codec) - - return self._check_for_error(self._lib.DSS_SetActiveClass(ClassName)) + return self._lib.DSS_SetActiveClass(ClassName) def Start(self, code: int) -> bool: ''' @@ -219,7 +219,7 @@ def Start(self, code: int) -> bool: Original COM help: https://opendss.epri.com/Start.html ''' - return self._check_for_error(self._lib.DSS_Start(code)) != 0 + return self._lib.DSS_Start(code) != 0 @property def Classes(self) -> List[str]: @@ -228,7 +228,7 @@ def Classes(self) -> List[str]: Original COM help: https://opendss.epri.com/Classes1.html ''' - return self._check_for_error(self._get_string_array(self._lib.DSS_Get_Classes)) + return self._lib.DSS_Get_Classes() @property def DataPath(self) -> str: @@ -237,14 +237,11 @@ def DataPath(self) -> str: Original COM help: https://opendss.epri.com/DataPath.html ''' - return self._get_string(self._check_for_error(self._lib.DSS_Get_DataPath())) + return self._lib.DSS_Get_DataPath() @DataPath.setter def DataPath(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.DSS_Set_DataPath(Value)) + self._lib.DSS_Set_DataPath(Value) @property def DefaultEditor(self) -> str: @@ -253,7 +250,7 @@ def DefaultEditor(self) -> str: Original COM help: https://opendss.epri.com/DefaultEditor.html ''' - return self._get_string(self._check_for_error(self._lib.DSS_Get_DefaultEditor())) + return self._lib.DSS_Get_DefaultEditor() @property def NumCircuits(self) -> int: @@ -262,7 +259,7 @@ def NumCircuits(self) -> int: Original COM help: https://opendss.epri.com/NumCircuits.html ''' - return self._check_for_error(self._lib.DSS_Get_NumCircuits()) + return self._lib.DSS_Get_NumCircuits() @property def NumClasses(self) -> int: @@ -271,7 +268,7 @@ def NumClasses(self) -> int: Original COM help: https://opendss.epri.com/NumClasses.html ''' - return self._check_for_error(self._lib.DSS_Get_NumClasses()) + return self._lib.DSS_Get_NumClasses() @property def NumUserClasses(self) -> int: @@ -280,7 +277,7 @@ def NumUserClasses(self) -> int: Original COM help: https://opendss.epri.com/NumUserClasses.html ''' - return self._check_for_error(self._lib.DSS_Get_NumUserClasses()) + return self._lib.DSS_Get_NumUserClasses() @property def UserClasses(self) -> List[str]: @@ -289,7 +286,7 @@ def UserClasses(self) -> List[str]: Original COM help: https://opendss.epri.com/UserClasses.html ''' - return self._check_for_error(self._get_string_array(self._lib.DSS_Get_UserClasses)) + return self._lib.DSS_Get_UserClasses() @property def Version(self) -> str: @@ -303,7 +300,7 @@ def Version(self) -> str: from . import __version__ as dss_python_version self._version = dss_python_version - return self._get_string(self._check_for_error(self._lib.DSS_Get_Version())) + f'\nDSS-Python version: {self._version}' + return self._lib.DSS_Get_Version() + f'\nDSS-Python version: {self._version}' @property def AllowForms(self) -> bool: @@ -312,11 +309,11 @@ def AllowForms(self) -> bool: Original COM help: https://opendss.epri.com/AllowForms.html ''' - return self._check_for_error(self._lib.DSS_Get_AllowForms()) != 0 + return self._lib.DSS_Get_AllowForms() @AllowForms.setter def AllowForms(self, value: bool): - self._check_for_error(self._lib.DSS_Set_AllowForms(value)) + self._lib.DSS_Set_AllowForms(value) @property def AllowEditor(self) -> bool: @@ -329,11 +326,11 @@ def AllowEditor(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.DSS_Get_AllowEditor()) != 0 + return self._lib.DSS_Get_AllowEditor() @AllowEditor.setter def AllowEditor(self, value: bool): - self._check_for_error(self._lib.DSS_Set_AllowEditor(value)) + self._lib.DSS_Set_AllowEditor(value) def ShowPanel(self): pass @@ -344,10 +341,7 @@ def NewCircuit(self, name) -> ICircuit: Original COM help: https://opendss.epri.com/NewCircuit.html ''' - if not isinstance(name, bytes): - name = name.encode(self._api_util.codec) - - self._check_for_error(self._lib.DSS_NewCircuit(name)) + self._lib.DSS_NewCircuit(name) return self.ActiveCircuit @@ -363,11 +357,11 @@ def LegacyModels(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.DSS_Get_LegacyModels()) != 0 + return self._lib.DSS_Get_LegacyModels() @LegacyModels.setter def LegacyModels(self, Value: bool): - self._check_for_error(self._lib.DSS_Set_LegacyModels(Value)) + self._lib.DSS_Set_LegacyModels(Value) @property def AllowChangeDir(self) -> bool: @@ -385,11 +379,11 @@ def AllowChangeDir(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.DSS_Get_AllowChangeDir()) != 0 + return self._lib.DSS_Get_AllowChangeDir() @AllowChangeDir.setter def AllowChangeDir(self, Value: bool): - self._check_for_error(self._lib.DSS_Set_AllowChangeDir(Value)) + self._lib.DSS_Set_AllowChangeDir(Value) @property def AllowDOScmd(self) -> bool: @@ -403,11 +397,11 @@ def AllowDOScmd(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.DSS_Get_AllowDOScmd()) != 0 + return self._lib.DSS_Get_AllowDOScmd() @AllowDOScmd.setter def AllowDOScmd(self, Value: bool): - self._check_for_error(self._lib.DSS_Set_AllowDOScmd(Value)) + self._lib.DSS_Set_AllowDOScmd(Value) @property def COMErrorResults(self) -> bool: @@ -428,11 +422,11 @@ def COMErrorResults(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.DSS_Get_COMErrorResults()) != 0 + return self._lib.DSS_Get_COMErrorResults() @COMErrorResults.setter def COMErrorResults(self, Value: bool): - self._check_for_error(self._lib.DSS_Set_COMErrorResults(Value)) + self._lib.DSS_Set_COMErrorResults(Value) def NewContext(self) -> IDSS: ''' @@ -518,13 +512,19 @@ def AdvancedTypes(self) -> bool: **(API Extension)** ''' - arr_dim = self._check_for_error(self._lib.DSS_Get_EnableArrayDimensions()) != 0 + arr_dim = self._lib.DSS_Get_EnableArrayDimensions() allow_complex = self._api_util._allow_complex return arr_dim and allow_complex @AdvancedTypes.setter def AdvancedTypes(self, Value: bool): - self._check_for_error(self._lib.DSS_Set_EnableArrayDimensions(Value)) + self._lib.DSS_Set_EnableArrayDimensions(Value) + _AdvancedTypes = 2 + if Value: + self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] | _AdvancedTypes + else: + self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] & ~_AdvancedTypes + self._api_util._allow_complex = bool(Value) @property @@ -546,8 +546,8 @@ def CompatFlags(self) -> int: **(API Extension)** ''' - return self._check_for_error(self._lib.DSS_Get_CompatFlags()) + return self._lib.DSS_Get_CompatFlags() @CompatFlags.setter def CompatFlags(self, Value: int): - self._check_for_error(self._lib.DSS_Set_CompatFlags(Value)) + self._lib.DSS_Set_CompatFlags(Value) diff --git a/dss/IDSSElement.py b/dss/IDSSElement.py index a123f7db..4035c960 100644 --- a/dss/IDSSElement.py +++ b/dss/IDSSElement.py @@ -1,9 +1,10 @@ # A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. # Copyright (c) 2016-2024 Paulo Meira # Copyright (c) 2018-2024 DSS-Extensions contributors +from __future__ import annotations from ._cffi_api_util import Base from .IDSSProperty import IDSSProperty -from typing import List +from typing import List, Optional from .enums import DSSJSONFlags class IDSSElement(Base): @@ -28,7 +29,7 @@ def AllPropertyNames(self) -> List[str]: Original COM help: https://opendss.epri.com/AllPropertyNames1.html ''' - return self._check_for_error(self._get_string_array(self._lib.DSSElement_Get_AllPropertyNames)) + return self._lib.DSSElement_Get_AllPropertyNames() @property def Name(self) -> str: @@ -37,7 +38,7 @@ def Name(self) -> str: Original COM help: https://opendss.epri.com/Name5.html ''' - return self._get_string(self._check_for_error(self._lib.DSSElement_Get_Name())) + return self._lib.DSSElement_Get_Name() @property def NumProperties(self) -> int: @@ -46,7 +47,7 @@ def NumProperties(self) -> int: Original COM help: https://opendss.epri.com/NumProperties1.html ''' - return self._check_for_error(self._lib.DSSElement_Get_NumProperties()) + return self._lib.DSSElement_Get_NumProperties() def ToJSON(self, options: DSSJSONFlags = 0) -> str: ''' @@ -57,4 +58,18 @@ def ToJSON(self, options: DSSJSONFlags = 0) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.DSSElement_ToJSON(options))) + return self._lib.DSSElement_ToJSON(options) + + + def to_altdss(self) -> Optional[DSSObject]: + ''' + Returns a Python object for the current active DSS object in this interface. + + Requires AltDSS-Python. + + *Available only for the AltDSS engine.* + + **(API Extension)** + ''' + ptr = self._api_util._lib.DSSElement_Get_Pointer() + return self._api_util.get_dss_obj(ptr) diff --git a/dss/IDSSProgress.py b/dss/IDSSProgress.py index 8a102b50..572cd73a 100644 --- a/dss/IDSSProgress.py +++ b/dss/IDSSProgress.py @@ -8,10 +8,10 @@ class IDSSProgress(Base): __slots__ = [] def Close(self): - self._check_for_error(self._lib.DSSProgress_Close()) + self._lib.DSSProgress_Close() def Show(self): - self._check_for_error(self._lib.DSSProgress_Show()) + self._lib.DSSProgress_Show() @property def Caption(self) -> str: @@ -24,10 +24,7 @@ def Caption(self) -> str: @Caption.setter def Caption(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.DSSProgress_Set_Caption(Value)) + self._lib.DSSProgress_Set_Caption(Value) @property def PctProgress(self) -> int: @@ -40,6 +37,6 @@ def PctProgress(self) -> int: @PctProgress.setter def PctProgress(self, Value: int): - self._check_for_error(self._lib.DSSProgress_Set_PctProgress(Value)) + self._lib.DSSProgress_Set_PctProgress(Value) diff --git a/dss/IDSSProperty.py b/dss/IDSSProperty.py index cecda5ed..10bc0fe6 100644 --- a/dss/IDSSProperty.py +++ b/dss/IDSSProperty.py @@ -21,7 +21,7 @@ def Description(self) -> str: Original COM help: https://opendss.epri.com/Description.html ''' - return self._get_string(self._check_for_error(self._lib.DSSProperty_Get_Description())) + return self._lib.DSSProperty_Get_Description() @property def Name(self) -> str: @@ -30,7 +30,7 @@ def Name(self) -> str: Original COM help: https://opendss.epri.com/Name6.html ''' - return self._get_string(self._check_for_error(self._lib.DSSProperty_Get_Name())) + return self._lib.DSSProperty_Get_Name() @property def Val(self) -> str: @@ -39,23 +39,17 @@ def Val(self) -> str: Original COM help: https://opendss.epri.com/Val.html ''' - return self._get_string(self._check_for_error(self._lib.DSSProperty_Get_Val())) + return self._lib.DSSProperty_Get_Val() @Val.setter def Val(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = str(Value).encode(self._api_util.codec) - - self._check_for_error(self._lib.DSSProperty_Set_Val(Value)) + self._lib.DSSProperty_Set_Val(Value) def __getitem__(self, propname_index: Union[AnyStr, int]) -> IDSSProperty: if isinstance(propname_index, int): - self._check_for_error(self._lib.DSSProperty_Set_Index(propname_index)) + self._lib.DSSProperty_Set_Index(propname_index) else: - if not isinstance(propname_index, bytes): - propname_index = propname_index.encode(self._api_util.codec) - - self._check_for_error(self._lib.DSSProperty_Set_Name(propname_index)) + self._lib.DSSProperty_Set_Name(propname_index) return self diff --git a/dss/IDSS_Executive.py b/dss/IDSS_Executive.py index 882fa79f..7b097a43 100644 --- a/dss/IDSS_Executive.py +++ b/dss/IDSS_Executive.py @@ -7,7 +7,7 @@ class IDSS_Executive(Base): __slots__ = [] _columns = [ - 'NumCommands', + 'NumCommands', 'NumOptions', ] @@ -17,7 +17,7 @@ def Command(self, i: int) -> str: Original COM help: https://opendss.epri.com/Command.html ''' - return self._get_string(self._check_for_error(self._lib.DSS_Executive_Get_Command(i))) + return self._lib.DSS_Executive_Get_Command(i) def CommandHelp(self, i: int) -> str: ''' @@ -25,7 +25,7 @@ def CommandHelp(self, i: int) -> str: Original COM help: https://opendss.epri.com/CommandHelp.html ''' - return self._get_string(self._check_for_error(self._lib.DSS_Executive_Get_CommandHelp(i))) + return self._lib.DSS_Executive_Get_CommandHelp(i) def Option(self, i: int) -> str: ''' @@ -33,7 +33,7 @@ def Option(self, i: int) -> str: Original COM help: https://opendss.epri.com/Option.html ''' - return self._get_string(self._check_for_error(self._lib.DSS_Executive_Get_Option(i))) + return self._lib.DSS_Executive_Get_Option(i) def OptionHelp(self, i: int) -> str: ''' @@ -41,7 +41,7 @@ def OptionHelp(self, i: int) -> str: Original COM help: https://opendss.epri.com/OptionHelp.html ''' - return self._get_string(self._check_for_error(self._lib.DSS_Executive_Get_OptionHelp(i))) + return self._lib.DSS_Executive_Get_OptionHelp(i) def OptionValue(self, i: int) -> str: ''' @@ -49,7 +49,7 @@ def OptionValue(self, i: int) -> str: Original COM help: https://opendss.epri.com/OptionValue.html ''' - return self._get_string(self._check_for_error(self._lib.DSS_Executive_Get_OptionValue(i))) + return self._lib.DSS_Executive_Get_OptionValue(i) @property def NumCommands(self) -> int: @@ -58,7 +58,7 @@ def NumCommands(self) -> int: Original COM help: https://opendss.epri.com/NumCommands.html ''' - return self._check_for_error(self._lib.DSS_Executive_Get_NumCommands()) + return self._lib.DSS_Executive_Get_NumCommands() @property def NumOptions(self) -> int: @@ -67,5 +67,5 @@ def NumOptions(self) -> int: Original COM help: https://opendss.epri.com/NumOptions.html ''' - return self._check_for_error(self._lib.DSS_Executive_Get_NumOptions()) + return self._lib.DSS_Executive_Get_NumOptions() diff --git a/dss/IDSSimComs.py b/dss/IDSSimComs.py index abcf2533..febf9c9c 100644 --- a/dss/IDSSimComs.py +++ b/dss/IDSSimComs.py @@ -8,11 +8,9 @@ class IDSSimComs(Base): __slots__ = [] def BusVoltage(self, Index: int) -> Float64Array: - self._check_for_error(self._lib.DSSimComs_BusVoltage_GR(Index)) - return self._get_float64_gr_array() + return self._lib.DSSimComs_BusVoltage_GR(Index) def BusVoltagepu(self, Index: int) -> Float64Array: - self._check_for_error(self._lib.DSSimComs_BusVoltagepu_GR(Index)) - return self._get_float64_gr_array() + return self._lib.DSSimComs_BusVoltagepu_GR(Index) diff --git a/dss/IError.py b/dss/IError.py index 97b1b227..50117dd8 100644 --- a/dss/IError.py +++ b/dss/IError.py @@ -19,7 +19,7 @@ def Description(self) -> str: Original COM help: https://opendss.epri.com/Description1.html ''' - return self._get_string(self._lib.Error_Get_Description()) + return self._lib.Error_Get_Description() @property def Number(self) -> int: @@ -37,7 +37,7 @@ def EarlyAbort(self) -> bool: **(API Extension)** ''' - return self._lib.Error_Get_EarlyAbort() != 0 + return self._lib.Error_Get_EarlyAbort() @EarlyAbort.setter def EarlyAbort(self, Value: bool): @@ -66,7 +66,7 @@ def ExtendedErrors(self) -> bool: **(API Extension)** ''' - return self._lib.Error_Get_ExtendedErrors() != 0 + return self._lib.Error_Get_ExtendedErrors() @ExtendedErrors.setter def ExtendedErrors(self, Value: bool): @@ -97,3 +97,8 @@ def UseExceptions(self) -> bool: @UseExceptions.setter def UseExceptions(self, value: bool): Base._enable_exceptions(value) + _UseExceptions = 1 + if value: + self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] | _UseExceptions + else: + self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] & ~_UseExceptions diff --git a/dss/IFuses.py b/dss/IFuses.py index f2f3a26e..87d2e2f6 100644 --- a/dss/IFuses.py +++ b/dss/IFuses.py @@ -31,7 +31,7 @@ def Close(self): Original COM help: https://opendss.epri.com/Close3.html ''' - self._check_for_error(self._lib.Fuses_Close()) + self._lib.Fuses_Close() def IsBlown(self) -> bool: ''' @@ -39,7 +39,7 @@ def IsBlown(self) -> bool: Original COM help: https://opendss.epri.com/IsBlown.html ''' - return self._check_for_error(self._lib.Fuses_IsBlown()) != 0 + return self._lib.Fuses_IsBlown() def Open(self): ''' @@ -47,7 +47,7 @@ def Open(self): Original COM help: https://opendss.epri.com/Open2.html ''' - self._check_for_error(self._lib.Fuses_Open()) + self._lib.Fuses_Open() def Reset(self): ''' @@ -55,7 +55,7 @@ def Reset(self): Original COM help: https://opendss.epri.com/Reset7.html ''' - self._check_for_error(self._lib.Fuses_Reset()) + self._lib.Fuses_Reset() @property def Delay(self) -> float: @@ -65,11 +65,11 @@ def Delay(self) -> float: Original COM help: https://opendss.epri.com/Delay1.html ''' - return self._check_for_error(self._lib.Fuses_Get_Delay()) + return self._lib.Fuses_Get_Delay() @Delay.setter def Delay(self, Value: float): - self._check_for_error(self._lib.Fuses_Set_Delay(Value)) + self._lib.Fuses_Set_Delay(Value) @property def MonitoredObj(self) -> str: @@ -78,14 +78,11 @@ def MonitoredObj(self) -> str: Original COM help: https://opendss.epri.com/MonitoredObj1.html ''' - return self._get_string(self._check_for_error(self._lib.Fuses_Get_MonitoredObj())) + return self._lib.Fuses_Get_MonitoredObj() @MonitoredObj.setter def MonitoredObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Fuses_Set_MonitoredObj(Value)) + self._lib.Fuses_Set_MonitoredObj(Value) @property def MonitoredTerm(self) -> int: @@ -94,11 +91,11 @@ def MonitoredTerm(self) -> int: Original COM help: https://opendss.epri.com/MonitoredTerm1.html ''' - return self._check_for_error(self._lib.Fuses_Get_MonitoredTerm()) + return self._lib.Fuses_Get_MonitoredTerm() @MonitoredTerm.setter def MonitoredTerm(self, Value: int): - self._check_for_error(self._lib.Fuses_Set_MonitoredTerm(Value)) + self._lib.Fuses_Set_MonitoredTerm(Value) @property def NumPhases(self) -> int: @@ -107,7 +104,7 @@ def NumPhases(self) -> int: Original COM help: https://opendss.epri.com/NumPhases1.html ''' - return self._check_for_error(self._lib.Fuses_Get_NumPhases()) + return self._lib.Fuses_Get_NumPhases() @property def RatedCurrent(self) -> float: @@ -118,11 +115,11 @@ def RatedCurrent(self) -> float: Original COM help: https://opendss.epri.com/RatedCurrent.html ''' - return self._check_for_error(self._lib.Fuses_Get_RatedCurrent()) + return self._lib.Fuses_Get_RatedCurrent() @RatedCurrent.setter def RatedCurrent(self, Value: float): - self._check_for_error(self._lib.Fuses_Set_RatedCurrent(Value)) + self._lib.Fuses_Set_RatedCurrent(Value) @property def SwitchedObj(self) -> str: @@ -132,14 +129,11 @@ def SwitchedObj(self) -> str: Original COM help: https://opendss.epri.com/SwitchedObj.html ''' - return self._get_string(self._check_for_error(self._lib.Fuses_Get_SwitchedObj())) + return self._lib.Fuses_Get_SwitchedObj() @SwitchedObj.setter def SwitchedObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Fuses_Set_SwitchedObj(Value)) + self._lib.Fuses_Set_SwitchedObj(Value) @property def SwitchedTerm(self) -> int: @@ -148,11 +142,11 @@ def SwitchedTerm(self) -> int: Original COM help: https://opendss.epri.com/SwitchedTerm.html ''' - return self._check_for_error(self._lib.Fuses_Get_SwitchedTerm()) + return self._lib.Fuses_Get_SwitchedTerm() @SwitchedTerm.setter def SwitchedTerm(self, Value: int): - self._check_for_error(self._lib.Fuses_Set_SwitchedTerm(Value)) + self._lib.Fuses_Set_SwitchedTerm(Value) @property def TCCcurve(self) -> str: @@ -161,14 +155,11 @@ def TCCcurve(self) -> str: Original COM help: https://opendss.epri.com/TCCcurve.html ''' - return self._get_string(self._check_for_error(self._lib.Fuses_Get_TCCcurve())) + return self._lib.Fuses_Get_TCCcurve() @TCCcurve.setter def TCCcurve(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Fuses_Set_TCCcurve(Value)) + self._lib.Fuses_Set_TCCcurve(Value) @property def State(self) -> List[str]: @@ -177,11 +168,11 @@ def State(self) -> List[str]: Original COM help: https://opendss.epri.com/State2.html ''' - return self._check_for_error(self._get_string_array(self._lib.Fuses_Get_State)) + return self._lib.Fuses_Get_State() @State.setter def State(self, Value: List[AnyStr]): - self._check_for_error(self._set_string_array(self._lib.Fuses_Set_State, Value)) + self._set_string_array(self._lib.Fuses_Set_State, Value) @property def NormalState(self) -> List[str]: @@ -190,8 +181,8 @@ def NormalState(self) -> List[str]: Original COM help: https://opendss.epri.com/NormalState2.html ''' - return self._check_for_error(self._get_string_array(self._lib.Fuses_Get_NormalState)) + return self._lib.Fuses_Get_NormalState() @NormalState.setter def NormalState(self, Value: List[AnyStr]): - self._check_for_error(self._set_string_array(self._lib.Fuses_Set_NormalState, Value)) + self._set_string_array(self._lib.Fuses_Set_NormalState, Value) diff --git a/dss/IGICSources.py b/dss/IGICSources.py index 44f2e3a4..8beafc05 100644 --- a/dss/IGICSources.py +++ b/dss/IGICSources.py @@ -25,81 +25,81 @@ class IGICSources(Iterable): @property def Bus1(self) -> str: '''First bus name of GICSource (Created name)''' - return self._get_string(self._check_for_error(self._lib.GICSources_Get_Bus1())) + return self._lib.GICSources_Get_Bus1() @property def Bus2(self) -> str: '''Second bus name''' - return self._get_string(self._check_for_error(self._lib.GICSources_Get_Bus2())) + return self._lib.GICSources_Get_Bus2() @property def Phases(self) -> int: '''Number of Phases, this GICSource element.''' - return self._check_for_error(self._lib.GICSources_Get_Phases()) + return self._lib.GICSources_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.GICSources_Set_Phases(Value)) + self._lib.GICSources_Set_Phases(Value) @property def EN(self) -> float: '''Northward E Field V/km''' - return self._check_for_error(self._lib.GICSources_Get_EN()) + return self._lib.GICSources_Get_EN() @EN.setter def EN(self, Value: float): - self._check_for_error(self._lib.GICSources_Set_EN(Value)) + self._lib.GICSources_Set_EN(Value) @property def EE(self) -> float: '''Eastward E Field, V/km''' - return self._check_for_error(self._lib.GICSources_Get_EE()) + return self._lib.GICSources_Get_EE() @EE.setter def EE(self, Value: float): - self._check_for_error(self._lib.GICSources_Set_EE(Value)) + self._lib.GICSources_Set_EE(Value) @property def Lat1(self) -> float: '''Latitude of Bus1 (degrees)''' - return self._check_for_error(self._lib.GICSources_Get_Lat1()) + return self._lib.GICSources_Get_Lat1() @Lat1.setter def Lat1(self, Value: float): - self._check_for_error(self._lib.GICSources_Set_Lat1(Value)) + self._lib.GICSources_Set_Lat1(Value) @property def Lat2(self) -> float: '''Latitude of Bus2 (degrees)''' - return self._check_for_error(self._lib.GICSources_Get_Lat2()) + return self._lib.GICSources_Get_Lat2() @Lat2.setter def Lat2(self, Value: float): - self._check_for_error(self._lib.GICSources_Set_Lat2(Value)) + self._lib.GICSources_Set_Lat2(Value) @property def Lon1(self) -> float: '''Longitude of Bus1 (Degrees)''' - return self._check_for_error(self._lib.GICSources_Get_Lon1()) + return self._lib.GICSources_Get_Lon1() @Lon1.setter def Lon1(self, Value: float): - self._check_for_error(self._lib.GICSources_Set_Lon1(Value)) + self._lib.GICSources_Set_Lon1(Value) @property def Lon2(self) -> float: '''Longitude of Bus2 (Degrees)''' - return self._check_for_error(self._lib.GICSources_Get_Lon2()) + return self._lib.GICSources_Get_Lon2() @Lon2.setter def Lon2(self, Value: float): - self._check_for_error(self._lib.GICSources_Set_Lon2(Value)) + self._lib.GICSources_Set_Lon2(Value) @property def Volts(self) -> float: '''Specify dc voltage directly''' - return self._check_for_error(self._lib.GICSources_Get_Volts()) + return self._lib.GICSources_Get_Volts() @Volts.setter def Volts(self, Value: float): - self._check_for_error(self._lib.GICSources_Set_Volts(Value)) + self._lib.GICSources_Set_Volts(Value) diff --git a/dss/IGenerators.py b/dss/IGenerators.py index ae1fabd5..350a1e8c 100644 --- a/dss/IGenerators.py +++ b/dss/IGenerators.py @@ -42,11 +42,11 @@ def ForcedON(self) -> bool: Original COM help: https://opendss.epri.com/ForcedON.html ''' - return self._check_for_error(self._lib.Generators_Get_ForcedON()) != 0 + return self._lib.Generators_Get_ForcedON() @ForcedON.setter def ForcedON(self, Value: bool): - self._check_for_error(self._lib.Generators_Set_ForcedON(Value)) + self._lib.Generators_Set_ForcedON(Value) @property def Model(self) -> int: @@ -55,11 +55,11 @@ def Model(self) -> int: Original COM help: https://opendss.epri.com/Model.html ''' - return self._check_for_error(self._lib.Generators_Get_Model()) #TODO: use enum + return self._lib.Generators_Get_Model() #TODO: use enum @Model.setter def Model(self, Value: int): - self._check_for_error(self._lib.Generators_Set_Model(Value)) + self._lib.Generators_Set_Model(Value) @property def PF(self) -> float: @@ -68,11 +68,11 @@ def PF(self) -> float: Original COM help: https://opendss.epri.com/PF.html ''' - return self._check_for_error(self._lib.Generators_Get_PF()) + return self._lib.Generators_Get_PF() @PF.setter def PF(self, Value: float): - self._check_for_error(self._lib.Generators_Set_PF(Value)) + self._lib.Generators_Set_PF(Value) @property def Phases(self) -> int: @@ -81,11 +81,11 @@ def Phases(self) -> int: Original COM help: https://opendss.epri.com/Phases.html ''' - return self._check_for_error(self._lib.Generators_Get_Phases()) + return self._lib.Generators_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.Generators_Set_Phases(Value)) + self._lib.Generators_Set_Phases(Value) @property def RegisterNames(self) -> List[str]: @@ -94,7 +94,7 @@ def RegisterNames(self) -> List[str]: See also the enum `GeneratorRegisters`. ''' - return self._check_for_error(self._get_string_array(self._lib.Generators_Get_RegisterNames)) + return self._lib.Generators_Get_RegisterNames() @property def RegisterValues(self) -> Float64Array: @@ -103,8 +103,7 @@ def RegisterValues(self) -> Float64Array: Original COM help: https://opendss.epri.com/RegisterValues.html ''' - self._check_for_error(self._lib.Generators_Get_RegisterValues_GR()) - return self._get_float64_gr_array() + return self._lib.Generators_Get_RegisterValues_GR() @property def Vmaxpu(self) -> float: @@ -113,11 +112,11 @@ def Vmaxpu(self) -> float: Original COM help: https://opendss.epri.com/Vmaxpu.html ''' - return self._check_for_error(self._lib.Generators_Get_Vmaxpu()) + return self._lib.Generators_Get_Vmaxpu() @Vmaxpu.setter def Vmaxpu(self, Value: float): - self._check_for_error(self._lib.Generators_Set_Vmaxpu(Value)) + self._lib.Generators_Set_Vmaxpu(Value) @property def Vminpu(self) -> float: @@ -126,11 +125,11 @@ def Vminpu(self) -> float: Original COM help: https://opendss.epri.com/Vminpu.html ''' - return self._check_for_error(self._lib.Generators_Get_Vminpu()) + return self._lib.Generators_Get_Vminpu() @Vminpu.setter def Vminpu(self, Value: float): - self._check_for_error(self._lib.Generators_Set_Vminpu(Value)) + self._lib.Generators_Set_Vminpu(Value) @property def kV(self) -> float: @@ -139,11 +138,11 @@ def kV(self) -> float: Original COM help: https://opendss.epri.com/kV1.html ''' - return self._check_for_error(self._lib.Generators_Get_kV()) + return self._lib.Generators_Get_kV() @kV.setter def kV(self, Value: float): - self._check_for_error(self._lib.Generators_Set_kV(Value)) + self._lib.Generators_Set_kV(Value) @property def kVArated(self) -> float: @@ -152,11 +151,11 @@ def kVArated(self) -> float: Original COM help: https://opendss.epri.com/kVArated.html ''' - return self._check_for_error(self._lib.Generators_Get_kVArated()) + return self._lib.Generators_Get_kVArated() @kVArated.setter def kVArated(self, Value: float): - self._check_for_error(self._lib.Generators_Set_kVArated(Value)) + self._lib.Generators_Set_kVArated(Value) @property def kW(self) -> float: @@ -165,11 +164,11 @@ def kW(self) -> float: Original COM help: https://opendss.epri.com/kW.html ''' - return self._check_for_error(self._lib.Generators_Get_kW()) + return self._lib.Generators_Get_kW() @kW.setter def kW(self, Value: float): - self._check_for_error(self._lib.Generators_Set_kW(Value)) + self._lib.Generators_Set_kW(Value) @property def kvar(self) -> float: @@ -178,11 +177,11 @@ def kvar(self) -> float: Original COM help: https://opendss.epri.com/kvar.html ''' - return self._check_for_error(self._lib.Generators_Get_kvar()) + return self._lib.Generators_Get_kvar() @kvar.setter def kvar(self, Value: float): - self._check_for_error(self._lib.Generators_Set_kvar(Value)) + self._lib.Generators_Set_kvar(Value) @property def daily(self) -> str: @@ -191,14 +190,11 @@ def daily(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.Generators_Get_daily())) + return self._lib.Generators_Get_daily() @daily.setter def daily(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Generators_Set_daily(Value)) + self._lib.Generators_Set_daily(Value) @property def duty(self) -> str: @@ -207,14 +203,11 @@ def duty(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.Generators_Get_duty())) + return self._lib.Generators_Get_duty() @duty.setter def duty(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Generators_Set_duty(Value)) + self._lib.Generators_Set_duty(Value) @property def Yearly(self) -> str: @@ -223,14 +216,11 @@ def Yearly(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.Generators_Get_Yearly())) + return self._lib.Generators_Get_Yearly() @Yearly.setter def Yearly(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Generators_Set_Yearly(Value)) + self._lib.Generators_Set_Yearly(Value) @property def Status(self) -> GeneratorStatus: @@ -241,11 +231,11 @@ def Status(self) -> GeneratorStatus: **(API Extension)** ''' - return GeneratorStatus(self._check_for_error(self._lib.Generators_Get_Status())) + return GeneratorStatus(self._lib.Generators_Get_Status()) @Status.setter def Status(self, Value: Union[int, GeneratorStatus]): - self._check_for_error(self._lib.Generators_Set_Status(Value)) + self._lib.Generators_Set_Status(Value) @property def IsDelta(self) -> bool: @@ -254,11 +244,11 @@ def IsDelta(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.Generators_Get_IsDelta()) != 0 + return self._lib.Generators_Get_IsDelta() @IsDelta.setter def IsDelta(self, Value: bool): - self._check_for_error(self._lib.Generators_Set_IsDelta(Value)) + self._lib.Generators_Set_IsDelta(Value) @property def kva(self) -> float: @@ -267,11 +257,11 @@ def kva(self) -> float: **(API Extension)** ''' - return self._check_for_error(self._lib.Generators_Get_kva()) + return self._lib.Generators_Get_kva() @kva.setter def kva(self, Value: float): - self._check_for_error(self._lib.Generators_Set_kva(Value)) + self._lib.Generators_Set_kva(Value) @property def Class(self) -> int: @@ -280,11 +270,11 @@ def Class(self) -> int: **(API Extension)** ''' - return self._check_for_error(self._lib.Generators_Get_Class_()) + return self._lib.Generators_Get_Class_() @Class.setter def Class(self, Value: int): - self._check_for_error(self._lib.Generators_Set_Class_(Value)) + self._lib.Generators_Set_Class_(Value) @property def Bus1(self) -> str: @@ -293,12 +283,9 @@ def Bus1(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.Generators_Get_Bus1())) + return self._lib.Generators_Get_Bus1() @Bus1.setter def Bus1(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Generators_Set_Bus1(Value)) + self._lib.Generators_Set_Bus1(Value) diff --git a/dss/IISources.py b/dss/IISources.py index 52fa7159..9e14e22b 100644 --- a/dss/IISources.py +++ b/dss/IISources.py @@ -22,11 +22,11 @@ def Amps(self) -> float: Original COM help: https://opendss.epri.com/Amps.html ''' - return self._check_for_error(self._lib.ISources_Get_Amps()) + return self._lib.ISources_Get_Amps() @Amps.setter def Amps(self, Value: float): - self._check_for_error(self._lib.ISources_Set_Amps(Value)) + self._lib.ISources_Set_Amps(Value) @property def AngleDeg(self) -> float: @@ -35,11 +35,11 @@ def AngleDeg(self) -> float: Original COM help: https://opendss.epri.com/AngleDeg.html ''' - return self._check_for_error(self._lib.ISources_Get_AngleDeg()) + return self._lib.ISources_Get_AngleDeg() @AngleDeg.setter def AngleDeg(self, Value: float): - self._check_for_error(self._lib.ISources_Set_AngleDeg(Value)) + self._lib.ISources_Set_AngleDeg(Value) @property def Frequency(self) -> float: @@ -48,9 +48,9 @@ def Frequency(self) -> float: Original COM help: https://opendss.epri.com/Frequency.html ''' - return self._check_for_error(self._lib.ISources_Get_Frequency()) + return self._lib.ISources_Get_Frequency() @Frequency.setter def Frequency(self, Value: float): - self._check_for_error(self._lib.ISources_Set_Frequency(Value)) + self._lib.ISources_Set_Frequency(Value) \ No newline at end of file diff --git a/dss/ILineCodes.py b/dss/ILineCodes.py index ee126443..b4fa1abc 100644 --- a/dss/ILineCodes.py +++ b/dss/ILineCodes.py @@ -35,11 +35,11 @@ def C0(self): Original COM help: https://opendss.epri.com/C2.html ''' - return self._check_for_error(self._lib.LineCodes_Get_C0()) + return self._lib.LineCodes_Get_C0() @C0.setter def C0(self, Value): - self._check_for_error(self._lib.LineCodes_Set_C0(Value)) + self._lib.LineCodes_Set_C0(Value) @property def C1(self): @@ -48,11 +48,11 @@ def C1(self): Original COM help: https://opendss.epri.com/C3.html ''' - return self._check_for_error(self._lib.LineCodes_Get_C1()) + return self._lib.LineCodes_Get_C1() @C1.setter def C1(self, Value): - self._check_for_error(self._lib.LineCodes_Set_C1(Value)) + self._lib.LineCodes_Set_C1(Value) @property def Cmatrix(self) -> Float64Array: @@ -61,13 +61,12 @@ def Cmatrix(self) -> Float64Array: Original COM help: https://opendss.epri.com/Cmatrix1.html ''' - self._check_for_error(self._lib.LineCodes_Get_Cmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.LineCodes_Get_Cmatrix_GR() @Cmatrix.setter def Cmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LineCodes_Set_Cmatrix(ValuePtr, ValueCount)) + self._lib.LineCodes_Set_Cmatrix(ValuePtr, ValueCount) @property def EmergAmps(self) -> float: @@ -76,11 +75,11 @@ def EmergAmps(self) -> float: Original COM help: https://opendss.epri.com/EmergAmps2.html ''' - return self._check_for_error(self._lib.LineCodes_Get_EmergAmps()) + return self._lib.LineCodes_Get_EmergAmps() @EmergAmps.setter def EmergAmps(self, Value: float): - self._check_for_error(self._lib.LineCodes_Set_EmergAmps(Value)) + self._lib.LineCodes_Set_EmergAmps(Value) @property def IsZ1Z0(self) -> bool: @@ -89,7 +88,7 @@ def IsZ1Z0(self) -> bool: Original COM help: https://opendss.epri.com/IsZ1Z0.html ''' - return self._check_for_error(self._lib.LineCodes_Get_IsZ1Z0()) != 0 + return self._lib.LineCodes_Get_IsZ1Z0() @property def NormAmps(self) -> float: @@ -98,11 +97,11 @@ def NormAmps(self) -> float: Original COM help: https://opendss.epri.com/NormAmps1.html ''' - return self._check_for_error(self._lib.LineCodes_Get_NormAmps()) + return self._lib.LineCodes_Get_NormAmps() @NormAmps.setter def NormAmps(self, Value: float): - self._check_for_error(self._lib.LineCodes_Set_NormAmps(Value)) + self._lib.LineCodes_Set_NormAmps(Value) @property def Phases(self) -> int: @@ -111,11 +110,11 @@ def Phases(self) -> int: Original COM help: https://opendss.epri.com/Phases2.html ''' - return self._check_for_error(self._lib.LineCodes_Get_Phases()) + return self._lib.LineCodes_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.LineCodes_Set_Phases(Value)) + self._lib.LineCodes_Set_Phases(Value) @property def R0(self) -> float: @@ -124,11 +123,11 @@ def R0(self) -> float: Original COM help: https://opendss.epri.com/R2.html ''' - return self._check_for_error(self._lib.LineCodes_Get_R0()) + return self._lib.LineCodes_Get_R0() @R0.setter def R0(self, Value: float): - self._check_for_error(self._lib.LineCodes_Set_R0(Value)) + self._lib.LineCodes_Set_R0(Value) @property def R1(self) -> float: @@ -137,11 +136,11 @@ def R1(self) -> float: Original COM help: https://opendss.epri.com/R3.html ''' - return self._check_for_error(self._lib.LineCodes_Get_R1()) + return self._lib.LineCodes_Get_R1() @R1.setter def R1(self, Value: float): - self._check_for_error(self._lib.LineCodes_Set_R1(Value)) + self._lib.LineCodes_Set_R1(Value) @property def Rmatrix(self) -> Float64Array: @@ -150,21 +149,20 @@ def Rmatrix(self) -> Float64Array: Original COM help: https://opendss.epri.com/Rmatrix1.html ''' - self._check_for_error(self._lib.LineCodes_Get_Rmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.LineCodes_Get_Rmatrix_GR() @Rmatrix.setter def Rmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LineCodes_Set_Rmatrix(ValuePtr, ValueCount)) + self._lib.LineCodes_Set_Rmatrix(ValuePtr, ValueCount) @property def Units(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.LineCodes_Get_Units())) + return LineUnits(self._lib.LineCodes_Get_Units()) @Units.setter def Units(self, Value: Union[int, LineUnits]): - self._check_for_error(self._lib.LineCodes_Set_Units(Value)) + self._lib.LineCodes_Set_Units(Value) @property def X0(self) -> float: @@ -173,11 +171,11 @@ def X0(self) -> float: Original COM help: https://opendss.epri.com/X2.html ''' - return self._check_for_error(self._lib.LineCodes_Get_X0()) + return self._lib.LineCodes_Get_X0() @X0.setter def X0(self, Value: float): - self._check_for_error(self._lib.LineCodes_Set_X0(Value)) + self._lib.LineCodes_Set_X0(Value) @property def X1(self) -> float: @@ -186,11 +184,11 @@ def X1(self) -> float: Original COM help: https://opendss.epri.com/X3.html ''' - return self._check_for_error(self._lib.LineCodes_Get_X1()) + return self._lib.LineCodes_Get_X1() @X1.setter def X1(self, Value: float): - self._check_for_error(self._lib.LineCodes_Set_X1(Value)) + self._lib.LineCodes_Set_X1(Value) @property def Xmatrix(self) -> Float64Array: @@ -199,10 +197,9 @@ def Xmatrix(self) -> Float64Array: Original COM help: https://opendss.epri.com/Xmatrix1.html ''' - self._check_for_error(self._lib.LineCodes_Get_Xmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.LineCodes_Get_Xmatrix_GR() @Xmatrix.setter def Xmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LineCodes_Set_Xmatrix(ValuePtr, ValueCount)) + self._lib.LineCodes_Set_Xmatrix(ValuePtr, ValueCount) diff --git a/dss/ILineGeometries.py b/dss/ILineGeometries.py index 5208c176..517f6a22 100644 --- a/dss/ILineGeometries.py +++ b/dss/ILineGeometries.py @@ -36,108 +36,101 @@ class ILineGeometries(Iterable): @property def Conductors(self) -> List[str]: '''Array of strings with names of all conductors in the active LineGeometry object''' - return self._check_for_error(self._get_string_array(self._lib.LineGeometries_Get_Conductors)) + return self._lib.LineGeometries_Get_Conductors() @property def EmergAmps(self) -> float: '''Emergency ampere rating''' - return self._check_for_error(self._lib.LineGeometries_Get_EmergAmps()) + return self._lib.LineGeometries_Get_EmergAmps() @EmergAmps.setter def EmergAmps(self, Value: float): - self._check_for_error(self._lib.LineGeometries_Set_EmergAmps(Value)) + self._lib.LineGeometries_Set_EmergAmps(Value) @property def NormAmps(self) -> float: '''Normal ampere rating''' - return self._check_for_error(self._lib.LineGeometries_Get_NormAmps()) + return self._lib.LineGeometries_Get_NormAmps() @NormAmps.setter def NormAmps(self, Value: float): - self._check_for_error(self._lib.LineGeometries_Set_NormAmps(Value)) + self._lib.LineGeometries_Set_NormAmps(Value) @property def RhoEarth(self) -> float: - return self._check_for_error(self._lib.LineGeometries_Get_RhoEarth()) + return self._lib.LineGeometries_Get_RhoEarth() @RhoEarth.setter def RhoEarth(self, Value: float): - self._check_for_error(self._lib.LineGeometries_Set_RhoEarth(Value)) + self._lib.LineGeometries_Set_RhoEarth(Value) @property def Reduce(self) -> bool: - return self._check_for_error(self._lib.LineGeometries_Get_Reduce()) != 0 + return self._lib.LineGeometries_Get_Reduce() @Reduce.setter def Reduce(self, Value: bool): - self._check_for_error(self._lib.LineGeometries_Set_Reduce(Value)) + self._lib.LineGeometries_Set_Reduce(Value) @property def Phases(self) -> int: '''Number of Phases''' - return self._check_for_error(self._lib.LineGeometries_Get_Phases()) + return self._lib.LineGeometries_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.LineGeometries_Set_Phases(Value)) + self._lib.LineGeometries_Set_Phases(Value) def Rmatrix(self, Frequency: float, Length: float, Units: int) -> Float64Array: '''Resistance matrix, ohms''' - self._check_for_error(self._lib.LineGeometries_Get_Rmatrix_GR(Frequency, Length, Units)) - return self._get_float64_gr_array() + return self._lib.LineGeometries_Get_Rmatrix_GR(Frequency, Length, Units) def Xmatrix(self, Frequency: float, Length: float, Units: int) -> Float64Array: '''Reactance matrix, ohms''' - self._check_for_error(self._lib.LineGeometries_Get_Xmatrix_GR(Frequency, Length, Units)) - return self._get_float64_gr_array() + return self._lib.LineGeometries_Get_Xmatrix_GR(Frequency, Length, Units) def Zmatrix(self, Frequency: float, Length: float, Units: int) -> Float64ArrayOrComplexArray: '''Complex impedance matrix, ohms''' - self._check_for_error(self._lib.LineGeometries_Get_Zmatrix_GR(Frequency, Length, Units)) - return self._get_complex128_gr_array() + return self._lib.LineGeometries_Get_Zmatrix_GR(Frequency, Length, Units) def Cmatrix(self, Frequency: float, Length: float, Units: int) -> Float64Array: '''Capacitance matrix, nF''' - self._check_for_error(self._lib.LineGeometries_Get_Cmatrix_GR(Frequency, Length, Units)) - return self._get_float64_gr_array() + return self._lib.LineGeometries_Get_Cmatrix_GR(Frequency, Length, Units) @property def Units(self) -> List[LineUnits]: - self._check_for_error(self._lib.LineGeometries_Get_Units_GR()) - return [LineUnits(unit) for unit in self._get_int32_gr_array()] + return [LineUnits(unit) for unit in self._lib.LineGeometries_Get_Units_GR()] @Units.setter def Units(self, Value: Union[Int32Array, List[LineUnits]]): Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) - self._check_for_error(self._lib.LineGeometries_Set_Units(ValuePtr, ValueCount)) + self._lib.LineGeometries_Set_Units(ValuePtr, ValueCount) @property def Xcoords(self) -> Float64Array: '''Get/Set the X (horizontal) coordinates of the conductors''' - self._check_for_error(self._lib.LineGeometries_Get_Xcoords_GR()) - return self._get_float64_gr_array() + return self._lib.LineGeometries_Get_Xcoords_GR() @Xcoords.setter def Xcoords(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LineGeometries_Set_Xcoords(ValuePtr, ValueCount)) + self._lib.LineGeometries_Set_Xcoords(ValuePtr, ValueCount) @property def Ycoords(self) -> Float64Array: '''Get/Set the Y (vertical/height) coordinates of the conductors''' - self._check_for_error(self._lib.LineGeometries_Get_Ycoords_GR()) - return self._get_float64_gr_array() + return self._lib.LineGeometries_Get_Ycoords_GR() @Ycoords.setter def Ycoords(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LineGeometries_Set_Ycoords(ValuePtr, ValueCount)) + self._lib.LineGeometries_Set_Ycoords(ValuePtr, ValueCount) @property def Nconds(self) -> int: '''Number of conductors in this geometry. Default is 3. Triggers memory allocations. Define first!''' - return self._check_for_error(self._lib.LineGeometries_Get_Nconds()) + return self._lib.LineGeometries_Get_Nconds() @Nconds.setter def Nconds(self, Value: int): - self._check_for_error(self._lib.LineGeometries_Set_Nconds(Value)) + self._lib.LineGeometries_Set_Nconds(Value) diff --git a/dss/ILineSpacings.py b/dss/ILineSpacings.py index 83995488..105239f6 100644 --- a/dss/ILineSpacings.py +++ b/dss/ILineSpacings.py @@ -28,46 +28,44 @@ class ILineSpacings(Iterable): @property def Phases(self) -> int: '''Number of Phases''' - return self._check_for_error(self._lib.LineSpacings_Get_Phases()) + return self._lib.LineSpacings_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.LineSpacings_Set_Phases(Value)) + self._lib.LineSpacings_Set_Phases(Value) @property def Nconds(self) -> int: - return self._check_for_error(self._lib.LineSpacings_Get_Nconds()) + return self._lib.LineSpacings_Get_Nconds() @Nconds.setter def Nconds(self, Value: int): - self._check_for_error(self._lib.LineSpacings_Set_Nconds(Value)) + self._lib.LineSpacings_Set_Nconds(Value) @property def Units(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.LineSpacings_Get_Units())) + return LineUnits(self._lib.LineSpacings_Get_Units()) @Units.setter def Units(self, Value: Union[int, LineUnits]): - self._check_for_error(self._lib.LineSpacings_Set_Units(Value)) + self._lib.LineSpacings_Set_Units(Value) @property def Xcoords(self) -> Float64Array: '''Get/Set the X (horizontal) coordinates of the conductors''' - self._check_for_error(self._lib.LineSpacings_Get_Xcoords_GR()) - return self._get_float64_gr_array() + return self._lib.LineSpacings_Get_Xcoords_GR() @Xcoords.setter def Xcoords(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LineSpacings_Set_Xcoords(ValuePtr, ValueCount)) + self._lib.LineSpacings_Set_Xcoords(ValuePtr, ValueCount) @property def Ycoords(self) -> Float64Array: '''Get/Set the Y (vertical/height) coordinates of the conductors''' - self._check_for_error(self._lib.LineSpacings_Get_Ycoords_GR()) - return self._get_float64_gr_array() + return self._lib.LineSpacings_Get_Ycoords_GR() @Ycoords.setter def Ycoords(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LineSpacings_Set_Ycoords(ValuePtr, ValueCount)) + self._lib.LineSpacings_Set_Ycoords(ValuePtr, ValueCount) diff --git a/dss/ILines.py b/dss/ILines.py index 142eb011..f80c462c 100644 --- a/dss/ILines.py +++ b/dss/ILines.py @@ -43,10 +43,7 @@ class ILines(Iterable): ] def New(self, Name): - if not isinstance(Name, bytes): - Name = Name.encode(self._api_util.codec) - - return self._check_for_error(self._lib.Lines_New(Name)) + return self._lib.Lines_New(Name) @property def Bus1(self) -> str: @@ -55,14 +52,11 @@ def Bus1(self) -> str: Original COM help: https://opendss.epri.com/Bus1.html ''' - return self._get_string(self._check_for_error(self._lib.Lines_Get_Bus1())) + return self._lib.Lines_Get_Bus1() @Bus1.setter def Bus1(self, Value): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Lines_Set_Bus1(Value)) + self._lib.Lines_Set_Bus1(Value) @property def Bus2(self) -> str: @@ -71,14 +65,11 @@ def Bus2(self) -> str: Original COM help: https://opendss.epri.com/Bus2.html ''' - return self._get_string(self._check_for_error(self._lib.Lines_Get_Bus2())) + return self._lib.Lines_Get_Bus2() @Bus2.setter def Bus2(self, Value): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Lines_Set_Bus2(Value)) + self._lib.Lines_Set_Bus2(Value) @property def C0(self) -> float: @@ -87,11 +78,11 @@ def C0(self) -> float: Original COM help: https://opendss.epri.com/C0.html ''' - return self._check_for_error(self._lib.Lines_Get_C0()) + return self._lib.Lines_Get_C0() @C0.setter def C0(self, Value: float): - self._check_for_error(self._lib.Lines_Set_C0(Value)) + self._lib.Lines_Set_C0(Value) @property def C1(self) -> float: @@ -100,21 +91,20 @@ def C1(self) -> float: Original COM help: https://opendss.epri.com/C1.html ''' - return self._check_for_error(self._lib.Lines_Get_C1()) + return self._lib.Lines_Get_C1() @C1.setter def C1(self, Value: float): - self._check_for_error(self._lib.Lines_Set_C1(Value)) + self._lib.Lines_Set_C1(Value) @property def Cmatrix(self) -> Float64Array: - self._check_for_error(self._lib.Lines_Get_Cmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.Lines_Get_Cmatrix_GR() @Cmatrix.setter def Cmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Lines_Set_Cmatrix(ValuePtr, ValueCount)) + self._lib.Lines_Set_Cmatrix(ValuePtr, ValueCount) @property def EmergAmps(self) -> float: @@ -123,11 +113,11 @@ def EmergAmps(self) -> float: Original COM help: https://opendss.epri.com/EmergAmps1.html ''' - return self._check_for_error(self._lib.Lines_Get_EmergAmps()) + return self._lib.Lines_Get_EmergAmps() @EmergAmps.setter def EmergAmps(self, Value: float): - self._check_for_error(self._lib.Lines_Set_EmergAmps(Value)) + self._lib.Lines_Set_EmergAmps(Value) @property def Geometry(self) -> str: @@ -136,14 +126,11 @@ def Geometry(self) -> str: Original COM help: https://opendss.epri.com/Geometry.html ''' - return self._get_string(self._check_for_error(self._lib.Lines_Get_Geometry())) + return self._lib.Lines_Get_Geometry() @Geometry.setter def Geometry(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Lines_Set_Geometry(Value)) + self._lib.Lines_Set_Geometry(Value) @property def Length(self) -> float: @@ -152,11 +139,11 @@ def Length(self) -> float: Original COM help: https://opendss.epri.com/Length.html ''' - return self._check_for_error(self._lib.Lines_Get_Length()) + return self._lib.Lines_Get_Length() @Length.setter def Length(self, Value: float): - self._check_for_error(self._lib.Lines_Set_Length(Value)) + self._lib.Lines_Set_Length(Value) @property def LineCode(self) -> str: @@ -165,14 +152,11 @@ def LineCode(self) -> str: Original COM help: https://opendss.epri.com/LineCode.html ''' - return self._get_string(self._check_for_error(self._lib.Lines_Get_LineCode())) + return self._lib.Lines_Get_LineCode() @LineCode.setter def LineCode(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Lines_Set_LineCode(Value)) + self._lib.Lines_Set_LineCode(Value) @property def NormAmps(self) -> float: @@ -181,11 +165,11 @@ def NormAmps(self) -> float: Original COM help: https://opendss.epri.com/NormAmps.html ''' - return self._check_for_error(self._lib.Lines_Get_NormAmps()) + return self._lib.Lines_Get_NormAmps() @NormAmps.setter def NormAmps(self, Value: float): - self._check_for_error(self._lib.Lines_Set_NormAmps(Value)) + self._lib.Lines_Set_NormAmps(Value) @property def NumCust(self) -> int: @@ -196,7 +180,7 @@ def NumCust(self) -> int: Original COM help: https://opendss.epri.com/NumCust.html ''' - return self._check_for_error(self._lib.Lines_Get_NumCust()) + return self._lib.Lines_Get_NumCust() @property def Parent(self) -> int: @@ -207,7 +191,7 @@ def Parent(self) -> int: Original COM help: https://opendss.epri.com/Parent.html ''' - return self._check_for_error(self._lib.Lines_Get_Parent()) + return self._lib.Lines_Get_Parent() @property def Phases(self) -> int: @@ -216,11 +200,11 @@ def Phases(self) -> int: Original COM help: https://opendss.epri.com/Phases1.html ''' - return self._check_for_error(self._lib.Lines_Get_Phases()) + return self._lib.Lines_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.Lines_Set_Phases(Value)) + self._lib.Lines_Set_Phases(Value) @property def R0(self) -> float: @@ -229,11 +213,11 @@ def R0(self) -> float: Original COM help: https://opendss.epri.com/R0.html ''' - return self._check_for_error(self._lib.Lines_Get_R0()) + return self._lib.Lines_Get_R0() @R0.setter def R0(self, Value: float): - self._check_for_error(self._lib.Lines_Set_R0(Value)) + self._lib.Lines_Set_R0(Value) @property def R1(self) -> float: @@ -242,11 +226,11 @@ def R1(self) -> float: Original COM help: https://opendss.epri.com/R1.html ''' - return self._check_for_error(self._lib.Lines_Get_R1()) + return self._lib.Lines_Get_R1() @R1.setter def R1(self, Value: float): - self._check_for_error(self._lib.Lines_Set_R1(Value)) + self._lib.Lines_Set_R1(Value) @property def Rg(self) -> float: @@ -255,11 +239,11 @@ def Rg(self) -> float: Original COM help: https://opendss.epri.com/Rg.html ''' - return self._check_for_error(self._lib.Lines_Get_Rg()) + return self._lib.Lines_Get_Rg() @Rg.setter def Rg(self, Value: float): - self._check_for_error(self._lib.Lines_Set_Rg(Value)) + self._lib.Lines_Set_Rg(Value) @property def Rho(self) -> float: @@ -268,11 +252,11 @@ def Rho(self) -> float: Original COM help: https://opendss.epri.com/Rho.html ''' - return self._check_for_error(self._lib.Lines_Get_Rho()) + return self._lib.Lines_Get_Rho() @Rho.setter def Rho(self, Value: float): - self._check_for_error(self._lib.Lines_Set_Rho(Value)) + self._lib.Lines_Set_Rho(Value) @property def Rmatrix(self) -> Float64Array: @@ -281,13 +265,12 @@ def Rmatrix(self) -> Float64Array: Original COM help: https://opendss.epri.com/Rmatrix.html ''' - self._check_for_error(self._lib.Lines_Get_Rmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.Lines_Get_Rmatrix_GR() @Rmatrix.setter def Rmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Lines_Set_Rmatrix(ValuePtr, ValueCount)) + self._lib.Lines_Set_Rmatrix(ValuePtr, ValueCount) @property def Spacing(self) -> str: @@ -296,14 +279,11 @@ def Spacing(self) -> str: Original COM help: https://opendss.epri.com/Spacing.html ''' - return self._get_string(self._check_for_error(self._lib.Lines_Get_Spacing())) + return self._lib.Lines_Get_Spacing() @Spacing.setter def Spacing(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Lines_Set_Spacing(Value)) + self._lib.Lines_Set_Spacing(Value) @property def TotalCust(self) -> int: @@ -312,15 +292,15 @@ def TotalCust(self) -> int: Original COM help: https://opendss.epri.com/TotalCust.html ''' - return self._check_for_error(self._lib.Lines_Get_TotalCust()) + return self._lib.Lines_Get_TotalCust() @property def Units(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.Lines_Get_Units())) + return LineUnits(self._lib.Lines_Get_Units()) @Units.setter def Units(self, Value: Union[int, LineUnits]): - self._check_for_error(self._lib.Lines_Set_Units(Value)) + self._lib.Lines_Set_Units(Value) @property def X0(self) -> float: @@ -329,11 +309,11 @@ def X0(self) -> float: Original COM help: https://opendss.epri.com/X0.html ''' - return self._check_for_error(self._lib.Lines_Get_X0()) + return self._lib.Lines_Get_X0() @X0.setter def X0(self, Value: float): - self._check_for_error(self._lib.Lines_Set_X0(Value)) + self._lib.Lines_Set_X0(Value) @property def X1(self) -> float: @@ -342,11 +322,11 @@ def X1(self) -> float: Original COM help: https://opendss.epri.com/X1.html ''' - return self._check_for_error(self._lib.Lines_Get_X1()) + return self._lib.Lines_Get_X1() @X1.setter def X1(self, Value: float): - self._check_for_error(self._lib.Lines_Set_X1(Value)) + self._lib.Lines_Set_X1(Value) @property def Xg(self) -> float: @@ -355,11 +335,11 @@ def Xg(self) -> float: Original COM help: https://opendss.epri.com/Xg.html ''' - return self._check_for_error(self._lib.Lines_Get_Xg()) + return self._lib.Lines_Get_Xg() @Xg.setter def Xg(self, Value: float): - self._check_for_error(self._lib.Lines_Set_Xg(Value)) + self._lib.Lines_Set_Xg(Value) @property def Xmatrix(self) -> Float64Array: @@ -368,13 +348,12 @@ def Xmatrix(self) -> Float64Array: Original COM help: https://opendss.epri.com/Xmatrix.html ''' - self._check_for_error(self._lib.Lines_Get_Xmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.Lines_Get_Xmatrix_GR() @Xmatrix.setter def Xmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Lines_Set_Xmatrix(ValuePtr, ValueCount)) + self._lib.Lines_Set_Xmatrix(ValuePtr, ValueCount) @property def Yprim(self) -> Float64ArrayOrComplexArray: @@ -383,13 +362,12 @@ def Yprim(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/Yprim1.html ''' - self._check_for_error(self._lib.Lines_Get_Yprim_GR()) - return self._get_complex128_gr_array() + return self._lib.Lines_Get_Yprim_GR() @Yprim.setter def Yprim(self, Value: Float64ArrayOrComplexArray): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Lines_Set_Yprim(ValuePtr, ValueCount)) + self._lib.Lines_Set_Yprim(ValuePtr, ValueCount) @property def SeasonRating(self) -> float: @@ -398,7 +376,7 @@ def SeasonRating(self) -> float: Original COM help: https://opendss.epri.com/SeasonRating.html ''' - return self._check_for_error(self._lib.Lines_Get_SeasonRating()) + return self._lib.Lines_Get_SeasonRating() @property def IsSwitch(self) -> bool: @@ -407,9 +385,9 @@ def IsSwitch(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.Lines_Get_IsSwitch()) != 0 + return self._lib.Lines_Get_IsSwitch() @IsSwitch.setter def IsSwitch(self, Value: bool): - self._check_for_error(self._lib.Lines_Set_IsSwitch(Value)) + self._lib.Lines_Set_IsSwitch(Value) diff --git a/dss/ILoadShapes.py b/dss/ILoadShapes.py index 5c9ab89d..30ccea84 100644 --- a/dss/ILoadShapes.py +++ b/dss/ILoadShapes.py @@ -25,14 +25,11 @@ class ILoadShapes(Iterable): def New(self, Name: AnyStr): '''Create a new LoadShape, with default parameters''' - if not isinstance(Name, bytes): - Name = Name.encode(self._api_util.codec) - - return self._check_for_error(self._lib.LoadShapes_New(Name)) + return self._lib.LoadShapes_New(Name) def Normalize(self): '''Normalize the LoadShape data inplace''' - self._check_for_error(self._lib.LoadShapes_Normalize()) + self._lib.LoadShapes_Normalize() @property def HrInterval(self) -> float: @@ -41,11 +38,11 @@ def HrInterval(self) -> float: Original COM help: https://opendss.epri.com/HrInterval.html ''' - return self._check_for_error(self._lib.LoadShapes_Get_HrInterval()) + return self._lib.LoadShapes_Get_HrInterval() @HrInterval.setter def HrInterval(self, Value: float): - self._check_for_error(self._lib.LoadShapes_Set_HrInterval(Value)) + self._lib.LoadShapes_Set_HrInterval(Value) @property def MinInterval(self) -> float: @@ -54,11 +51,11 @@ def MinInterval(self) -> float: Original COM help: https://opendss.epri.com/MinInterval.html ''' - return self._check_for_error(self._lib.LoadShapes_Get_MinInterval()) + return self._lib.LoadShapes_Get_MinInterval() @MinInterval.setter def MinInterval(self, Value: float): - self._check_for_error(self._lib.LoadShapes_Set_MinInterval(Value)) + self._lib.LoadShapes_Set_MinInterval(Value) @property def Npts(self) -> int: @@ -67,11 +64,11 @@ def Npts(self) -> int: Original COM help: https://opendss.epri.com/Npts.html ''' - return self._check_for_error(self._lib.LoadShapes_Get_Npts()) + return self._lib.LoadShapes_Get_Npts() @Npts.setter def Npts(self, Value: int): - self._check_for_error(self._lib.LoadShapes_Set_Npts(Value)) + self._lib.LoadShapes_Set_Npts(Value) @property def PBase(self) -> float: @@ -80,11 +77,11 @@ def PBase(self) -> float: Original COM help: https://opendss.epri.com/Pbase.html ''' - return self._check_for_error(self._lib.LoadShapes_Get_PBase()) + return self._lib.LoadShapes_Get_PBase() @PBase.setter def PBase(self, Value: float): - self._check_for_error(self._lib.LoadShapes_Set_PBase(Value)) + self._lib.LoadShapes_Set_PBase(Value) Pbase = PBase @@ -95,13 +92,12 @@ def Pmult(self) -> Float64Array: Original COM help: https://opendss.epri.com/Pmult.html ''' - self._check_for_error(self._lib.LoadShapes_Get_Pmult_GR()) - return self._get_float64_gr_array() + return self._lib.LoadShapes_Get_Pmult_GR() @Pmult.setter def Pmult(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LoadShapes_Set_Pmult(ValuePtr, ValueCount)) + self._lib.LoadShapes_Set_Pmult(ValuePtr, ValueCount) @property def QBase(self) -> float: @@ -110,11 +106,11 @@ def QBase(self) -> float: Original COM help: https://opendss.epri.com/Qbase.html ''' - return self._check_for_error(self._lib.LoadShapes_Get_Qbase()) + return self._lib.LoadShapes_Get_Qbase() @QBase.setter def QBase(self, Value: float): - self._check_for_error(self._lib.LoadShapes_Set_Qbase(Value)) + self._lib.LoadShapes_Set_Qbase(Value) Qbase = QBase @@ -125,13 +121,12 @@ def Qmult(self) -> Float64Array: Original COM help: https://opendss.epri.com/Qmult.html ''' - self._check_for_error(self._lib.LoadShapes_Get_Qmult_GR()) - return self._get_float64_gr_array() + return self._lib.LoadShapes_Get_Qmult_GR() @Qmult.setter def Qmult(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LoadShapes_Set_Qmult(ValuePtr, ValueCount)) + self._lib.LoadShapes_Set_Qmult(ValuePtr, ValueCount) @property def TimeArray(self) -> Float64Array: @@ -140,13 +135,12 @@ def TimeArray(self) -> Float64Array: Original COM help: https://opendss.epri.com/TimeArray.html ''' - self._check_for_error(self._lib.LoadShapes_Get_TimeArray_GR()) - return self._get_float64_gr_array() + return self._lib.LoadShapes_Get_TimeArray_GR() @TimeArray.setter def TimeArray(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.LoadShapes_Set_TimeArray(ValuePtr, ValueCount)) + self._lib.LoadShapes_Set_TimeArray(ValuePtr, ValueCount) @property def UseActual(self) -> bool: @@ -155,11 +149,11 @@ def UseActual(self) -> bool: Original COM help: https://opendss.epri.com/UseActual.html ''' - return self._check_for_error(self._lib.LoadShapes_Get_UseActual()) != 0 + return self._lib.LoadShapes_Get_UseActual() @UseActual.setter def UseActual(self, Value: bool): - self._check_for_error(self._lib.LoadShapes_Set_UseActual(Value)) + self._lib.LoadShapes_Set_UseActual(Value) @property def sInterval(self) -> float: @@ -168,11 +162,11 @@ def sInterval(self) -> float: Original COM help: https://opendss.epri.com/Sinterval.html ''' - return self._check_for_error(self._lib.LoadShapes_Get_SInterval()) + return self._lib.LoadShapes_Get_SInterval() @sInterval.setter def sInterval(self, Value: float): - self._check_for_error(self._lib.LoadShapes_Set_SInterval(Value)) + self._lib.LoadShapes_Set_SInterval(Value) Sinterval = sInterval SInterval = sInterval @@ -184,7 +178,7 @@ def UseFloat32(self): **(API Extension)** ''' - self._check_for_error(self._lib.LoadShapes_UseFloat32()) + self._lib.LoadShapes_UseFloat32() def UseFloat64(self): ''' @@ -193,4 +187,4 @@ def UseFloat64(self): **(API Extension)** ''' - self._check_for_error(self._lib.LoadShapes_UseFloat64()) + self._lib.LoadShapes_UseFloat64() diff --git a/dss/ILoads.py b/dss/ILoads.py index 4a855605..eb77fe41 100644 --- a/dss/ILoads.py +++ b/dss/ILoads.py @@ -58,11 +58,11 @@ def AllocationFactor(self) -> float: Original COM help: https://opendss.epri.com/AllocationFactor.html ''' - return self._check_for_error(self._lib.Loads_Get_AllocationFactor()) + return self._lib.Loads_Get_AllocationFactor() @AllocationFactor.setter def AllocationFactor(self, Value: float): - self._check_for_error(self._lib.Loads_Set_AllocationFactor(Value)) + self._lib.Loads_Set_AllocationFactor(Value) @property def CVRcurve(self) -> str: @@ -71,14 +71,11 @@ def CVRcurve(self) -> str: Original COM help: https://opendss.epri.com/CVRcurve.html ''' - return self._get_string(self._check_for_error(self._lib.Loads_Get_CVRcurve())) + return self._lib.Loads_Get_CVRcurve() @CVRcurve.setter def CVRcurve(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Loads_Set_CVRcurve(Value)) + self._lib.Loads_Set_CVRcurve(Value) @property def CVRvars(self) -> float: @@ -87,11 +84,11 @@ def CVRvars(self) -> float: Original COM help: https://opendss.epri.com/CVRvars.html ''' - return self._check_for_error(self._lib.Loads_Get_CVRvars()) + return self._lib.Loads_Get_CVRvars() @CVRvars.setter def CVRvars(self, Value: float): - self._check_for_error(self._lib.Loads_Set_CVRvars(Value)) + self._lib.Loads_Set_CVRvars(Value) @property def CVRwatts(self) -> float: @@ -100,11 +97,11 @@ def CVRwatts(self) -> float: Original COM help: https://opendss.epri.com/CVRwatts.html ''' - return self._check_for_error(self._lib.Loads_Get_CVRwatts()) + return self._lib.Loads_Get_CVRwatts() @CVRwatts.setter def CVRwatts(self, Value: float): - self._check_for_error(self._lib.Loads_Set_CVRwatts(Value)) + self._lib.Loads_Set_CVRwatts(Value) @property def Cfactor(self) -> float: @@ -113,11 +110,11 @@ def Cfactor(self) -> float: Original COM help: https://opendss.epri.com/Cfactor.html ''' - return self._check_for_error(self._lib.Loads_Get_Cfactor()) + return self._lib.Loads_Get_Cfactor() @Cfactor.setter def Cfactor(self, Value: float): - self._check_for_error(self._lib.Loads_Set_Cfactor(Value)) + self._lib.Loads_Set_Cfactor(Value) @property def Class(self) -> int: @@ -126,11 +123,11 @@ def Class(self) -> int: Original COM help: https://opendss.epri.com/Class.html ''' - return self._check_for_error(self._lib.Loads_Get_Class_()) + return self._lib.Loads_Get_Class_() @Class.setter def Class(self, Value: int): - self._check_for_error(self._lib.Loads_Set_Class_(Value)) + self._lib.Loads_Set_Class_(Value) @property def Growth(self) -> str: @@ -139,14 +136,11 @@ def Growth(self) -> str: Original COM help: https://opendss.epri.com/Growth.html ''' - return self._get_string(self._check_for_error(self._lib.Loads_Get_Growth())) + return self._lib.Loads_Get_Growth() @Growth.setter def Growth(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Loads_Set_Growth(Value)) + self._lib.Loads_Set_Growth(Value) @property def IsDelta(self) -> bool: @@ -155,11 +149,11 @@ def IsDelta(self) -> bool: Original COM help: https://opendss.epri.com/IsDelta1.html ''' - return self._check_for_error(self._lib.Loads_Get_IsDelta()) != 0 + return self._lib.Loads_Get_IsDelta() @IsDelta.setter def IsDelta(self, Value: bool): - self._check_for_error(self._lib.Loads_Set_IsDelta(Value)) + self._lib.Loads_Set_IsDelta(Value) @property def Model(self) -> LoadModels: @@ -168,11 +162,11 @@ def Model(self) -> LoadModels: Original COM help: https://opendss.epri.com/Model1.html ''' - return self._check_for_error(LoadModels(self._lib.Loads_Get_Model())) + return LoadModels(self._lib.Loads_Get_Model()) @Model.setter def Model(self, Value: Union[int, LoadModels]): - self._check_for_error(self._lib.Loads_Set_Model(Value)) + self._lib.Loads_Set_Model(Value) @property def NumCust(self) -> int: @@ -181,11 +175,11 @@ def NumCust(self) -> int: Original COM help: https://opendss.epri.com/NumCust1.html ''' - return self._check_for_error(self._lib.Loads_Get_NumCust()) + return self._lib.Loads_Get_NumCust() @NumCust.setter def NumCust(self, Value: int): - self._check_for_error(self._lib.Loads_Set_NumCust(Value)) + self._lib.Loads_Set_NumCust(Value) @property def PF(self) -> float: @@ -194,11 +188,11 @@ def PF(self) -> float: Original COM help: https://opendss.epri.com/PF1.html ''' - return self._check_for_error(self._lib.Loads_Get_PF()) + return self._lib.Loads_Get_PF() @PF.setter def PF(self, Value: float): - self._check_for_error(self._lib.Loads_Set_PF(Value)) + self._lib.Loads_Set_PF(Value) @property def PctMean(self) -> float: @@ -207,11 +201,11 @@ def PctMean(self) -> float: Original COM help: https://opendss.epri.com/PctMean.html ''' - return self._check_for_error(self._lib.Loads_Get_PctMean()) + return self._lib.Loads_Get_PctMean() @PctMean.setter def PctMean(self, Value: float): - self._check_for_error(self._lib.Loads_Set_PctMean(Value)) + self._lib.Loads_Set_PctMean(Value) @property def PctStdDev(self) -> float: @@ -220,11 +214,11 @@ def PctStdDev(self) -> float: Original COM help: https://opendss.epri.com/PctStdDev.html ''' - return self._check_for_error(self._lib.Loads_Get_PctStdDev()) + return self._lib.Loads_Get_PctStdDev() @PctStdDev.setter def PctStdDev(self, Value: float): - self._check_for_error(self._lib.Loads_Set_PctStdDev(Value)) + self._lib.Loads_Set_PctStdDev(Value) @property def RelWeight(self) -> float: @@ -233,11 +227,11 @@ def RelWeight(self) -> float: Original COM help: https://opendss.epri.com/RelWeight.html ''' - return self._check_for_error(self._lib.Loads_Get_RelWeight()) + return self._lib.Loads_Get_RelWeight() @RelWeight.setter def RelWeight(self, Value: float): - self._check_for_error(self._lib.Loads_Set_RelWeight(Value)) + self._lib.Loads_Set_RelWeight(Value) @property def Rneut(self) -> float: @@ -246,11 +240,11 @@ def Rneut(self) -> float: Original COM help: https://opendss.epri.com/Rneut.html ''' - return self._check_for_error(self._lib.Loads_Get_Rneut()) + return self._lib.Loads_Get_Rneut() @Rneut.setter def Rneut(self, Value: float): - self._check_for_error(self._lib.Loads_Set_Rneut(Value)) + self._lib.Loads_Set_Rneut(Value) @property def Spectrum(self) -> str: @@ -259,14 +253,11 @@ def Spectrum(self) -> str: Original COM help: https://opendss.epri.com/Spectrum.html ''' - return self._get_string(self._check_for_error(self._lib.Loads_Get_Spectrum())) + return self._lib.Loads_Get_Spectrum() @Spectrum.setter def Spectrum(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Loads_Set_Spectrum(Value)) + self._lib.Loads_Set_Spectrum(Value) @property def Status(self) -> LoadStatus: @@ -275,11 +266,11 @@ def Status(self) -> LoadStatus: Original COM help: https://opendss.epri.com/Status.html ''' - return LoadStatus(self._check_for_error(self._lib.Loads_Get_Status())) + return LoadStatus(self._lib.Loads_Get_Status()) @Status.setter def Status(self, Value: Union[int, LoadStatus]): - self._check_for_error(self._lib.Loads_Set_Status(Value)) + self._lib.Loads_Set_Status(Value) @property def Vmaxpu(self) -> float: @@ -288,11 +279,11 @@ def Vmaxpu(self) -> float: Original COM help: https://opendss.epri.com/Vmaxpu1.html ''' - return self._check_for_error(self._lib.Loads_Get_Vmaxpu()) + return self._lib.Loads_Get_Vmaxpu() @Vmaxpu.setter def Vmaxpu(self, Value: float): - self._check_for_error(self._lib.Loads_Set_Vmaxpu(Value)) + self._lib.Loads_Set_Vmaxpu(Value) @property def Vminemerg(self) -> float: @@ -301,11 +292,11 @@ def Vminemerg(self) -> float: Original COM help: https://opendss.epri.com/Vminemerg.html ''' - return self._check_for_error(self._lib.Loads_Get_Vminemerg()) + return self._lib.Loads_Get_Vminemerg() @Vminemerg.setter def Vminemerg(self, Value: float): - self._check_for_error(self._lib.Loads_Set_Vminemerg(Value)) + self._lib.Loads_Set_Vminemerg(Value) @property def Vminnorm(self) -> float: @@ -314,11 +305,11 @@ def Vminnorm(self) -> float: Original COM help: https://opendss.epri.com/Vminnorm.html ''' - return self._check_for_error(self._lib.Loads_Get_Vminnorm()) + return self._lib.Loads_Get_Vminnorm() @Vminnorm.setter def Vminnorm(self, Value: float): - self._check_for_error(self._lib.Loads_Set_Vminnorm(Value)) + self._lib.Loads_Set_Vminnorm(Value) @property def Vminpu(self) -> float: @@ -327,11 +318,11 @@ def Vminpu(self) -> float: Original COM help: https://opendss.epri.com/Vminpu1.html ''' - return self._check_for_error(self._lib.Loads_Get_Vminpu()) + return self._lib.Loads_Get_Vminpu() @Vminpu.setter def Vminpu(self, Value: float): - self._check_for_error(self._lib.Loads_Set_Vminpu(Value)) + self._lib.Loads_Set_Vminpu(Value) @property def Xneut(self) -> float: @@ -340,11 +331,11 @@ def Xneut(self) -> float: Original COM help: https://opendss.epri.com/Xneut.html ''' - return self._check_for_error(self._lib.Loads_Get_Xneut()) + return self._lib.Loads_Get_Xneut() @Xneut.setter def Xneut(self, Value: float): - self._check_for_error(self._lib.Loads_Set_Xneut(Value)) + self._lib.Loads_Set_Xneut(Value) @property def Yearly(self) -> str: @@ -353,14 +344,11 @@ def Yearly(self) -> str: Original COM help: https://opendss.epri.com/Yearly.html ''' - return self._get_string(self._check_for_error(self._lib.Loads_Get_Yearly())) + return self._lib.Loads_Get_Yearly() @Yearly.setter def Yearly(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Loads_Set_Yearly(Value)) + self._lib.Loads_Set_Yearly(Value) @property def ZIPV(self) -> Float64Array: @@ -369,13 +357,12 @@ def ZIPV(self) -> Float64Array: Original COM help: https://opendss.epri.com/ZIPV.html ''' - self._check_for_error(self._lib.Loads_Get_ZIPV_GR()) - return self._get_float64_gr_array() + return self._lib.Loads_Get_ZIPV_GR() @ZIPV.setter def ZIPV(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Loads_Set_ZIPV(ValuePtr, ValueCount)) + self._lib.Loads_Set_ZIPV(ValuePtr, ValueCount) @property def daily(self) -> str: @@ -384,14 +371,11 @@ def daily(self) -> str: Original COM help: https://opendss.epri.com/daily.html ''' - return self._get_string(self._check_for_error(self._lib.Loads_Get_daily())) + return self._lib.Loads_Get_daily() @daily.setter def daily(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Loads_Set_daily(Value)) + self._lib.Loads_Set_daily(Value) @property def duty(self) -> str: @@ -400,14 +384,11 @@ def duty(self) -> str: Original COM help: https://opendss.epri.com/duty.html ''' - return self._get_string(self._check_for_error(self._lib.Loads_Get_duty())) + return self._lib.Loads_Get_duty() @duty.setter def duty(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Loads_Set_duty(Value)) + self._lib.Loads_Set_duty(Value) @property def kV(self) -> float: @@ -416,11 +397,11 @@ def kV(self) -> float: Original COM help: https://opendss.epri.com/kV2.html ''' - return self._check_for_error(self._lib.Loads_Get_kV()) + return self._lib.Loads_Get_kV() @kV.setter def kV(self, Value: float): - self._check_for_error(self._lib.Loads_Set_kV(Value)) + self._lib.Loads_Set_kV(Value) @property def kW(self) -> float: @@ -429,11 +410,11 @@ def kW(self) -> float: Original COM help: https://opendss.epri.com/kW1.html ''' - return self._check_for_error(self._lib.Loads_Get_kW()) + return self._lib.Loads_Get_kW() @kW.setter def kW(self, Value: float): - self._check_for_error(self._lib.Loads_Set_kW(Value)) + self._lib.Loads_Set_kW(Value) @property def kva(self) -> float: @@ -442,11 +423,11 @@ def kva(self) -> float: Original COM help: https://opendss.epri.com/kva.html ''' - return self._check_for_error(self._lib.Loads_Get_kva()) + return self._lib.Loads_Get_kva() @kva.setter def kva(self, Value: float): - self._check_for_error(self._lib.Loads_Set_kva(Value)) + self._lib.Loads_Set_kva(Value) @property def kvar(self) -> float: @@ -455,11 +436,11 @@ def kvar(self) -> float: Original COM help: https://opendss.epri.com/kvar1.html ''' - return self._check_for_error(self._lib.Loads_Get_kvar()) + return self._lib.Loads_Get_kvar() @kvar.setter def kvar(self, Value: float): - self._check_for_error(self._lib.Loads_Set_kvar(Value)) + self._lib.Loads_Set_kvar(Value) @property def kwh(self) -> float: @@ -468,11 +449,11 @@ def kwh(self) -> float: Original COM help: https://opendss.epri.com/kwh.html ''' - return self._check_for_error(self._lib.Loads_Get_kwh()) + return self._lib.Loads_Get_kwh() @kwh.setter def kwh(self, Value: float): - self._check_for_error(self._lib.Loads_Set_kwh(Value)) + self._lib.Loads_Set_kwh(Value) @property def kwhdays(self) -> float: @@ -481,11 +462,11 @@ def kwhdays(self) -> float: Original COM help: https://opendss.epri.com/kwhdays.html ''' - return self._check_for_error(self._lib.Loads_Get_kwhdays()) + return self._lib.Loads_Get_kwhdays() @kwhdays.setter def kwhdays(self, Value: float): - self._check_for_error(self._lib.Loads_Set_kwhdays(Value)) + self._lib.Loads_Set_kwhdays(Value) @property def pctSeriesRL(self) -> float: @@ -494,11 +475,11 @@ def pctSeriesRL(self) -> float: Original COM help: https://opendss.epri.com/pctSeriesRL.html ''' - return self._check_for_error(self._lib.Loads_Get_pctSeriesRL()) + return self._lib.Loads_Get_pctSeriesRL() @pctSeriesRL.setter def pctSeriesRL(self, Value: float): - self._check_for_error(self._lib.Loads_Set_pctSeriesRL(Value)) + self._lib.Loads_Set_pctSeriesRL(Value) @property def xfkVA(self) -> float: @@ -507,11 +488,11 @@ def xfkVA(self) -> float: Original COM help: https://opendss.epri.com/xfkVA.html ''' - return self._check_for_error(self._lib.Loads_Get_xfkVA()) + return self._lib.Loads_Get_xfkVA() @xfkVA.setter def xfkVA(self, Value: float): - self._check_for_error(self._lib.Loads_Set_xfkVA(Value)) + self._lib.Loads_Set_xfkVA(Value) @property def Sensor(self) -> str: @@ -520,7 +501,7 @@ def Sensor(self) -> str: Original COM help: https://opendss.epri.com/Sensor.html ''' - return self._get_string(self._check_for_error(self._lib.Loads_Get_Sensor())) + return self._lib.Loads_Get_Sensor() # API extensions @property @@ -530,8 +511,8 @@ def Phases(self) -> int: **(API Extension)** ''' - return self._check_for_error(self._lib.Loads_Get_Phases()) + return self._lib.Loads_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.Loads_Set_Phases(Value)) + self._lib.Loads_Set_Phases(Value) diff --git a/dss/IMeters.py b/dss/IMeters.py index 6d3b3bdf..8ac7212f 100644 --- a/dss/IMeters.py +++ b/dss/IMeters.py @@ -47,7 +47,7 @@ def CloseAllDIFiles(self): Original COM help: https://opendss.epri.com/CloseAllDIFiles.html ''' - self._check_for_error(self._lib.Meters_CloseAllDIFiles()) + self._lib.Meters_CloseAllDIFiles() def DoReliabilityCalc(self, AssumeRestoration: bool): ''' @@ -55,7 +55,7 @@ def DoReliabilityCalc(self, AssumeRestoration: bool): Original COM help: https://opendss.epri.com/DoReliabilityCalc.html ''' - self._check_for_error(self._lib.Meters_DoReliabilityCalc(AssumeRestoration)) + self._lib.Meters_DoReliabilityCalc(AssumeRestoration) def OpenAllDIFiles(self): ''' @@ -63,7 +63,7 @@ def OpenAllDIFiles(self): Original COM help: https://opendss.epri.com/OpenAllDIFiles.html ''' - self._check_for_error(self._lib.Meters_OpenAllDIFiles()) + self._lib.Meters_OpenAllDIFiles() def Reset(self): ''' @@ -71,7 +71,7 @@ def Reset(self): Original COM help: https://opendss.epri.com/Reset2.html ''' - self._check_for_error(self._lib.Meters_Reset()) + self._lib.Meters_Reset() def ResetAll(self): ''' @@ -79,7 +79,7 @@ def ResetAll(self): Original COM help: https://opendss.epri.com/ResetAll.html ''' - self._check_for_error(self._lib.Meters_ResetAll()) + self._lib.Meters_ResetAll() def Sample(self): ''' @@ -87,7 +87,7 @@ def Sample(self): Original COM help: https://opendss.epri.com/Sample1.html ''' - self._check_for_error(self._lib.Meters_Sample()) + self._lib.Meters_Sample() def SampleAll(self): ''' @@ -95,7 +95,7 @@ def SampleAll(self): Original COM help: https://opendss.epri.com/SampleAll.html ''' - self._check_for_error(self._lib.Meters_SampleAll()) + self._lib.Meters_SampleAll() def Save(self): ''' @@ -103,7 +103,7 @@ def Save(self): Original COM help: https://opendss.epri.com/Save.html ''' - self._check_for_error(self._lib.Meters_Save()) + self._lib.Meters_Save() def SaveAll(self): ''' @@ -111,10 +111,10 @@ def SaveAll(self): Original COM help: https://opendss.epri.com/SaveAll.html ''' - self._check_for_error(self._lib.Meters_SaveAll()) + self._lib.Meters_SaveAll() def SetActiveSection(self, SectIdx: int): - self._check_for_error(self._lib.Meters_SetActiveSection(SectIdx)) + self._lib.Meters_SetActiveSection(SectIdx) @property def AllBranchesInZone(self) -> List[str]: @@ -123,7 +123,7 @@ def AllBranchesInZone(self) -> List[str]: Original COM help: https://opendss.epri.com/AllBranchesInZone.html ''' - return self._check_for_error(self._get_string_array(self._lib.Meters_Get_AllBranchesInZone)) + return self._lib.Meters_Get_AllBranchesInZone() @property def AllEndElements(self) -> List[str]: @@ -132,7 +132,7 @@ def AllEndElements(self) -> List[str]: Original COM help: https://opendss.epri.com/AllEndElements.html ''' - return self._check_for_error(self._get_string_array(self._lib.Meters_Get_AllEndElements)) + return self._lib.Meters_Get_AllEndElements() @property def AllocFactors(self) -> Float64Array: @@ -141,13 +141,12 @@ def AllocFactors(self) -> Float64Array: Original COM help: https://opendss.epri.com/AllocFactors.html ''' - self._check_for_error(self._lib.Meters_Get_AllocFactors_GR()) - return self._get_float64_gr_array() + return self._lib.Meters_Get_AllocFactors_GR() @AllocFactors.setter def AllocFactors(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Meters_Set_AllocFactors(ValuePtr, ValueCount)) + self._lib.Meters_Set_AllocFactors(ValuePtr, ValueCount) @property def AvgRepairTime(self) -> float: @@ -156,7 +155,7 @@ def AvgRepairTime(self) -> float: Original COM help: https://opendss.epri.com/AvgRepairTime.html ''' - return self._check_for_error(self._lib.Meters_Get_AvgRepairTime()) + return self._lib.Meters_Get_AvgRepairTime() @property def CalcCurrent(self) -> Float64Array: @@ -165,13 +164,12 @@ def CalcCurrent(self) -> Float64Array: Original COM help: https://opendss.epri.com/CalcCurrent.html ''' - self._check_for_error(self._lib.Meters_Get_CalcCurrent_GR()) - return self._get_float64_gr_array() + return self._lib.Meters_Get_CalcCurrent_GR() @CalcCurrent.setter def CalcCurrent(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Meters_Set_CalcCurrent(ValuePtr, ValueCount)) + self._lib.Meters_Set_CalcCurrent(ValuePtr, ValueCount) @property def CountBranches(self) -> int: @@ -180,7 +178,7 @@ def CountBranches(self) -> int: Original COM help: https://opendss.epri.com/CountBranches.html ''' - return self._check_for_error(self._lib.Meters_Get_CountBranches()) + return self._lib.Meters_Get_CountBranches() @property def CountEndElements(self) -> int: @@ -189,7 +187,7 @@ def CountEndElements(self) -> int: Original COM help: https://opendss.epri.com/CountEndElements.html ''' - return self._check_for_error(self._lib.Meters_Get_CountEndElements()) + return self._lib.Meters_Get_CountEndElements() @property def CustInterrupts(self) -> float: @@ -198,7 +196,7 @@ def CustInterrupts(self) -> float: Original COM help: https://opendss.epri.com/CustInterrupts.html ''' - return self._check_for_error(self._lib.Meters_Get_CustInterrupts()) + return self._lib.Meters_Get_CustInterrupts() @property def DIFilesAreOpen(self) -> bool: @@ -207,7 +205,7 @@ def DIFilesAreOpen(self) -> bool: Original COM help: https://opendss.epri.com/DIFilesAreOpen.html ''' - return self._check_for_error(self._lib.Meters_Get_DIFilesAreOpen()) != 0 + return self._lib.Meters_Get_DIFilesAreOpen() @property def FaultRateXRepairHrs(self) -> float: @@ -216,7 +214,7 @@ def FaultRateXRepairHrs(self) -> float: Original COM help: https://opendss.epri.com/FaultRateXRepairHrs.html ''' - return self._check_for_error(self._lib.Meters_Get_FaultRateXRepairHrs()) + return self._lib.Meters_Get_FaultRateXRepairHrs() @property def MeteredElement(self) -> str: @@ -225,14 +223,11 @@ def MeteredElement(self) -> str: Original COM help: https://opendss.epri.com/MeteredElement.html ''' - return self._get_string(self._check_for_error(self._lib.Meters_Get_MeteredElement())) + return self._lib.Meters_Get_MeteredElement() @MeteredElement.setter def MeteredElement(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Meters_Set_MeteredElement(Value)) + self._lib.Meters_Set_MeteredElement(Value) @property def MeteredTerminal(self) -> int: @@ -241,11 +236,11 @@ def MeteredTerminal(self) -> int: Original COM help: https://opendss.epri.com/MeteredTerminal.html ''' - return self._check_for_error(self._lib.Meters_Get_MeteredTerminal()) + return self._lib.Meters_Get_MeteredTerminal() @MeteredTerminal.setter def MeteredTerminal(self, Value: int): - self._check_for_error(self._lib.Meters_Set_MeteredTerminal(Value)) + self._lib.Meters_Set_MeteredTerminal(Value) @property def NumSectionBranches(self) -> int: @@ -254,7 +249,7 @@ def NumSectionBranches(self) -> int: Original COM help: https://opendss.epri.com/NumSectionBranches.html ''' - return self._check_for_error(self._lib.Meters_Get_NumSectionBranches()) + return self._lib.Meters_Get_NumSectionBranches() @property def NumSectionCustomers(self) -> int: @@ -263,7 +258,7 @@ def NumSectionCustomers(self) -> int: Original COM help: https://opendss.epri.com/NumSectionCustomers.html ''' - return self._check_for_error(self._lib.Meters_Get_NumSectionCustomers()) + return self._lib.Meters_Get_NumSectionCustomers() @property def NumSections(self) -> int: @@ -272,7 +267,7 @@ def NumSections(self) -> int: Original COM help: https://opendss.epri.com/NumSections.html ''' - return self._check_for_error(self._lib.Meters_Get_NumSections()) + return self._lib.Meters_Get_NumSections() @property def OCPDeviceType(self) -> OCPDevTypeEnum: @@ -281,7 +276,7 @@ def OCPDeviceType(self) -> OCPDevTypeEnum: Original COM help: https://opendss.epri.com/OCPDeviceType.html ''' - return OCPDevTypeEnum(self._check_for_error(self._lib.Meters_Get_OCPDeviceType())) + return OCPDevTypeEnum(self._lib.Meters_Get_OCPDeviceType()) @property def Peakcurrent(self) -> Float64Array: @@ -290,13 +285,12 @@ def Peakcurrent(self) -> Float64Array: Original COM help: https://opendss.epri.com/Peakcurrent.html ''' - self._check_for_error(self._lib.Meters_Get_Peakcurrent_GR()) - return self._get_float64_gr_array() + return self._lib.Meters_Get_Peakcurrent_GR() @Peakcurrent.setter def Peakcurrent(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Meters_Set_Peakcurrent(ValuePtr, ValueCount)) + self._lib.Meters_Set_Peakcurrent(ValuePtr, ValueCount) @property def RegisterNames(self) -> List[str]: @@ -309,7 +303,7 @@ def RegisterNames(self) -> List[str]: Original COM help: https://opendss.epri.com/RegisterNames1.html ''' - return self._check_for_error(self._get_string_array(self._lib.Meters_Get_RegisterNames)) + return self._lib.Meters_Get_RegisterNames() @property def RegisterValues(self) -> Float64Array: @@ -318,8 +312,7 @@ def RegisterValues(self) -> Float64Array: Original COM help: https://opendss.epri.com/RegisterValues1.html ''' - self._check_for_error(self._lib.Meters_Get_RegisterValues_GR()) - return self._get_float64_gr_array() + return self._lib.Meters_Get_RegisterValues_GR() @property def SAIDI(self) -> float: @@ -328,7 +321,7 @@ def SAIDI(self) -> float: Original COM help: https://opendss.epri.com/SAIDI.html ''' - return self._check_for_error(self._lib.Meters_Get_SAIDI()) + return self._lib.Meters_Get_SAIDI() @property def SAIFI(self) -> float: @@ -337,7 +330,7 @@ def SAIFI(self) -> float: Original COM help: https://opendss.epri.com/SAIFI.html ''' - return self._check_for_error(self._lib.Meters_Get_SAIFI()) + return self._lib.Meters_Get_SAIFI() @property def SAIFIKW(self) -> float: @@ -346,7 +339,7 @@ def SAIFIKW(self) -> float: Original COM help: https://opendss.epri.com/SAIFIKW.html ''' - return self._check_for_error(self._lib.Meters_Get_SAIFIKW()) + return self._lib.Meters_Get_SAIFIKW() @property def SectSeqIdx(self) -> int: @@ -355,7 +348,7 @@ def SectSeqIdx(self) -> int: Original COM help: https://opendss.epri.com/SectSeqIdx.html ''' - return self._check_for_error(self._lib.Meters_Get_SectSeqIdx()) + return self._lib.Meters_Get_SectSeqIdx() @property def SectTotalCust(self) -> int: @@ -364,7 +357,7 @@ def SectTotalCust(self) -> int: Original COM help: https://opendss.epri.com/SectTotalCust.html ''' - return self._check_for_error(self._lib.Meters_Get_SectTotalCust()) + return self._lib.Meters_Get_SectTotalCust() @property def SeqListSize(self) -> int: @@ -373,7 +366,7 @@ def SeqListSize(self) -> int: Original COM help: https://opendss.epri.com/SeqListSize.html ''' - return self._check_for_error(self._lib.Meters_Get_SeqListSize()) + return self._lib.Meters_Get_SeqListSize() @property def SequenceIndex(self) -> int: @@ -383,11 +376,11 @@ def SequenceIndex(self) -> int: Original COM help: https://opendss.epri.com/SequenceIndex.html ''' - return self._check_for_error(self._lib.Meters_Get_SequenceIndex()) + return self._lib.Meters_Get_SequenceIndex() @SequenceIndex.setter def SequenceIndex(self, Value: int): - self._check_for_error(self._lib.Meters_Set_SequenceIndex(Value)) + self._lib.Meters_Set_SequenceIndex(Value) @property def SumBranchFltRates(self) -> float: @@ -396,7 +389,7 @@ def SumBranchFltRates(self) -> float: Original COM help: https://opendss.epri.com/SumBranchFltRates.html ''' - return self._check_for_error(self._lib.Meters_Get_SumBranchFltRates()) + return self._lib.Meters_Get_SumBranchFltRates() @property def TotalCustomers(self) -> int: @@ -405,7 +398,7 @@ def TotalCustomers(self) -> int: Original COM help: https://opendss.epri.com/TotalCustomers.html ''' - return self._check_for_error(self._lib.Meters_Get_TotalCustomers()) + return self._lib.Meters_Get_TotalCustomers() @property def Totals(self) -> Float64Array: @@ -414,8 +407,7 @@ def Totals(self) -> Float64Array: Original COM help: https://opendss.epri.com/Totals.html ''' - self._check_for_error(self._lib.Meters_Get_Totals_GR()) - return self._get_float64_gr_array() + return self._lib.Meters_Get_Totals_GR() @property def ZonePCE(self) -> List[str]: @@ -424,7 +416,7 @@ def ZonePCE(self) -> List[str]: Original COM help: https://opendss.epri.com/ZonePCE.html ''' - result = self._check_for_error(self._get_string_array(self._lib.Meters_Get_ZonePCE)) + result = self._lib.Meters_Get_ZonePCE() if not result: result = ['NONE'] #TODO: remove diff --git a/dss/IMonitors.py b/dss/IMonitors.py index 6b14f7f6..bc1cb6b8 100644 --- a/dss/IMonitors.py +++ b/dss/IMonitors.py @@ -34,7 +34,7 @@ def Channel(self, Index: int) -> Float32Array: Original COM help: https://opendss.epri.com/Channel.html ''' - num_channels = self._check_for_error(self._lib.Monitors_Get_NumChannels()) + num_channels = self._lib.Monitors_Get_NumChannels() if Index < 1 or Index > num_channels: raise DSSException( 0, @@ -43,8 +43,10 @@ def Channel(self, Index: int) -> Float32Array: )) ffi = self._api_util.ffi - self._check_for_error(self._lib.Monitors_Get_ByteStream_GR()) - ptr, cnt = self._api_util.gr_int8_pointers + api_util = self._api_util + api_util.lib_unpatched.Monitors_Get_ByteStream_GR() + api_util._check_for_error() + ptr, cnt = api_util.gr_int8_pointers cnt = cnt[0] if cnt == 272: return np.zeros((1,), dtype=np.float32) @@ -64,8 +66,10 @@ def AsMatrix(self) -> Float64Array: ''' ffi = self._api_util.ffi - self._check_for_error(self._lib.Monitors_Get_ByteStream_GR()) - ptr, cnt = self._api_util.gr_int8_pointers + api_util = self._api_util + api_util.lib_unpatched.Monitors_Get_ByteStream_GR() + api_util._check_for_error() + ptr, cnt = api_util.gr_int8_pointers cnt = cnt[0] if cnt == 272: return None #np.zeros((0,), dtype=np.float32) @@ -82,7 +86,7 @@ def Process(self): Original COM help: https://opendss.epri.com/Process.html ''' - self._check_for_error(self._lib.Monitors_Process()) + self._lib.Monitors_Process() def ProcessAll(self): ''' @@ -90,7 +94,7 @@ def ProcessAll(self): Original COM help: https://opendss.epri.com/ProcessAll.html ''' - self._check_for_error(self._lib.Monitors_ProcessAll()) + self._lib.Monitors_ProcessAll() def Reset(self): ''' @@ -98,7 +102,7 @@ def Reset(self): Original COM help: https://opendss.epri.com/Reset3.html ''' - self._check_for_error(self._lib.Monitors_Reset()) + self._lib.Monitors_Reset() def ResetAll(self): ''' @@ -106,7 +110,7 @@ def ResetAll(self): Original COM help: https://opendss.epri.com/ResetAll1.html ''' - self._check_for_error(self._lib.Monitors_ResetAll()) + self._lib.Monitors_ResetAll() def Sample(self): ''' @@ -114,7 +118,7 @@ def Sample(self): Original COM help: https://opendss.epri.com/Sample2.html ''' - self._check_for_error(self._lib.Monitors_Sample()) + self._lib.Monitors_Sample() def SampleAll(self): ''' @@ -122,7 +126,7 @@ def SampleAll(self): Original COM help: https://opendss.epri.com/SampleAll1.html ''' - self._check_for_error(self._lib.Monitors_SampleAll()) + self._lib.Monitors_SampleAll() def Save(self): ''' @@ -134,7 +138,7 @@ def Save(self): Original COM help: https://opendss.epri.com/Save1.html ''' - self._check_for_error(self._lib.Monitors_Save()) + self._lib.Monitors_Save() def SaveAll(self): ''' @@ -144,7 +148,7 @@ def SaveAll(self): Original COM help: https://opendss.epri.com/SaveAll1.html ''' - self._check_for_error(self._lib.Monitors_SaveAll()) + self._lib.Monitors_SaveAll() def Show(self): ''' @@ -152,7 +156,7 @@ def Show(self): Original COM help: https://opendss.epri.com/Show3.html ''' - self._check_for_error(self._lib.Monitors_Show()) + self._lib.Monitors_Show() @property def ByteStream(self) -> Int8Array: @@ -161,8 +165,7 @@ def ByteStream(self) -> Int8Array: Original COM help: https://opendss.epri.com/ByteStream.html ''' - self._check_for_error(self._lib.Monitors_Get_ByteStream_GR()) - return self._get_int8_gr_array() + return self._lib.Monitors_Get_ByteStream_GR() @property def Element(self) -> str: @@ -171,14 +174,11 @@ def Element(self) -> str: Original COM help: https://opendss.epri.com/Element.html ''' - return self._get_string(self._check_for_error(self._lib.Monitors_Get_Element())) + return self._lib.Monitors_Get_Element() @Element.setter def Element(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Monitors_Set_Element(Value)) + self._lib.Monitors_Set_Element(Value) @property def FileName(self) -> str: @@ -187,7 +187,7 @@ def FileName(self) -> str: Original COM help: https://opendss.epri.com/FileName.html ''' - return self._get_string(self._check_for_error(self._lib.Monitors_Get_FileName())) + return self._lib.Monitors_Get_FileName() @property def FileVersion(self) -> int: @@ -196,7 +196,7 @@ def FileVersion(self) -> int: Original COM help: https://opendss.epri.com/FileVersion.html ''' - return self._check_for_error(self._lib.Monitors_Get_FileVersion()) + return self._lib.Monitors_Get_FileVersion() @property def Header(self) -> List[str]: @@ -205,7 +205,7 @@ def Header(self) -> List[str]: Original COM help: https://opendss.epri.com/Header.html ''' - return self._check_for_error(self._get_string_array(self._lib.Monitors_Get_Header)) + return self._lib.Monitors_Get_Header() @property def Mode(self) -> int: @@ -214,11 +214,11 @@ def Mode(self) -> int: Original COM help: https://opendss.epri.com/Mode1.html ''' - return self._check_for_error(self._lib.Monitors_Get_Mode()) # TODO: expose this better + return self._lib.Monitors_Get_Mode() # TODO: expose this better @Mode.setter def Mode(self, Value: int): - self._check_for_error(self._lib.Monitors_Set_Mode(Value)) + self._lib.Monitors_Set_Mode(Value) @property def NumChannels(self) -> int: @@ -227,7 +227,7 @@ def NumChannels(self) -> int: Original COM help: https://opendss.epri.com/NumChannels.html ''' - return self._check_for_error(self._lib.Monitors_Get_NumChannels()) + return self._lib.Monitors_Get_NumChannels() @property def RecordSize(self) -> int: @@ -236,7 +236,7 @@ def RecordSize(self) -> int: Original COM help: https://opendss.epri.com/RecordSize.html ''' - return self._check_for_error(self._lib.Monitors_Get_RecordSize()) + return self._lib.Monitors_Get_RecordSize() @property def SampleCount(self) -> int: @@ -245,7 +245,7 @@ def SampleCount(self) -> int: Original COM help: https://opendss.epri.com/SampleCount.html ''' - return self._check_for_error(self._lib.Monitors_Get_SampleCount()) + return self._lib.Monitors_Get_SampleCount() @property def Terminal(self) -> int: @@ -254,11 +254,11 @@ def Terminal(self) -> int: Original COM help: https://opendss.epri.com/Terminal.html ''' - return self._check_for_error(self._lib.Monitors_Get_Terminal()) + return self._lib.Monitors_Get_Terminal() @Terminal.setter def Terminal(self, Value: int): - self._check_for_error(self._lib.Monitors_Set_Terminal(Value)) + self._lib.Monitors_Set_Terminal(Value) @property def dblFreq(self) -> Float64Array: @@ -267,8 +267,7 @@ def dblFreq(self) -> Float64Array: Original COM help: https://opendss.epri.com/dblFreq.html ''' - self._check_for_error(self._lib.Monitors_Get_dblFreq_GR()) - return self._get_float64_gr_array() + return self._lib.Monitors_Get_dblFreq_GR() @property def dblHour(self) -> Float64Array: @@ -277,5 +276,4 @@ def dblHour(self) -> Float64Array: Original COM help: https://opendss.epri.com/dblHour.html ''' - self._check_for_error(self._lib.Monitors_Get_dblHour_GR()) - return self._get_float64_gr_array() + return self._lib.Monitors_Get_dblHour_GR() diff --git a/dss/IPDElements.py b/dss/IPDElements.py index 2bb32178..4f7797e3 100644 --- a/dss/IPDElements.py +++ b/dss/IPDElements.py @@ -34,7 +34,7 @@ def AccumulatedL(self) -> float: Original COM help: https://opendss.epri.com/AccumulatedL.html ''' - return self._check_for_error(self._lib.PDElements_Get_AccumulatedL()) + return self._lib.PDElements_Get_AccumulatedL() @property def Count(self) -> int: @@ -43,10 +43,10 @@ def Count(self) -> int: Original COM help: https://opendss.epri.com/Count12.html ''' - return self._check_for_error(self._lib.PDElements_Get_Count()) + return self._lib.PDElements_Get_Count() def __len__(self) -> int: - return self._check_for_error(self._lib.PDElements_Get_Count()) + return self._lib.PDElements_Get_Count() @property def FaultRate(self) -> float: @@ -54,11 +54,11 @@ def FaultRate(self) -> float: Get/Set Number of failures per year. For LINE elements: Number of failures per unit length per year. ''' - return self._check_for_error(self._lib.PDElements_Get_FaultRate()) + return self._lib.PDElements_Get_FaultRate() @FaultRate.setter def FaultRate(self, Value: float): - self._check_for_error(self._lib.PDElements_Set_FaultRate(Value)) + self._lib.PDElements_Set_FaultRate(Value) @property def First(self) -> int: @@ -66,7 +66,7 @@ def First(self) -> int: (read-only) Set the first enabled PD element to be the active element. Returns 0 if none found. ''' - return self._check_for_error(self._lib.PDElements_Get_First()) + return self._lib.PDElements_Get_First() @property def FromTerminal(self) -> int: @@ -76,7 +76,7 @@ def FromTerminal(self) -> int: *Requires an energy meter with an updated zone.* ''' - return self._check_for_error(self._lib.PDElements_Get_FromTerminal()) + return self._lib.PDElements_Get_FromTerminal() @property def IsShunt(self) -> bool: @@ -85,7 +85,7 @@ def IsShunt(self) -> bool: element rather than a series element. Applies to Capacitor and Reactor elements in particular. ''' - return self._check_for_error(self._lib.PDElements_Get_IsShunt()) != 0 + return self._lib.PDElements_Get_IsShunt() @property def Lambda(self) -> float: @@ -96,7 +96,7 @@ def Lambda(self) -> float: Original COM help: https://opendss.epri.com/Lambda1.html ''' - return self._check_for_error(self._lib.PDElements_Get_Lambda()) + return self._lib.PDElements_Get_Lambda() @property def Name(self) -> str: @@ -104,14 +104,11 @@ def Name(self) -> str: Get/Set name of active PD Element. Returns null string if active element is not PDElement type. ''' - return self._get_string(self._check_for_error(self._lib.PDElements_Get_Name())) + return self._lib.PDElements_Get_Name() @Name.setter def Name(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.PDElements_Set_Name(Value)) + self._lib.PDElements_Set_Name(Value) @property def Next(self) -> int: @@ -119,7 +116,7 @@ def Next(self) -> int: (read-only) Advance to the next PD element in the circuit. Enabled elements only. Returns 0 when no more elements. ''' - return self._check_for_error(self._lib.PDElements_Get_Next()) + return self._lib.PDElements_Get_Next() @property def Numcustomers(self) -> int: @@ -130,7 +127,7 @@ def Numcustomers(self) -> int: Original COM help: https://opendss.epri.com/Numcustomers.html ''' - return self._check_for_error(self._lib.PDElements_Get_Numcustomers()) + return self._lib.PDElements_Get_Numcustomers() @property def ParentPDElement(self) -> int: @@ -140,7 +137,7 @@ def ParentPDElement(self) -> int: *Requires an energy meter with an updated zone.* ''' - return self._check_for_error(self._lib.PDElements_Get_ParentPDElement()) + return self._lib.PDElements_Get_ParentPDElement() @property def RepairTime(self) -> float: @@ -149,11 +146,11 @@ def RepairTime(self) -> float: Original COM help: https://opendss.epri.com/RepairTime.html ''' - return self._check_for_error(self._lib.PDElements_Get_RepairTime()) + return self._lib.PDElements_Get_RepairTime() @RepairTime.setter def RepairTime(self, Value: float): - self._check_for_error(self._lib.PDElements_Set_RepairTime(Value)) + self._lib.PDElements_Set_RepairTime(Value) @property def SectionID(self) -> int: @@ -164,7 +161,7 @@ def SectionID(self) -> int: Original COM help: https://opendss.epri.com/SectionID1.html ''' - return self._check_for_error(self._lib.PDElements_Get_SectionID()) + return self._lib.PDElements_Get_SectionID() @property def TotalMiles(self) -> float: @@ -175,7 +172,7 @@ def TotalMiles(self) -> float: Original COM help: https://opendss.epri.com/TotalMiles1.html ''' - return self._check_for_error(self._lib.PDElements_Get_TotalMiles()) + return self._lib.PDElements_Get_TotalMiles() @property def Totalcustomers(self) -> int: @@ -186,7 +183,7 @@ def Totalcustomers(self) -> int: Original COM help: https://opendss.epri.com/TotalCustomers1.html ''' - return self._check_for_error(self._lib.PDElements_Get_Totalcustomers()) + return self._lib.PDElements_Get_Totalcustomers() @property def pctPermanent(self) -> float: @@ -195,11 +192,11 @@ def pctPermanent(self) -> float: Original COM help: https://opendss.epri.com/pctPermanent.html ''' - return self._check_for_error(self._lib.PDElements_Get_pctPermanent()) + return self._lib.PDElements_Get_pctPermanent() @pctPermanent.setter def pctPermanent(self, Value: float): - self._check_for_error(self._lib.PDElements_Set_pctPermanent(Value)) + self._lib.PDElements_Set_pctPermanent(Value) def __iter__(self) -> Iterator[IPDElements]: idx = self.First @@ -214,7 +211,7 @@ def AllNames(self) -> List[str]: **(API Extension)** ''' - return self._check_for_error(self._get_string_array(self._lib.PDElements_Get_AllNames)) + return self._lib.PDElements_Get_AllNames() def AllMaxCurrents(self, AllNodes: bool = False) -> Float64Array: ''' @@ -230,8 +227,7 @@ def AllMaxCurrents(self, AllNodes: bool = False) -> Float64Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllMaxCurrents_GR(AllNodes)) - return self._get_float64_gr_array() + return self._lib.PDElements_Get_AllMaxCurrents_GR(AllNodes) def AllPctNorm(self, AllNodes: bool = False) -> Float64Array: ''' @@ -247,9 +243,8 @@ def AllPctNorm(self, AllNodes: bool = False) -> Float64Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllPctNorm_GR(AllNodes)) - return self._get_float64_gr_array() - + return self._lib.PDElements_Get_AllPctNorm_GR(AllNodes) + def AllPctEmerg(self, AllNodes: bool = False) -> Float64Array: ''' Array of doubles with the maximum current across the conductors as a percentage @@ -264,8 +259,7 @@ def AllPctEmerg(self, AllNodes: bool = False) -> Float64Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllPctEmerg_GR(AllNodes)) - return self._get_float64_gr_array() + return self._lib.PDElements_Get_AllPctEmerg_GR(AllNodes) @property def AllCurrents(self) -> Float64ArrayOrComplexArray: @@ -274,8 +268,7 @@ def AllCurrents(self) -> Float64ArrayOrComplexArray: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllCurrents_GR()) - return self._get_complex128_gr_array() + return self._lib.PDElements_Get_AllCurrents_GR() @property def AllCurrentsMagAng(self) -> Float64Array: @@ -284,8 +277,7 @@ def AllCurrentsMagAng(self) -> Float64Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllCurrentsMagAng_GR()) - return self._get_float64_gr_array() + return self._lib.PDElements_Get_AllCurrentsMagAng_GR() @property def AllCplxSeqCurrents(self) -> Float64ArrayOrComplexArray: @@ -294,8 +286,7 @@ def AllCplxSeqCurrents(self) -> Float64ArrayOrComplexArray: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllCplxSeqCurrents_GR()) - return self._get_complex128_gr_array() + return self._lib.PDElements_Get_AllCplxSeqCurrents_GR() @property def AllSeqCurrents(self) -> Float64Array: @@ -304,8 +295,7 @@ def AllSeqCurrents(self) -> Float64Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllSeqCurrents_GR()) - return self._get_float64_gr_array() + return self._lib.PDElements_Get_AllSeqCurrents_GR() @property def AllPowers(self) -> Float64ArrayOrComplexArray: @@ -314,8 +304,7 @@ def AllPowers(self) -> Float64ArrayOrComplexArray: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllPowers_GR()) - return self._get_complex128_gr_array() + return self._lib.PDElements_Get_AllPowers_GR() @property def AllSeqPowers(self) -> Float64ArrayOrComplexArray: @@ -324,8 +313,7 @@ def AllSeqPowers(self) -> Float64ArrayOrComplexArray: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllSeqPowers_GR()) - return self._get_complex128_gr_array() + return self._lib.PDElements_Get_AllSeqPowers_GR() @property def AllNumPhases(self) -> Int32Array: @@ -334,8 +322,7 @@ def AllNumPhases(self) -> Int32Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllNumPhases_GR()) - return self._get_int32_gr_array() + return self._lib.PDElements_Get_AllNumPhases_GR() @property def AllNumConductors(self) -> Int32Array: @@ -344,9 +331,7 @@ def AllNumConductors(self) -> Int32Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllNumConductors_GR()) - return self._get_int32_gr_array() - + return self._lib.PDElements_Get_AllNumConductors_GR() @property def AllNumTerminals(self) -> Int32Array: @@ -355,7 +340,6 @@ def AllNumTerminals(self) -> Int32Array: **(API Extension)** ''' - self._check_for_error(self._lib.PDElements_Get_AllNumTerminals_GR()) - return self._get_int32_gr_array() + return self._lib.PDElements_Get_AllNumTerminals_GR() diff --git a/dss/IPVSystems.py b/dss/IPVSystems.py index 644d57ee..71d48b85 100644 --- a/dss/IPVSystems.py +++ b/dss/IPVSystems.py @@ -37,11 +37,11 @@ def Irradiance(self) -> float: Original COM help: https://opendss.epri.com/Irradiance.html ''' - return self._check_for_error(self._lib.PVSystems_Get_Irradiance()) + return self._lib.PVSystems_Get_Irradiance() @Irradiance.setter def Irradiance(self, Value: float): - self._check_for_error(self._lib.PVSystems_Set_Irradiance(Value)) + self._lib.PVSystems_Set_Irradiance(Value) @property def PF(self) -> float: @@ -50,11 +50,11 @@ def PF(self) -> float: Original COM help: https://opendss.epri.com/PF2.html ''' - return self._check_for_error(self._lib.PVSystems_Get_PF()) + return self._lib.PVSystems_Get_PF() @PF.setter def PF(self, Value: float): - self._check_for_error(self._lib.PVSystems_Set_PF(Value)) + self._lib.PVSystems_Set_PF(Value) @property def RegisterNames(self) -> List[str]: @@ -65,7 +65,7 @@ def RegisterNames(self) -> List[str]: Original COM help: https://opendss.epri.com/RegisterNames2.html ''' - return self._check_for_error(self._get_string_array(self._lib.PVSystems_Get_RegisterNames)) + return self._lib.PVSystems_Get_RegisterNames() @property def RegisterValues(self) -> Float64Array: @@ -74,8 +74,7 @@ def RegisterValues(self) -> Float64Array: Original COM help: https://opendss.epri.com/RegisterValues2.html ''' - self._check_for_error(self._lib.PVSystems_Get_RegisterValues_GR()) - return self._get_float64_gr_array() + return self._lib.PVSystems_Get_RegisterValues_GR() @property def kVArated(self) -> float: @@ -84,11 +83,11 @@ def kVArated(self) -> float: Original COM help: https://opendss.epri.com/kVArated1.html ''' - return self._check_for_error(self._lib.PVSystems_Get_kVArated()) + return self._lib.PVSystems_Get_kVArated() @kVArated.setter def kVArated(self, Value: float): - self._check_for_error(self._lib.PVSystems_Set_kVArated(Value)) + self._lib.PVSystems_Set_kVArated(Value) @property def kW(self) -> float: @@ -97,7 +96,7 @@ def kW(self) -> float: Original COM help: https://opendss.epri.com/kW2.html ''' - return self._check_for_error(self._lib.PVSystems_Get_kW()) + return self._lib.PVSystems_Get_kW() @property def kvar(self) -> float: @@ -106,11 +105,11 @@ def kvar(self) -> float: Original COM help: https://opendss.epri.com/kvar2.html ''' - return self._check_for_error(self._lib.PVSystems_Get_kvar()) + return self._lib.PVSystems_Get_kvar() @kvar.setter def kvar(self, Value: float): - self._check_for_error(self._lib.PVSystems_Set_kvar(Value)) + self._lib.PVSystems_Set_kvar(Value) @property def daily(self) -> str: @@ -121,14 +120,11 @@ def daily(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.PVSystems_Get_daily())) + return self._lib.PVSystems_Get_daily() @daily.setter def daily(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.PVSystems_Set_daily(Value)) + self._lib.PVSystems_Set_daily(Value) @property def duty(self) -> str: @@ -139,14 +135,11 @@ def duty(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.PVSystems_Get_duty())) + return self._lib.PVSystems_Get_duty() @duty.setter def duty(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.PVSystems_Set_duty(Value)) + self._lib.PVSystems_Set_duty(Value) @property def yearly(self) -> str: @@ -158,14 +151,11 @@ def yearly(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.PVSystems_Get_yearly())) + return self._lib.PVSystems_Get_yearly() @yearly.setter def yearly(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.PVSystems_Set_yearly(Value)) + self._lib.PVSystems_Set_yearly(Value) @property def Tdaily(self) -> str: @@ -177,14 +167,11 @@ def Tdaily(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.PVSystems_Get_Tdaily())) + return self._lib.PVSystems_Get_Tdaily() @Tdaily.setter def Tdaily(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.PVSystems_Set_Tdaily(Value)) + self._lib.PVSystems_Set_Tdaily(Value) @property def Tduty(self) -> str: @@ -199,14 +186,11 @@ def Tduty(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.PVSystems_Get_Tduty())) + return self._lib.PVSystems_Get_Tduty() @Tduty.setter def Tduty(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.PVSystems_Set_Tduty(Value)) + self._lib.PVSystems_Set_Tduty(Value) @property def Tyearly(self) -> str: @@ -219,14 +203,11 @@ def Tyearly(self) -> str: **(API Extension)** ''' - return self._get_string(self._check_for_error(self._lib.PVSystems_Get_Tyearly())) + return self._lib.PVSystems_Get_Tyearly() @Tyearly.setter def Tyearly(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.PVSystems_Set_Tyearly(Value)) + self._lib.PVSystems_Set_Tyearly(Value) @property def IrradianceNow(self) -> float: @@ -236,7 +217,7 @@ def IrradianceNow(self) -> float: Original COM help: https://opendss.epri.com/IrradianceNow.html ''' - return self._check_for_error(self._lib.PVSystems_Get_IrradianceNow()) + return self._lib.PVSystems_Get_IrradianceNow() @property def Pmpp(self) -> float: @@ -246,11 +227,11 @@ def Pmpp(self) -> float: Original COM help: https://opendss.epri.com/Pmpp.html ''' - return self._check_for_error(self._lib.PVSystems_Get_Pmpp()) + return self._lib.PVSystems_Get_Pmpp() @Pmpp.setter def Pmpp(self, Value: float): - self._check_for_error(self._lib.PVSystems_Set_Pmpp(Value)) + self._lib.PVSystems_Set_Pmpp(Value) @property def Sensor(self) -> str: @@ -259,4 +240,4 @@ def Sensor(self) -> str: Original COM help: https://opendss.epri.com/Sensor1.html ''' - return self._get_string(self._check_for_error(self._lib.PVSystems_Get_Sensor())) + return self._lib.PVSystems_Get_Sensor() diff --git a/dss/IParallel.py b/dss/IParallel.py index 89a529d9..3b2af4dd 100644 --- a/dss/IParallel.py +++ b/dss/IParallel.py @@ -18,7 +18,7 @@ def CreateActor(self): ''' Create a new actor, if there are still cores available. ''' - self._check_for_error(self._lib.Parallel_CreateActor()) + self._lib.Parallel_CreateActor() def Wait(self): ''' @@ -26,7 +26,7 @@ def Wait(self): Original COM help: https://opendss.epri.com/Wait.html ''' - self._check_for_error(self._lib.Parallel_Wait()) + self._lib.Parallel_Wait() @property def ActiveActor(self) -> int: @@ -35,11 +35,11 @@ def ActiveActor(self) -> int: Original COM help: https://opendss.epri.com/ActiveActor.html ''' - return self._check_for_error(self._lib.Parallel_Get_ActiveActor()) + return self._lib.Parallel_Get_ActiveActor() @ActiveActor.setter def ActiveActor(self, Value: int): - self._check_for_error(self._lib.Parallel_Set_ActiveActor(Value)) + self._lib.Parallel_Set_ActiveActor(Value) @property def ActiveParallel(self) -> int: @@ -49,11 +49,11 @@ def ActiveParallel(self) -> int: Original COM help: https://opendss.epri.com/ActiveParallel.html ''' - return self._check_for_error(self._lib.Parallel_Get_ActiveParallel()) #TODO: use boolean for consistency + return self._lib.Parallel_Get_ActiveParallel() #TODO: use boolean for consistency @ActiveParallel.setter def ActiveParallel(self, Value: int): - self._check_for_error(self._lib.Parallel_Set_ActiveParallel(Value)) + self._lib.Parallel_Set_ActiveParallel(Value) @property def ActorCPU(self) -> int: @@ -62,11 +62,11 @@ def ActorCPU(self) -> int: Original COM help: https://opendss.epri.com/ActorCPU.html ''' - return self._check_for_error(self._lib.Parallel_Get_ActorCPU()) + return self._lib.Parallel_Get_ActorCPU() @ActorCPU.setter def ActorCPU(self, Value: int): - self._check_for_error(self._lib.Parallel_Set_ActorCPU(Value)) + self._lib.Parallel_Set_ActorCPU(Value) @property def ActorProgress(self) -> Int32Array: @@ -75,8 +75,7 @@ def ActorProgress(self) -> Int32Array: Original COM help: https://opendss.epri.com/ActorProgress.html ''' - self._check_for_error(self._lib.Parallel_Get_ActorProgress_GR()) - return self._get_int32_gr_array() + return self._lib.Parallel_Get_ActorProgress_GR() @property def ActorStatus(self) -> Int32Array: @@ -85,8 +84,7 @@ def ActorStatus(self) -> Int32Array: Original COM help: https://opendss.epri.com/ActorStatus.html ''' - self._check_for_error(self._lib.Parallel_Get_ActorStatus_GR()) - return self._get_int32_gr_array() + return self._lib.Parallel_Get_ActorStatus_GR() @property def ConcatenateReports(self) -> int: @@ -96,11 +94,11 @@ def ConcatenateReports(self) -> int: Original COM help: https://opendss.epri.com/ConcatenateReports.html ''' - return self._check_for_error(self._lib.Parallel_Get_ConcatenateReports()) #TODO: use boolean for consistency + return self._lib.Parallel_Get_ConcatenateReports() #TODO: use boolean for consistency @ConcatenateReports.setter def ConcatenateReports(self, Value: int): - self._check_for_error(self._lib.Parallel_Set_ConcatenateReports(Value)) + self._lib.Parallel_Set_ConcatenateReports(Value) @property def NumCPUs(self) -> int: @@ -109,7 +107,7 @@ def NumCPUs(self) -> int: Original COM help: https://opendss.epri.com/NumCPUs.html ''' - return self._check_for_error(self._lib.Parallel_Get_NumCPUs()) + return self._lib.Parallel_Get_NumCPUs() @property def NumCores(self) -> int: @@ -118,7 +116,7 @@ def NumCores(self) -> int: Original COM help: https://opendss.epri.com/NumCores.html ''' - return self._check_for_error(self._lib.Parallel_Get_NumCores()) + return self._lib.Parallel_Get_NumCores() @property def NumOfActors(self) -> int: @@ -127,6 +125,6 @@ def NumOfActors(self) -> int: Original COM help: https://opendss.epri.com/NumOfActors.html ''' - return self._check_for_error(self._lib.Parallel_Get_NumOfActors()) + return self._lib.Parallel_Get_NumOfActors() diff --git a/dss/IParser.py b/dss/IParser.py index 8afc2c34..f1478f1e 100644 --- a/dss/IParser.py +++ b/dss/IParser.py @@ -14,18 +14,15 @@ class IParser(Base): def Matrix(self, ExpectedOrder: int) -> Float64Array: '''Use this property to parse a Matrix token in OpenDSS format. Returns square matrix of order specified. Order same as default Fortran order: column by column.''' - self._check_for_error(self._lib.Parser_Get_Matrix_GR(ExpectedOrder)) - return self._get_float64_gr_array() + return self._lib.Parser_Get_Matrix_GR(ExpectedOrder) def SymMatrix(self, ExpectedOrder: int) -> Float64Array: '''Use this property to parse a matrix token specified in lower triangle form. Symmetry is forced.''' - self._check_for_error(self._lib.Parser_Get_SymMatrix_GR(ExpectedOrder)) - return self._get_float64_gr_array() + return self._lib.Parser_Get_SymMatrix_GR(ExpectedOrder) def Vector(self, ExpectedSize: int) -> Float64Array: '''Returns token as array of doubles. For parsing quoted array syntax.''' - self._check_for_error(self._lib.Parser_Get_Vector_GR(ExpectedSize)) - return self._get_float64_gr_array() + return self._lib.Parser_Get_Vector_GR(ExpectedSize) def ResetDelimiters(self): ''' @@ -33,7 +30,7 @@ def ResetDelimiters(self): Original COM help: https://opendss.epri.com/ResetDelimiters.html ''' - self._check_for_error(self._lib.Parser_ResetDelimiters()) + self._lib.Parser_ResetDelimiters() @property def AutoIncrement(self) -> bool: @@ -42,11 +39,11 @@ def AutoIncrement(self) -> bool: Original COM help: https://opendss.epri.com/AutoIncrement.html ''' - return self._check_for_error(self._lib.Parser_Get_AutoIncrement()) != 0 + return self._lib.Parser_Get_AutoIncrement() @AutoIncrement.setter def AutoIncrement(self, Value: bool): - self._check_for_error(self._lib.Parser_Set_AutoIncrement(Value)) + self._lib.Parser_Set_AutoIncrement(Value) @property def BeginQuote(self) -> str: @@ -55,14 +52,11 @@ def BeginQuote(self) -> str: Original COM help: https://opendss.epri.com/BeginQuote.html ''' - return self._get_string(self._check_for_error(self._lib.Parser_Get_BeginQuote())) + return self._lib.Parser_Get_BeginQuote() @BeginQuote.setter def BeginQuote(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Parser_Set_BeginQuote(Value)) + self._lib.Parser_Set_BeginQuote(Value) @property def CmdString(self) -> str: @@ -71,14 +65,11 @@ def CmdString(self) -> str: Original COM help: https://opendss.epri.com/CmdString.html ''' - return self._get_string(self._check_for_error(self._lib.Parser_Get_CmdString())) + return self._lib.Parser_Get_CmdString() @CmdString.setter def CmdString(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Parser_Set_CmdString(Value)) + self._lib.Parser_Set_CmdString(Value) @property def DblValue(self) -> float: @@ -87,7 +78,7 @@ def DblValue(self) -> float: Original COM help: https://opendss.epri.com/DblValue.html ''' - return self._check_for_error(self._lib.Parser_Get_DblValue()) + return self._lib.Parser_Get_DblValue() @property def Delimiters(self) -> str: @@ -96,14 +87,11 @@ def Delimiters(self) -> str: Original COM help: https://opendss.epri.com/Delimiters.html ''' - return self._get_string(self._check_for_error(self._lib.Parser_Get_Delimiters())) + return self._lib.Parser_Get_Delimiters() @Delimiters.setter def Delimiters(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Parser_Set_Delimiters(Value)) + self._lib.Parser_Set_Delimiters(Value) @property def EndQuote(self) -> str: @@ -112,14 +100,11 @@ def EndQuote(self) -> str: Original COM help: https://opendss.epri.com/EndQuote.html ''' - return self._get_string(self._check_for_error(self._lib.Parser_Get_EndQuote())) + return self._lib.Parser_Get_EndQuote() @EndQuote.setter def EndQuote(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Parser_Set_EndQuote(Value)) + self._lib.Parser_Set_EndQuote(Value) @property def IntValue(self) -> int: @@ -128,7 +113,7 @@ def IntValue(self) -> int: Original COM help: https://opendss.epri.com/IntValue.html ''' - return self._check_for_error(self._lib.Parser_Get_IntValue()) + return self._lib.Parser_Get_IntValue() @property def NextParam(self) -> str: @@ -137,7 +122,7 @@ def NextParam(self) -> str: Original COM help: https://opendss.epri.com/NextParam.html ''' - return self._get_string(self._check_for_error(self._lib.Parser_Get_NextParam())) + return self._lib.Parser_Get_NextParam() @property def StrValue(self) -> str: @@ -146,7 +131,7 @@ def StrValue(self) -> str: Original COM help: https://opendss.epri.com/StrValue.html ''' - return self._get_string(self._check_for_error(self._lib.Parser_Get_StrValue())) + return self._lib.Parser_Get_StrValue() @property def WhiteSpace(self) -> str: @@ -155,12 +140,9 @@ def WhiteSpace(self) -> str: Original COM help: https://opendss.epri.com/WhiteSpace.html ''' - return self._get_string(self._check_for_error(self._lib.Parser_Get_WhiteSpace())) + return self._lib.Parser_Get_WhiteSpace() @WhiteSpace.setter def WhiteSpace(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Parser_Set_WhiteSpace(Value)) + self._lib.Parser_Set_WhiteSpace(Value) diff --git a/dss/IReactors.py b/dss/IReactors.py index 5a1963a4..f28364fa 100644 --- a/dss/IReactors.py +++ b/dss/IReactors.py @@ -45,61 +45,61 @@ def SpecType(self) -> int: How the reactor data was provided: 1=kvar, 2=R+jX, 3=R and X matrices, 4=sym components. Depending on this value, only some properties are filled or make sense in the context. ''' - return self._check_for_error(self._lib.Reactors_Get_SpecType()) #TODO: use enum + return self._lib.Reactors_Get_SpecType() #TODO: use enum @property def IsDelta(self) -> bool: '''Delta connection or wye?''' - return self._check_for_error(self._lib.Reactors_Get_IsDelta()) != 0 + return self._lib.Reactors_Get_IsDelta() @IsDelta.setter def IsDelta(self, Value: bool): - self._check_for_error(self._lib.Reactors_Set_IsDelta(Value)) + self._lib.Reactors_Set_IsDelta(Value) @property def Parallel(self) -> bool: '''Indicates whether Rmatrix and Xmatrix are to be considered in parallel.''' - return self._check_for_error(self._lib.Reactors_Get_Parallel()) != 0 + return self._lib.Reactors_Get_Parallel() @Parallel.setter def Parallel(self, Value: bool): - self._check_for_error(self._lib.Reactors_Set_Parallel(Value)) + self._lib.Reactors_Set_Parallel(Value) @property def LmH(self) -> float: '''Inductance, mH. Alternate way to define the reactance, X, property.''' - return self._check_for_error(self._lib.Reactors_Get_LmH()) + return self._lib.Reactors_Get_LmH() @LmH.setter def LmH(self, Value: float): - self._check_for_error(self._lib.Reactors_Set_LmH(Value)) + self._lib.Reactors_Set_LmH(Value) @property def kV(self) -> float: '''For 2, 3-phase, kV phase-phase. Otherwise specify actual coil rating.''' - return self._check_for_error(self._lib.Reactors_Get_kV()) + return self._lib.Reactors_Get_kV() @kV.setter def kV(self, Value: float): - self._check_for_error(self._lib.Reactors_Set_kV(Value)) + self._lib.Reactors_Set_kV(Value) @property def kvar(self) -> float: '''Total kvar, all phases. Evenly divided among phases. Only determines X. Specify R separately''' - return self._check_for_error(self._lib.Reactors_Get_kvar()) + return self._lib.Reactors_Get_kvar() @kvar.setter def kvar(self, Value: float): - self._check_for_error(self._lib.Reactors_Set_kvar(Value)) + self._lib.Reactors_Set_kvar(Value) @property def Phases(self) -> int: '''Number of phases.''' - return self._check_for_error(self._lib.Reactors_Get_Phases()) + return self._lib.Reactors_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.Reactors_Set_Phases(Value)) + self._lib.Reactors_Set_Phases(Value) @property def Bus1(self) -> str: @@ -108,14 +108,11 @@ def Bus1(self) -> str: Bus2 property will default to this bus, node 0, unless previously specified. Only Bus1 need be specified for a Yg shunt reactor. ''' - return self._get_string(self._check_for_error(self._lib.Reactors_Get_Bus1())) + return self._lib.Reactors_Get_Bus1() @Bus1.setter def Bus1(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Reactors_Set_Bus1(Value)) + self._lib.Reactors_Set_Bus1(Value) @property def Bus2(self) -> str: @@ -123,98 +120,86 @@ def Bus2(self) -> str: Name of 2nd bus. Defaults to all phases connected to first bus, node 0, (Shunt Wye Connection) except when Bus2 is specifically defined. Not necessary to specify for delta (LL) connection ''' - return self._get_string(self._check_for_error(self._lib.Reactors_Get_Bus2())) + return self._lib.Reactors_Get_Bus2() @Bus2.setter def Bus2(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Reactors_Set_Bus2(Value)) + self._lib.Reactors_Set_Bus2(Value) @property def LCurve(self) -> str: '''Name of XYCurve object, previously defined, describing per-unit variation of phase inductance, L=X/w, vs. frequency. Applies to reactance specified by X, LmH, Z, or kvar property. L generally decreases somewhat with frequency above the base frequency, approaching a limit at a few kHz.''' - return self._get_string(self._check_for_error(self._lib.Reactors_Get_LCurve())) + return self._lib.Reactors_Get_LCurve() @LCurve.setter def LCurve(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Reactors_Set_LCurve(Value)) + self._lib.Reactors_Set_LCurve(Value) @property def RCurve(self) -> str: '''Name of XYCurve object, previously defined, describing per-unit variation of phase resistance, R, vs. frequency. Applies to resistance specified by R or Z property. If actual values are not known, R often increases by approximately the square root of frequency.''' - return self._get_string(self._check_for_error(self._lib.Reactors_Get_RCurve())) + return self._lib.Reactors_Get_RCurve() @RCurve.setter def RCurve(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Reactors_Set_RCurve(Value)) + self._lib.Reactors_Set_RCurve(Value) @property def R(self) -> float: '''Resistance (in series with reactance), each phase, ohms. This property applies to REACTOR specified by either kvar or X. See also help on Z.''' - return self._check_for_error(self._lib.Reactors_Get_R()) + return self._lib.Reactors_Get_R() @R.setter def R(self, Value: float): - self._check_for_error(self._lib.Reactors_Set_R(Value)) + self._lib.Reactors_Set_R(Value) @property def X(self) -> float: '''Reactance, each phase, ohms at base frequency. See also help on Z and LmH properties.''' - return self._check_for_error(self._lib.Reactors_Get_X()) + return self._lib.Reactors_Get_X() @X.setter def X(self, Value: float): - self._check_for_error(self._lib.Reactors_Set_X(Value)) + self._lib.Reactors_Set_X(Value) @property def Rp(self) -> float: '''Resistance in parallel with R and X (the entire branch). Assumed infinite if not specified.''' - return self._check_for_error(self._lib.Reactors_Get_Rp()) + return self._lib.Reactors_Get_Rp() @Rp.setter def Rp(self, Value: float): - self._check_for_error(self._lib.Reactors_Set_Rp(Value)) + self._lib.Reactors_Set_Rp(Value) @property def Rmatrix(self) -> Float64Array: '''Resistance matrix, ohms at base frequency. Order of the matrix is the number of phases. Mutually exclusive to specifying parameters by kvar or X.''' - self._check_for_error(self._lib.Reactors_Get_Rmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.Reactors_Get_Rmatrix_GR() @Rmatrix.setter def Rmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Reactors_Set_Rmatrix(ValuePtr, ValueCount)) + self._lib.Reactors_Set_Rmatrix(ValuePtr, ValueCount) @property def Xmatrix(self) -> Float64Array: '''Reactance matrix, ohms at base frequency. Order of the matrix is the number of phases. Mutually exclusive to specifying parameters by kvar or X.''' - self._check_for_error(self._lib.Reactors_Get_Xmatrix_GR()) - return self._get_float64_gr_array() + return self._lib.Reactors_Get_Xmatrix_GR() @Xmatrix.setter def Xmatrix(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Reactors_Set_Xmatrix(ValuePtr, ValueCount)) + self._lib.Reactors_Set_Xmatrix(ValuePtr, ValueCount) @property def Z(self) -> Float64ArrayOrSimpleComplex: '''Alternative way of defining R and X properties. Enter a 2-element array representing R +jX in ohms.''' - self._check_for_error(self._lib.Reactors_Get_Z_GR()) - return self._get_complex128_gr_simple() + return self._lib.Reactors_Get_Z_GR() @Z.setter def Z(self, Value: Float64ArrayOrSimpleComplex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) - self._check_for_error(self._lib.Reactors_Set_Z(ValuePtr, ValueCount)) + self._lib.Reactors_Set_Z(ValuePtr, ValueCount) @property def Z1(self) -> Float64ArrayOrSimpleComplex: @@ -227,13 +212,12 @@ def Z1(self) -> Float64ArrayOrSimpleComplex: Side Effect: Sets Z2 and Z0 to same values unless they were previously defined. ''' - self._check_for_error(self._lib.Reactors_Get_Z1_GR()) - return self._get_complex128_gr_simple() + return self._lib.Reactors_Get_Z1_GR() @Z1.setter def Z1(self, Value: Float64ArrayOrSimpleComplex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) - self._check_for_error(self._lib.Reactors_Set_Z1(ValuePtr, ValueCount)) + self._lib.Reactors_Set_Z1(ValuePtr, ValueCount) @property def Z2(self) -> Float64ArrayOrSimpleComplex: @@ -244,13 +228,12 @@ def Z2(self) -> Float64ArrayOrSimpleComplex: Note: Z2 defaults to Z1 if it is not specifically defined. If Z2 is not equal to Z1, the impedance matrix is asymmetrical. ''' - self._check_for_error(self._lib.Reactors_Get_Z2_GR()) - return self._get_complex128_gr_simple() + return self._lib.Reactors_Get_Z2_GR() @Z2.setter def Z2(self, Value: Float64ArrayOrSimpleComplex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) - self._check_for_error(self._lib.Reactors_Set_Z2(ValuePtr, ValueCount)) + self._lib.Reactors_Set_Z2(ValuePtr, ValueCount) @property def Z0(self) -> Float64ArrayOrSimpleComplex: @@ -261,11 +244,10 @@ def Z0(self) -> Float64ArrayOrSimpleComplex: Note: Z0 defaults to Z1 if it is not specifically defined. ''' - self._check_for_error(self._lib.Reactors_Get_Z0_GR()) - return self._get_complex128_gr_simple() + return self._lib.Reactors_Get_Z0_GR() @Z0.setter def Z0(self, Value: Float64ArrayOrSimpleComplex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) - self._check_for_error(self._lib.Reactors_Set_Z0(ValuePtr, ValueCount)) + self._lib.Reactors_Set_Z0(ValuePtr, ValueCount) diff --git a/dss/IReclosers.py b/dss/IReclosers.py index c8d442cd..958777b1 100644 --- a/dss/IReclosers.py +++ b/dss/IReclosers.py @@ -28,10 +28,10 @@ class IReclosers(Iterable): ] def Close(self): - self._check_for_error(self._lib.Reclosers_Close()) + self._lib.Reclosers_Close() def Open(self): - self._check_for_error(self._lib.Reclosers_Open()) + self._lib.Reclosers_Open() @property def GroundInst(self) -> float: @@ -40,11 +40,11 @@ def GroundInst(self) -> float: Original COM help: https://opendss.epri.com/GroundInst.html ''' - return self._check_for_error(self._lib.Reclosers_Get_GroundInst()) + return self._lib.Reclosers_Get_GroundInst() @GroundInst.setter def GroundInst(self, Value: float): - self._check_for_error(self._lib.Reclosers_Set_GroundInst(Value)) + self._lib.Reclosers_Set_GroundInst(Value) @property def GroundTrip(self) -> float: @@ -53,11 +53,11 @@ def GroundTrip(self) -> float: Original COM help: https://opendss.epri.com/GroundTrip.html ''' - return self._check_for_error(self._lib.Reclosers_Get_GroundTrip()) + return self._lib.Reclosers_Get_GroundTrip() @GroundTrip.setter def GroundTrip(self, Value: float): - self._check_for_error(self._lib.Reclosers_Set_GroundTrip(Value)) + self._lib.Reclosers_Set_GroundTrip(Value) @property def MonitoredObj(self) -> str: @@ -66,14 +66,11 @@ def MonitoredObj(self) -> str: Original COM help: https://opendss.epri.com/MonitoredObj2.html ''' - return self._get_string(self._check_for_error(self._lib.Reclosers_Get_MonitoredObj())) + return self._lib.Reclosers_Get_MonitoredObj() @MonitoredObj.setter def MonitoredObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Reclosers_Set_MonitoredObj(Value)) + self._lib.Reclosers_Set_MonitoredObj(Value) @property def MonitoredTerm(self) -> int: @@ -82,11 +79,11 @@ def MonitoredTerm(self) -> int: Original COM help: https://opendss.epri.com/MonitoredTerm2.html ''' - return self._check_for_error(self._lib.Reclosers_Get_MonitoredTerm()) + return self._lib.Reclosers_Get_MonitoredTerm() @MonitoredTerm.setter def MonitoredTerm(self, Value: int): - self._check_for_error(self._lib.Reclosers_Set_MonitoredTerm(Value)) + self._lib.Reclosers_Set_MonitoredTerm(Value) @property def NumFast(self) -> int: @@ -95,11 +92,11 @@ def NumFast(self) -> int: Original COM help: https://opendss.epri.com/NumFast.html ''' - return self._check_for_error(self._lib.Reclosers_Get_NumFast()) + return self._lib.Reclosers_Get_NumFast() @NumFast.setter def NumFast(self, Value: int): - self._check_for_error(self._lib.Reclosers_Set_NumFast(Value)) + self._lib.Reclosers_Set_NumFast(Value) @property def PhaseInst(self) -> float: @@ -108,11 +105,11 @@ def PhaseInst(self) -> float: Original COM help: https://opendss.epri.com/PhaseInst.html ''' - return self._check_for_error(self._lib.Reclosers_Get_PhaseInst()) + return self._lib.Reclosers_Get_PhaseInst() @PhaseInst.setter def PhaseInst(self, Value: float): - self._check_for_error(self._lib.Reclosers_Set_PhaseInst(Value)) + self._lib.Reclosers_Set_PhaseInst(Value) @property def PhaseTrip(self) -> float: @@ -121,11 +118,11 @@ def PhaseTrip(self) -> float: Original COM help: https://opendss.epri.com/PhaseTrip.html ''' - return self._check_for_error(self._lib.Reclosers_Get_PhaseTrip()) + return self._lib.Reclosers_Get_PhaseTrip() @PhaseTrip.setter def PhaseTrip(self, Value: float): - self._check_for_error(self._lib.Reclosers_Set_PhaseTrip(Value)) + self._lib.Reclosers_Set_PhaseTrip(Value) @property def RecloseIntervals(self) -> Float64Array: @@ -134,8 +131,7 @@ def RecloseIntervals(self) -> Float64Array: Original COM help: https://opendss.epri.com/RecloseIntervals.html ''' - self._check_for_error(self._lib.Reclosers_Get_RecloseIntervals_GR()) - return self._get_float64_gr_array() + return self._lib.Reclosers_Get_RecloseIntervals_GR() @property def Shots(self) -> int: @@ -144,11 +140,11 @@ def Shots(self) -> int: Original COM help: https://opendss.epri.com/Shots.html ''' - return self._check_for_error(self._lib.Reclosers_Get_Shots()) + return self._lib.Reclosers_Get_Shots() @Shots.setter def Shots(self, Value: int): - self._check_for_error(self._lib.Reclosers_Set_Shots(Value)) + self._lib.Reclosers_Set_Shots(Value) @property def SwitchedObj(self) -> str: @@ -157,14 +153,11 @@ def SwitchedObj(self) -> str: Original COM help: https://opendss.epri.com/SwitchedObj1.html ''' - return self._get_string(self._check_for_error(self._lib.Reclosers_Get_SwitchedObj())) + return self._lib.Reclosers_Get_SwitchedObj() @SwitchedObj.setter def SwitchedObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Reclosers_Set_SwitchedObj(Value)) + self._lib.Reclosers_Set_SwitchedObj(Value) @property def SwitchedTerm(self) -> int: @@ -173,11 +166,11 @@ def SwitchedTerm(self) -> int: Original COM help: https://opendss.epri.com/SwitchedTerm1.html ''' - return self._check_for_error(self._lib.Reclosers_Get_SwitchedTerm()) + return self._lib.Reclosers_Get_SwitchedTerm() @SwitchedTerm.setter def SwitchedTerm(self, Value: int): - self._check_for_error(self._lib.Reclosers_Set_SwitchedTerm(Value)) + self._lib.Reclosers_Set_SwitchedTerm(Value) def Reset(self): @@ -186,7 +179,7 @@ def Reset(self): If open, lock out the recloser. If closed, resets recloser to first operation. ''' - self._check_for_error(self._lib.Reclosers_Reset()) + self._lib.Reclosers_Reset() @property def State(self) -> int: @@ -195,11 +188,11 @@ def State(self) -> int: If set to open (ActionCodes.Open=1), open recloser's controlled element and lock out the recloser. If set to close (ActionCodes.Close=2), close recloser's controlled element and resets recloser to first operation. ''' - return self._check_for_error(self._lib.Reclosers_Get_State()) + return self._lib.Reclosers_Get_State() @State.setter def State(self, Value: int): - self._check_for_error(self._lib.Reclosers_Set_State(Value)) + self._lib.Reclosers_Set_State(Value) @property def NormalState(self) -> int: @@ -208,8 +201,8 @@ def NormalState(self) -> int: Original COM help: https://opendss.epri.com/NormalState1.html ''' - return self._check_for_error(self._lib.Reclosers_Get_NormalState()) + return self._lib.Reclosers_Get_NormalState() @NormalState.setter def NormalState(self, Value: int): - self._check_for_error(self._lib.Reclosers_Set_NormalState(Value)) + self._lib.Reclosers_Set_NormalState(Value) diff --git a/dss/IReduceCkt.py b/dss/IReduceCkt.py index dfd8cc86..25c1e8ec 100644 --- a/dss/IReduceCkt.py +++ b/dss/IReduceCkt.py @@ -16,11 +16,11 @@ def Zmag(self) -> float: Original COM help: https://opendss.epri.com/Zmag.html ''' - return self._check_for_error(self._lib.ReduceCkt_Get_Zmag()) + return self._lib.ReduceCkt_Get_Zmag() @Zmag.setter def Zmag(self, Value: float): - self._check_for_error(self._lib.ReduceCkt_Set_Zmag(Value)) + self._lib.ReduceCkt_Set_Zmag(Value) @property def KeepLoad(self) -> bool: @@ -29,11 +29,11 @@ def KeepLoad(self) -> bool: Original COM help: https://opendss.epri.com/KeepLoad.html ''' - return self._check_for_error(self._lib.ReduceCkt_Get_KeepLoad()) != 0 + return self._lib.ReduceCkt_Get_KeepLoad() @KeepLoad.setter def KeepLoad(self, Value: bool): - self._check_for_error(self._lib.ReduceCkt_Set_KeepLoad(bool(Value))) + self._lib.ReduceCkt_Set_KeepLoad(Value) @property def EditString(self) -> str: @@ -42,14 +42,11 @@ def EditString(self) -> str: Original COM help: https://opendss.epri.com/EditString.html ''' - return self._get_string(self._check_for_error(self._lib.ReduceCkt_Get_EditString())) + return self._lib.ReduceCkt_Get_EditString() @EditString.setter def EditString(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.ReduceCkt_Set_EditString(Value)) + self._lib.ReduceCkt_Set_EditString(Value) @property def StartPDElement(self) -> str: @@ -58,14 +55,11 @@ def StartPDElement(self) -> str: Original COM help: https://opendss.epri.com/StartPDElement.html ''' - return self._get_string(self._check_for_error(self._lib.ReduceCkt_Get_StartPDElement())) + return self._lib.ReduceCkt_Get_StartPDElement() @StartPDElement.setter def StartPDElement(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.ReduceCkt_Set_StartPDElement(Value)) + self._lib.ReduceCkt_Set_StartPDElement(Value) @property def EnergyMeter(self) -> str: @@ -74,24 +68,18 @@ def EnergyMeter(self) -> str: Original COM help: https://opendss.epri.com/EnergyMeter1.html ''' - return self._get_string(self._check_for_error(self._lib.ReduceCkt_Get_EnergyMeter())) + return self._lib.ReduceCkt_Get_EnergyMeter() @EnergyMeter.setter def EnergyMeter(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.ReduceCkt_Set_EnergyMeter(Value)) + self._lib.ReduceCkt_Set_EnergyMeter(Value) def SaveCircuit(self, CktName: AnyStr): ''' Save present (reduced) circuit Filename is listed in the Text Result interface ''' - if not isinstance(CktName, bytes): - CktName = CktName.encode(self._api_util.codec) - - self._check_for_error(self._lib.ReduceCkt_SaveCircuit(CktName)) + self._lib.ReduceCkt_SaveCircuit(CktName) def DoDefault(self): ''' @@ -99,7 +87,7 @@ def DoDefault(self): Original COM help: https://opendss.epri.com/DoDefault.html ''' - self._check_for_error(self._lib.ReduceCkt_DoDefault()) + self._lib.ReduceCkt_DoDefault() def DoShortLines(self): ''' @@ -107,7 +95,7 @@ def DoShortLines(self): Original COM help: https://opendss.epri.com/DoShortLines.html ''' - self._check_for_error(self._lib.ReduceCkt_DoShortLines()) + self._lib.ReduceCkt_DoShortLines() def DoDangling(self): ''' @@ -115,7 +103,7 @@ def DoDangling(self): Original COM help: https://opendss.epri.com/DoDangling.html ''' - self._check_for_error(self._lib.ReduceCkt_DoDangling()) + self._lib.ReduceCkt_DoDangling() def DoLoopBreak(self): ''' @@ -123,19 +111,19 @@ def DoLoopBreak(self): Disables one of the Line objects at the head of a loop to force the circuit to be radial. ''' - self._check_for_error(self._lib.ReduceCkt_DoLoopBreak()) + self._lib.ReduceCkt_DoLoopBreak() def DoParallelLines(self): ''' Merge all parallel lines found in the circuit to facilitate its reduction. ''' - self._check_for_error(self._lib.ReduceCkt_DoParallelLines()) + self._lib.ReduceCkt_DoParallelLines() def DoSwitches(self): ''' Merge Line objects in which the IsSwitch property is true with the down-line Line object. ''' - self._check_for_error(self._lib.ReduceCkt_DoSwitches()) + self._lib.ReduceCkt_DoSwitches() def Do1phLaterals(self): ''' @@ -143,7 +131,7 @@ def Do1phLaterals(self): Loads and other shunt elements are moved to the parent 3-phase bus. ''' - self._check_for_error(self._lib.ReduceCkt_Do1phLaterals()) + self._lib.ReduceCkt_Do1phLaterals() def DoBranchRemove(self): ''' @@ -153,4 +141,4 @@ def DoBranchRemove(self): If KeepLoad=Y (default), a new Load element is defined and kW, kvar are set to present power flow solution for the first element eliminated. The EditString is applied to each new Load element defined. ''' - self._check_for_error(self._lib.ReduceCkt_DoBranchRemove()) + self._lib.ReduceCkt_DoBranchRemove() diff --git a/dss/IRegControls.py b/dss/IRegControls.py index d080891d..bfa46994 100644 --- a/dss/IRegControls.py +++ b/dss/IRegControls.py @@ -35,7 +35,7 @@ class IRegControls(Iterable): ] def Reset(self): - self._check_for_error(self._lib.RegControls_Reset()) + self._lib.RegControls_Reset() @property def CTPrimary(self) -> float: @@ -44,11 +44,11 @@ def CTPrimary(self) -> float: Original COM help: https://opendss.epri.com/CTPrimary.html ''' - return self._check_for_error(self._lib.RegControls_Get_CTPrimary()) + return self._lib.RegControls_Get_CTPrimary() @CTPrimary.setter def CTPrimary(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_CTPrimary(Value)) + self._lib.RegControls_Set_CTPrimary(Value) @property def Delay(self) -> float: @@ -57,11 +57,11 @@ def Delay(self) -> float: Original COM help: https://opendss.epri.com/Delay2.html ''' - return self._check_for_error(self._lib.RegControls_Get_Delay()) + return self._lib.RegControls_Get_Delay() @Delay.setter def Delay(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_Delay(Value)) + self._lib.RegControls_Set_Delay(Value) @property def ForwardBand(self) -> float: @@ -70,11 +70,11 @@ def ForwardBand(self) -> float: Original COM help: https://opendss.epri.com/ForwardBand.html ''' - return self._check_for_error(self._lib.RegControls_Get_ForwardBand()) + return self._lib.RegControls_Get_ForwardBand() @ForwardBand.setter def ForwardBand(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ForwardBand(Value)) + self._lib.RegControls_Set_ForwardBand(Value) @property def ForwardR(self) -> float: @@ -83,11 +83,11 @@ def ForwardR(self) -> float: Original COM help: https://opendss.epri.com/ForwardR.html ''' - return self._check_for_error(self._lib.RegControls_Get_ForwardR()) + return self._lib.RegControls_Get_ForwardR() @ForwardR.setter def ForwardR(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ForwardR(Value)) + self._lib.RegControls_Set_ForwardR(Value) @property def ForwardVreg(self) -> float: @@ -96,11 +96,11 @@ def ForwardVreg(self) -> float: Original COM help: https://opendss.epri.com/ForwardVreg.html ''' - return self._check_for_error(self._lib.RegControls_Get_ForwardVreg()) + return self._lib.RegControls_Get_ForwardVreg() @ForwardVreg.setter def ForwardVreg(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ForwardVreg(Value)) + self._lib.RegControls_Set_ForwardVreg(Value) @property def ForwardX(self) -> float: @@ -109,11 +109,11 @@ def ForwardX(self) -> float: Original COM help: https://opendss.epri.com/ForwardX.html ''' - return self._check_for_error(self._lib.RegControls_Get_ForwardX()) + return self._lib.RegControls_Get_ForwardX() @ForwardX.setter def ForwardX(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ForwardX(Value)) + self._lib.RegControls_Set_ForwardX(Value) @property def IsInverseTime(self) -> bool: @@ -122,11 +122,11 @@ def IsInverseTime(self) -> bool: Original COM help: https://opendss.epri.com/IsInverseTime.html ''' - return self._check_for_error(self._lib.RegControls_Get_IsInverseTime()) != 0 + return self._lib.RegControls_Get_IsInverseTime() @IsInverseTime.setter def IsInverseTime(self, Value: bool): - self._check_for_error(self._lib.RegControls_Set_IsInverseTime(Value)) + self._lib.RegControls_Set_IsInverseTime(Value) @property def IsReversible(self) -> bool: @@ -135,11 +135,11 @@ def IsReversible(self) -> bool: Original COM help: https://opendss.epri.com/IsReversible.html ''' - return self._check_for_error(self._lib.RegControls_Get_IsReversible()) != 0 + return self._lib.RegControls_Get_IsReversible() @IsReversible.setter def IsReversible(self, Value: bool): - self._check_for_error(self._lib.RegControls_Set_IsReversible(Value)) + self._lib.RegControls_Set_IsReversible(Value) @property def MaxTapChange(self) -> int: @@ -148,11 +148,11 @@ def MaxTapChange(self) -> int: Original COM help: https://opendss.epri.com/MaxTapChange.html ''' - return self._check_for_error(self._lib.RegControls_Get_MaxTapChange()) + return self._lib.RegControls_Get_MaxTapChange() @MaxTapChange.setter def MaxTapChange(self, Value: int): - self._check_for_error(self._lib.RegControls_Set_MaxTapChange(Value)) + self._lib.RegControls_Set_MaxTapChange(Value) @property def MonitoredBus(self) -> str: @@ -161,14 +161,11 @@ def MonitoredBus(self) -> str: Original COM help: https://opendss.epri.com/MonitoredBus.html ''' - return self._get_string(self._check_for_error(self._lib.RegControls_Get_MonitoredBus())) + return self._lib.RegControls_Get_MonitoredBus() @MonitoredBus.setter def MonitoredBus(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.RegControls_Set_MonitoredBus(Value)) + self._lib.RegControls_Set_MonitoredBus(Value) @property def PTratio(self) -> float: @@ -177,11 +174,11 @@ def PTratio(self) -> float: Original COM help: https://opendss.epri.com/PTratio1.html ''' - return self._check_for_error(self._lib.RegControls_Get_PTratio()) + return self._lib.RegControls_Get_PTratio() @PTratio.setter def PTratio(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_PTratio(Value)) + self._lib.RegControls_Set_PTratio(Value) @property def ReverseBand(self) -> float: @@ -190,11 +187,11 @@ def ReverseBand(self) -> float: Original COM help: https://opendss.epri.com/ReverseBand.html ''' - return self._check_for_error(self._lib.RegControls_Get_ReverseBand()) + return self._lib.RegControls_Get_ReverseBand() @ReverseBand.setter def ReverseBand(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ReverseBand(Value)) + self._lib.RegControls_Set_ReverseBand(Value) @property def ReverseR(self) -> float: @@ -203,11 +200,11 @@ def ReverseR(self) -> float: Original COM help: https://opendss.epri.com/ReverseR.html ''' - return self._check_for_error(self._lib.RegControls_Get_ReverseR()) + return self._lib.RegControls_Get_ReverseR() @ReverseR.setter def ReverseR(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ReverseR(Value)) + self._lib.RegControls_Set_ReverseR(Value) @property def ReverseVreg(self) -> float: @@ -216,11 +213,11 @@ def ReverseVreg(self) -> float: Original COM help: https://opendss.epri.com/ReverseVreg.html ''' - return self._check_for_error(self._lib.RegControls_Get_ReverseVreg()) + return self._lib.RegControls_Get_ReverseVreg() @ReverseVreg.setter def ReverseVreg(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ReverseVreg(Value)) + self._lib.RegControls_Set_ReverseVreg(Value) @property def ReverseX(self) -> float: @@ -229,11 +226,11 @@ def ReverseX(self) -> float: Original COM help: https://opendss.epri.com/ReverseX.html ''' - return self._check_for_error(self._lib.RegControls_Get_ReverseX()) + return self._lib.RegControls_Get_ReverseX() @ReverseX.setter def ReverseX(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_ReverseX(Value)) + self._lib.RegControls_Set_ReverseX(Value) @property def TapDelay(self) -> float: @@ -242,11 +239,11 @@ def TapDelay(self) -> float: Original COM help: https://opendss.epri.com/TapDelay.html ''' - return self._check_for_error(self._lib.RegControls_Get_TapDelay()) + return self._lib.RegControls_Get_TapDelay() @TapDelay.setter def TapDelay(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_TapDelay(Value)) + self._lib.RegControls_Set_TapDelay(Value) @property def TapNumber(self) -> int: @@ -255,11 +252,11 @@ def TapNumber(self) -> int: Original COM help: https://opendss.epri.com/TapNumber.html ''' - return self._check_for_error(self._lib.RegControls_Get_TapNumber()) + return self._lib.RegControls_Get_TapNumber() @TapNumber.setter def TapNumber(self, Value: int): - self._check_for_error(self._lib.RegControls_Set_TapNumber(Value)) + self._lib.RegControls_Set_TapNumber(Value) @property def TapWinding(self) -> int: @@ -268,11 +265,11 @@ def TapWinding(self) -> int: Original COM help: https://opendss.epri.com/TapWinding.html ''' - return self._check_for_error(self._lib.RegControls_Get_TapWinding()) + return self._lib.RegControls_Get_TapWinding() @TapWinding.setter def TapWinding(self, Value: int): - self._check_for_error(self._lib.RegControls_Set_TapWinding(Value)) + self._lib.RegControls_Set_TapWinding(Value) @property def Transformer(self) -> str: @@ -281,14 +278,11 @@ def Transformer(self) -> str: Original COM help: https://opendss.epri.com/Transformer.html ''' - return self._get_string(self._check_for_error(self._lib.RegControls_Get_Transformer())) + return self._lib.RegControls_Get_Transformer() @Transformer.setter def Transformer(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.RegControls_Set_Transformer(Value)) + self._lib.RegControls_Set_Transformer(Value) @property def VoltageLimit(self) -> float: @@ -297,11 +291,11 @@ def VoltageLimit(self) -> float: Original COM help: https://opendss.epri.com/VoltageLimit.html ''' - return self._check_for_error(self._lib.RegControls_Get_VoltageLimit()) + return self._lib.RegControls_Get_VoltageLimit() @VoltageLimit.setter def VoltageLimit(self, Value: float): - self._check_for_error(self._lib.RegControls_Set_VoltageLimit(Value)) + self._lib.RegControls_Set_VoltageLimit(Value) @property def Winding(self) -> int: @@ -310,10 +304,10 @@ def Winding(self) -> int: Original COM help: https://opendss.epri.com/Winding.html ''' - return self._check_for_error(self._lib.RegControls_Get_Winding()) + return self._lib.RegControls_Get_Winding() @Winding.setter def Winding(self, Value: int): - self._check_for_error(self._lib.RegControls_Set_Winding(Value)) + self._lib.RegControls_Set_Winding(Value) diff --git a/dss/IRelays.py b/dss/IRelays.py index 4bdf7f54..7d612225 100644 --- a/dss/IRelays.py +++ b/dss/IRelays.py @@ -26,14 +26,11 @@ def MonitoredObj(self) -> str: Original COM help: https://opendss.epri.com/MonitoredObj3.html ''' - return self._get_string(self._check_for_error(self._lib.Relays_Get_MonitoredObj())) + return self._lib.Relays_Get_MonitoredObj() @MonitoredObj.setter def MonitoredObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Relays_Set_MonitoredObj(Value)) + self._lib.Relays_Set_MonitoredObj(Value) @property def MonitoredTerm(self) -> int: @@ -42,11 +39,11 @@ def MonitoredTerm(self) -> int: Original COM help: https://opendss.epri.com/MonitoredTerm3.html ''' - return self._check_for_error(self._lib.Relays_Get_MonitoredTerm()) + return self._lib.Relays_Get_MonitoredTerm() @MonitoredTerm.setter def MonitoredTerm(self, Value: int): - self._check_for_error(self._lib.Relays_Set_MonitoredTerm(Value)) + self._lib.Relays_Set_MonitoredTerm(Value) @property def SwitchedObj(self) -> str: @@ -55,14 +52,11 @@ def SwitchedObj(self) -> str: Original COM help: https://opendss.epri.com/SwitchedObj2.html ''' - return self._get_string(self._check_for_error(self._lib.Relays_Get_SwitchedObj())) + return self._lib.Relays_Get_SwitchedObj() @SwitchedObj.setter def SwitchedObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Relays_Set_SwitchedObj(Value)) + self._lib.Relays_Set_SwitchedObj(Value) @property def SwitchedTerm(self) -> int: @@ -71,11 +65,11 @@ def SwitchedTerm(self) -> int: Original COM help: https://opendss.epri.com/SwitchedTerm2.html ''' - return self._check_for_error(self._lib.Relays_Get_SwitchedTerm()) + return self._lib.Relays_Get_SwitchedTerm() @SwitchedTerm.setter def SwitchedTerm(self, Value: int): - self._check_for_error(self._lib.Relays_Set_SwitchedTerm(Value)) + self._lib.Relays_Set_SwitchedTerm(Value) def Open(self): ''' @@ -83,7 +77,7 @@ def Open(self): Original COM help: https://opendss.epri.com/Open4.html ''' - self._check_for_error(self._lib.Relays_Open()) + self._lib.Relays_Open() def Close(self): ''' @@ -91,7 +85,7 @@ def Close(self): Original COM help: https://opendss.epri.com/Close5.html ''' - self._check_for_error(self._lib.Relays_Close()) + self._lib.Relays_Close() def Reset(self): ''' @@ -99,7 +93,7 @@ def Reset(self): If open, lock out the relay. If closed, resets relay to first operation. ''' - self._check_for_error(self._lib.Relays_Reset()) + self._lib.Relays_Reset() @property def State(self) -> int: @@ -108,11 +102,11 @@ def State(self) -> int: If set to open, open relay's controlled element and lock out the relay. If set to close, close relay's controlled element and resets relay to first operation. ''' - return self._check_for_error(self._lib.Relays_Get_State()) + return self._lib.Relays_Get_State() @State.setter def State(self, Value: int): - self._check_for_error(self._lib.Relays_Set_State(Value)) + self._lib.Relays_Set_State(Value) @property def NormalState(self) -> int: @@ -121,8 +115,8 @@ def NormalState(self) -> int: Original COM help: https://opendss.epri.com/NormalState3.html ''' - return self._check_for_error(self._lib.Relays_Get_NormalState()) + return self._lib.Relays_Get_NormalState() @NormalState.setter def NormalState(self, Value: int): - self._check_for_error(self._lib.Relays_Set_NormalState(Value)) + self._lib.Relays_Set_NormalState(Value) diff --git a/dss/ISensors.py b/dss/ISensors.py index 7ae06774..7a9082e4 100644 --- a/dss/ISensors.py +++ b/dss/ISensors.py @@ -27,10 +27,10 @@ class ISensors(Iterable): ] def Reset(self): - self._check_for_error(self._lib.Sensors_Reset()) + self._lib.Sensors_Reset() def ResetAll(self): - self._check_for_error(self._lib.Sensors_ResetAll()) + self._lib.Sensors_ResetAll() @property def Currents(self) -> Float64Array: @@ -39,13 +39,12 @@ def Currents(self) -> Float64Array: Original COM help: https://opendss.epri.com/Currents2.html ''' - self._check_for_error(self._lib.Sensors_Get_Currents_GR()) - return self._get_float64_gr_array() + return self._lib.Sensors_Get_Currents_GR() @Currents.setter def Currents(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Sensors_Set_Currents(ValuePtr, ValueCount)) + self._lib.Sensors_Set_Currents(ValuePtr, ValueCount) @property def IsDelta(self) -> bool: @@ -54,11 +53,11 @@ def IsDelta(self) -> bool: Original COM help: https://opendss.epri.com/IsDelta2.html ''' - return self._check_for_error(self._lib.Sensors_Get_IsDelta()) != 0 + return self._lib.Sensors_Get_IsDelta() @IsDelta.setter def IsDelta(self, Value: bool): - self._check_for_error(self._lib.Sensors_Set_IsDelta(Value)) + self._lib.Sensors_Set_IsDelta(Value) @property def MeteredElement(self) -> str: @@ -67,14 +66,11 @@ def MeteredElement(self) -> str: Original COM help: https://opendss.epri.com/MeteredElement1.html ''' - return self._get_string(self._check_for_error(self._lib.Sensors_Get_MeteredElement())) + return self._lib.Sensors_Get_MeteredElement() @MeteredElement.setter def MeteredElement(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Sensors_Set_MeteredElement(Value)) + self._lib.Sensors_Set_MeteredElement(Value) @property def MeteredTerminal(self) -> int: @@ -83,11 +79,11 @@ def MeteredTerminal(self) -> int: Original COM help: https://opendss.epri.com/MeteredTerminal1.html ''' - return self._check_for_error(self._lib.Sensors_Get_MeteredTerminal()) + return self._lib.Sensors_Get_MeteredTerminal() @MeteredTerminal.setter def MeteredTerminal(self, Value: int): - self._check_for_error(self._lib.Sensors_Set_MeteredTerminal(Value)) + self._lib.Sensors_Set_MeteredTerminal(Value) @property def PctError(self) -> float: @@ -96,11 +92,11 @@ def PctError(self) -> float: Original COM help: https://opendss.epri.com/PctError.html ''' - return self._check_for_error(self._lib.Sensors_Get_PctError()) + return self._lib.Sensors_Get_PctError() @PctError.setter def PctError(self, Value: float): - self._check_for_error(self._lib.Sensors_Set_PctError(Value)) + self._lib.Sensors_Set_PctError(Value) @property def ReverseDelta(self) -> bool: @@ -109,11 +105,11 @@ def ReverseDelta(self) -> bool: Original COM help: https://opendss.epri.com/ReverseDelta.html ''' - return self._check_for_error(self._lib.Sensors_Get_ReverseDelta()) != 0 + return self._lib.Sensors_Get_ReverseDelta() @ReverseDelta.setter def ReverseDelta(self, Value: bool): - self._check_for_error(self._lib.Sensors_Set_ReverseDelta(Value)) + self._lib.Sensors_Set_ReverseDelta(Value) @property def Weight(self) -> float: @@ -122,11 +118,11 @@ def Weight(self) -> float: Original COM help: https://opendss.epri.com/Weight.html ''' - return self._check_for_error(self._lib.Sensors_Get_Weight()) + return self._lib.Sensors_Get_Weight() @Weight.setter def Weight(self, Value: float): - self._check_for_error(self._lib.Sensors_Set_Weight(Value)) + self._lib.Sensors_Set_Weight(Value) @property def kVARS(self) -> Float64Array: @@ -135,13 +131,12 @@ def kVARS(self) -> Float64Array: Original COM help: https://opendss.epri.com/kVARS.html ''' - self._check_for_error(self._lib.Sensors_Get_kVARS_GR()) - return self._get_float64_gr_array() + return self._lib.Sensors_Get_kVARS_GR() @kVARS.setter def kVARS(self, Value): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Sensors_Set_kVARS(ValuePtr, ValueCount)) + self._lib.Sensors_Set_kVARS(ValuePtr, ValueCount) @property def kVS(self) -> Float64Array: @@ -150,13 +145,12 @@ def kVS(self) -> Float64Array: Original COM help: https://opendss.epri.com/kVS.html ''' - self._check_for_error(self._lib.Sensors_Get_kVS_GR()) - return self._get_float64_gr_array() + return self._lib.Sensors_Get_kVS_GR() @kVS.setter def kVS(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Sensors_Set_kVS(ValuePtr, ValueCount)) + self._lib.Sensors_Set_kVS(ValuePtr, ValueCount) @property def kVbase(self) -> float: @@ -165,11 +159,11 @@ def kVbase(self) -> float: Original COM help: https://opendss.epri.com/kVBase1.html ''' - return self._check_for_error(self._lib.Sensors_Get_kVbase()) + return self._lib.Sensors_Get_kVbase() @kVbase.setter def kVbase(self, Value: float): - self._check_for_error(self._lib.Sensors_Set_kVbase(Value)) + self._lib.Sensors_Set_kVbase(Value) @property def kWS(self) -> Float64Array: @@ -178,13 +172,12 @@ def kWS(self) -> Float64Array: Original COM help: https://opendss.epri.com/kWS.html ''' - self._check_for_error(self._lib.Sensors_Get_kWS_GR()) - return self._get_float64_gr_array() + return self._lib.Sensors_Get_kWS_GR() @kWS.setter def kWS(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Sensors_Set_kWS(ValuePtr, ValueCount)) + self._lib.Sensors_Set_kWS(ValuePtr, ValueCount) @property def AllocationFactor(self): @@ -193,5 +186,4 @@ def AllocationFactor(self): Original COM help: https://opendss.epri.com/AllocationFactor1.html ''' - self._check_for_error(self._lib.Sensors_Get_AllocationFactor_GR()) - return self._get_float64_gr_array() + return self._lib.Sensors_Get_AllocationFactor_GR() diff --git a/dss/ISettings.py b/dss/ISettings.py index bcda2b85..d0250a4b 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -41,11 +41,11 @@ def AllowDuplicates(self) -> bool: **NOTE**: for DSS-Extensions, we are considering removing this option in a future release since it has performance impacts even when not used. ''' - return self._check_for_error(self._lib.Settings_Get_AllowDuplicates()) != 0 + return self._lib.Settings_Get_AllowDuplicates() @AllowDuplicates.setter def AllowDuplicates(self, Value: bool): - self._check_for_error(self._lib.Settings_Set_AllowDuplicates(Value)) + self._lib.Settings_Set_AllowDuplicates(Value) @property def AutoBusList(self) -> str: @@ -54,14 +54,11 @@ def AutoBusList(self) -> str: Original COM help: https://opendss.epri.com/AutoBusList.html ''' - return self._get_string(self._check_for_error(self._lib.Settings_Get_AutoBusList())) + return self._lib.Settings_Get_AutoBusList() @AutoBusList.setter def AutoBusList(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Settings_Set_AutoBusList(Value)) + self._lib.Settings_Set_AutoBusList(Value) @property def CktModel(self) -> CktModels: @@ -70,11 +67,11 @@ def CktModel(self) -> CktModels: Original COM help: https://opendss.epri.com/CktModel.html ''' - return self._check_for_error(CktModels(self._lib.Settings_Get_CktModel())) + return CktModels(self._lib.Settings_Get_CktModel()) @CktModel.setter def CktModel(self, Value: Union[int, CktModels]): - self._check_for_error(self._lib.Settings_Set_CktModel(Value)) + self._lib.Settings_Set_CktModel(Value) @property def ControlTrace(self) -> bool: @@ -83,11 +80,11 @@ def ControlTrace(self) -> bool: Original COM help: https://opendss.epri.com/ControlTrace.html ''' - return self._check_for_error(self._lib.Settings_Get_ControlTrace()) != 0 + return self._lib.Settings_Get_ControlTrace() @ControlTrace.setter def ControlTrace(self, Value: bool): - self._check_for_error(self._lib.Settings_Set_ControlTrace(Value)) + self._lib.Settings_Set_ControlTrace(Value) @property def EmergVmaxpu(self) -> float: @@ -96,11 +93,11 @@ def EmergVmaxpu(self) -> float: Original COM help: https://opendss.epri.com/EmergVmaxpu.html ''' - return self._check_for_error(self._lib.Settings_Get_EmergVmaxpu()) + return self._lib.Settings_Get_EmergVmaxpu() @EmergVmaxpu.setter def EmergVmaxpu(self, Value: float): - self._check_for_error(self._lib.Settings_Set_EmergVmaxpu(Value)) + self._lib.Settings_Set_EmergVmaxpu(Value) @property def EmergVminpu(self) -> float: @@ -109,11 +106,11 @@ def EmergVminpu(self) -> float: Original COM help: https://opendss.epri.com/EmergVminpu.html ''' - return self._check_for_error(self._lib.Settings_Get_EmergVminpu()) + return self._lib.Settings_Get_EmergVminpu() @EmergVminpu.setter def EmergVminpu(self, Value: float): - self._check_for_error(self._lib.Settings_Set_EmergVminpu(Value)) + self._lib.Settings_Set_EmergVminpu(Value) @property def LossRegs(self) -> Int32Array: @@ -122,13 +119,12 @@ def LossRegs(self) -> Int32Array: Original COM help: https://opendss.epri.com/LossRegs.html ''' - self._check_for_error(self._lib.Settings_Get_LossRegs_GR()) - return self._get_int32_gr_array() + return self._lib.Settings_Get_LossRegs_GR() @LossRegs.setter def LossRegs(self, Value: Int32Array): Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) - self._check_for_error(self._lib.Settings_Set_LossRegs(ValuePtr, ValueCount)) + self._lib.Settings_Set_LossRegs(ValuePtr, ValueCount) @property def LossWeight(self) -> float: @@ -137,11 +133,11 @@ def LossWeight(self) -> float: Original COM help: https://opendss.epri.com/LossWeight.html ''' - return self._check_for_error(self._lib.Settings_Get_LossWeight()) + return self._lib.Settings_Get_LossWeight() @LossWeight.setter def LossWeight(self, Value: float): - self._check_for_error(self._lib.Settings_Set_LossWeight(Value)) + self._lib.Settings_Set_LossWeight(Value) @property def NormVmaxpu(self) -> float: @@ -150,11 +146,11 @@ def NormVmaxpu(self) -> float: Original COM help: https://opendss.epri.com/NormVmaxpu.html ''' - return self._check_for_error(self._lib.Settings_Get_NormVmaxpu()) + return self._lib.Settings_Get_NormVmaxpu() @NormVmaxpu.setter def NormVmaxpu(self, Value: float): - self._check_for_error(self._lib.Settings_Set_NormVmaxpu(Value)) + self._lib.Settings_Set_NormVmaxpu(Value) @property def NormVminpu(self) -> float: @@ -163,11 +159,11 @@ def NormVminpu(self) -> float: Original COM help: https://opendss.epri.com/NormVminpu.html ''' - return self._check_for_error(self._lib.Settings_Get_NormVminpu()) + return self._lib.Settings_Get_NormVminpu() @NormVminpu.setter def NormVminpu(self, Value: float): - self._check_for_error(self._lib.Settings_Set_NormVminpu(Value)) + self._lib.Settings_Set_NormVminpu(Value) @property def PriceCurve(self) -> str: @@ -176,14 +172,11 @@ def PriceCurve(self) -> str: Original COM help: https://opendss.epri.com/PriceCurve.html ''' - return self._get_string(self._check_for_error(self._lib.Settings_Get_PriceCurve())) + return self._lib.Settings_Get_PriceCurve() @PriceCurve.setter def PriceCurve(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Settings_Set_PriceCurve(Value)) + self._lib.Settings_Set_PriceCurve(Value) @property def PriceSignal(self) -> float: @@ -192,11 +185,11 @@ def PriceSignal(self) -> float: Original COM help: https://opendss.epri.com/PriceSignal.html ''' - return self._check_for_error(self._lib.Settings_Get_PriceSignal()) + return self._lib.Settings_Get_PriceSignal() @PriceSignal.setter def PriceSignal(self, Value: float): - self._check_for_error(self._lib.Settings_Set_PriceSignal(Value)) + self._lib.Settings_Set_PriceSignal(Value) @property def Trapezoidal(self) -> bool: @@ -205,11 +198,11 @@ def Trapezoidal(self) -> bool: Original COM help: https://opendss.epri.com/Trapezoidal.html ''' - return self._check_for_error(self._lib.Settings_Get_Trapezoidal()) != 0 + return self._lib.Settings_Get_Trapezoidal() @Trapezoidal.setter def Trapezoidal(self, Value: bool): - self._check_for_error(self._lib.Settings_Set_Trapezoidal(Value)) + self._lib.Settings_Set_Trapezoidal(Value) @property def UEregs(self) -> Int32Array: @@ -218,13 +211,12 @@ def UEregs(self) -> Int32Array: Original COM help: https://opendss.epri.com/UEregs.html ''' - self._check_for_error(self._lib.Settings_Get_UEregs_GR()) - return self._get_int32_gr_array() + return self._lib.Settings_Get_UEregs_GR() @UEregs.setter def UEregs(self, Value: Int32Array): Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) - self._check_for_error(self._lib.Settings_Set_UEregs(ValuePtr, ValueCount)) + self._lib.Settings_Set_UEregs(ValuePtr, ValueCount) @property def UEweight(self) -> float: @@ -233,11 +225,11 @@ def UEweight(self) -> float: Original COM help: https://opendss.epri.com/UEweight.html ''' - return self._check_for_error(self._lib.Settings_Get_UEweight()) + return self._lib.Settings_Get_UEweight() @UEweight.setter def UEweight(self, Value: float): - self._check_for_error(self._lib.Settings_Set_UEweight(Value)) + self._lib.Settings_Set_UEweight(Value) @property def VoltageBases(self) -> Float64Array: @@ -246,13 +238,12 @@ def VoltageBases(self) -> Float64Array: Original COM help: https://opendss.epri.com/VoltageBases.html ''' - self._check_for_error(self._lib.Settings_Get_VoltageBases_GR()) - return self._get_float64_gr_array() + return self._lib.Settings_Get_VoltageBases_GR() @VoltageBases.setter def VoltageBases(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.Settings_Set_VoltageBases(ValuePtr, ValueCount)) + self._lib.Settings_Set_VoltageBases(ValuePtr, ValueCount) @property def ZoneLock(self) -> bool: @@ -261,11 +252,11 @@ def ZoneLock(self) -> bool: Original COM help: https://opendss.epri.com/ZoneLock.html ''' - return self._check_for_error(self._lib.Settings_Get_ZoneLock()) != 0 + return self._lib.Settings_Get_ZoneLock() @ZoneLock.setter def ZoneLock(self, Value: bool): - self._check_for_error(self._lib.Settings_Set_ZoneLock(Value)) + self._lib.Settings_Set_ZoneLock(Value) @property def AllocationFactors(self): @@ -274,7 +265,7 @@ def AllocationFactors(self): @AllocationFactors.setter def AllocationFactors(self, Value: float): - self._check_for_error(self._lib.Settings_Set_AllocationFactors(Value)) + self._lib.Settings_Set_AllocationFactors(Value) @property def LoadsTerminalCheck(self) -> bool: @@ -284,11 +275,11 @@ def LoadsTerminalCheck(self) -> bool: **(API Extension)** ''' - return self._check_for_error(self._lib.Settings_Get_LoadsTerminalCheck()) != 0 + return self._lib.Settings_Get_LoadsTerminalCheck() @LoadsTerminalCheck.setter def LoadsTerminalCheck(self, Value: bool): - self._check_for_error(self._lib.Settings_Set_LoadsTerminalCheck(Value)) + self._lib.Settings_Set_LoadsTerminalCheck(Value) @property def IterateDisabled(self) -> int: @@ -302,11 +293,11 @@ def IterateDisabled(self) -> int: **(API Extension)** ''' - return self._check_for_error(self._lib.Settings_Get_IterateDisabled()) + return self._lib.Settings_Get_IterateDisabled() @IterateDisabled.setter def IterateDisabled(self, Value: int): - self._check_for_error(self._lib.Settings_Set_IterateDisabled(Value)) + self._lib.Settings_Set_IterateDisabled(Value) def SetPropertyNameStyle(self, value: DSSPropertyNameStyle): ''' @@ -318,4 +309,4 @@ def SetPropertyNameStyle(self, value: DSSPropertyNameStyle): **(API Extension)** ''' - self._check_for_error(self._lib.Settings_SetPropertyNameStyle(value)) + self._lib.Settings_SetPropertyNameStyle(value) diff --git a/dss/ISolution.py b/dss/ISolution.py index e6e5fdd7..369704a1 100644 --- a/dss/ISolution.py +++ b/dss/ISolution.py @@ -52,49 +52,49 @@ class ISolution(Base): ] def BuildYMatrix(self, BuildOption: int, AllocateVI: bool): - self._check_for_error(self._lib.Solution_BuildYMatrix(BuildOption, AllocateVI)) + self._lib.Solution_BuildYMatrix(BuildOption, AllocateVI) def CheckControls(self): - self._check_for_error(self._lib.Solution_CheckControls()) + self._lib.Solution_CheckControls() def CheckFaultStatus(self): - self._check_for_error(self._lib.Solution_CheckFaultStatus()) + self._lib.Solution_CheckFaultStatus() def Cleanup(self): - self._check_for_error(self._lib.Solution_Cleanup()) + self._lib.Solution_Cleanup() def DoControlActions(self): - self._check_for_error(self._lib.Solution_DoControlActions()) + self._lib.Solution_DoControlActions() def FinishTimeStep(self): - self._check_for_error(self._lib.Solution_FinishTimeStep()) + self._lib.Solution_FinishTimeStep() def InitSnap(self): - self._check_for_error(self._lib.Solution_InitSnap()) + self._lib.Solution_InitSnap() def SampleControlDevices(self): - self._check_for_error(self._lib.Solution_SampleControlDevices()) + self._lib.Solution_SampleControlDevices() def Sample_DoControlActions(self): - self._check_for_error(self._lib.Solution_Sample_DoControlActions()) + self._lib.Solution_Sample_DoControlActions() def Solve(self): - self._check_for_error(self._lib.Solution_Solve()) + self._lib.Solution_Solve() def SolveDirect(self): - self._check_for_error(self._lib.Solution_SolveDirect()) + self._lib.Solution_SolveDirect() def SolveNoControl(self): - self._check_for_error(self._lib.Solution_SolveNoControl()) + self._lib.Solution_SolveNoControl() def SolvePflow(self): - self._check_for_error(self._lib.Solution_SolvePflow()) + self._lib.Solution_SolvePflow() def SolvePlusControl(self): - self._check_for_error(self._lib.Solution_SolvePlusControl()) + self._lib.Solution_SolvePlusControl() def SolveSnap(self): - self._check_for_error(self._lib.Solution_SolveSnap()) + self._lib.Solution_SolveSnap() @property def AddType(self) -> int: @@ -103,11 +103,11 @@ def AddType(self) -> int: Original COM help: https://opendss.epri.com/AddType.html ''' - return self._check_for_error(self._lib.Solution_Get_AddType()) + return self._lib.Solution_Get_AddType() @AddType.setter def AddType(self, Value: int): - self._check_for_error(self._lib.Solution_Set_AddType(Value)) + self._lib.Solution_Set_AddType(Value) @property def Algorithm(self) -> SolutionAlgorithms: @@ -116,11 +116,11 @@ def Algorithm(self) -> SolutionAlgorithms: Original COM help: https://opendss.epri.com/Algorithm.html ''' - return SolutionAlgorithms(self._check_for_error(self._lib.Solution_Get_Algorithm())) + return SolutionAlgorithms(self._lib.Solution_Get_Algorithm()) @Algorithm.setter def Algorithm(self, Value: Union[int, SolutionAlgorithms]): - self._check_for_error(self._lib.Solution_Set_Algorithm(Value)) + self._lib.Solution_Set_Algorithm(Value) @property def Capkvar(self) -> float: @@ -129,11 +129,11 @@ def Capkvar(self) -> float: Original COM help: https://opendss.epri.com/Capkvar.html ''' - return self._check_for_error(self._lib.Solution_Get_Capkvar()) + return self._lib.Solution_Get_Capkvar() @Capkvar.setter def Capkvar(self, Value: float): - self._check_for_error(self._lib.Solution_Set_Capkvar(Value)) + self._lib.Solution_Set_Capkvar(Value) @property def ControlActionsDone(self) -> bool: @@ -142,11 +142,11 @@ def ControlActionsDone(self) -> bool: Original COM help: https://opendss.epri.com/ControlActionsDone.html ''' - return self._check_for_error(self._lib.Solution_Get_ControlActionsDone()) != 0 + return self._lib.Solution_Get_ControlActionsDone() @ControlActionsDone.setter def ControlActionsDone(self, Value: bool): - self._check_for_error(self._lib.Solution_Set_ControlActionsDone(Value)) + self._lib.Solution_Set_ControlActionsDone(Value) @property def ControlIterations(self) -> int: @@ -155,11 +155,11 @@ def ControlIterations(self) -> int: Original COM help: https://opendss.epri.com/ControlIterations.html ''' - return self._check_for_error(self._lib.Solution_Get_ControlIterations()) + return self._lib.Solution_Get_ControlIterations() @ControlIterations.setter def ControlIterations(self, Value: int): - self._check_for_error(self._lib.Solution_Set_ControlIterations(Value)) + self._lib.Solution_Set_ControlIterations(Value) @property def ControlMode(self) -> ControlModes: @@ -168,11 +168,11 @@ def ControlMode(self) -> ControlModes: Original COM help: https://opendss.epri.com/ControlMode.html ''' - return ControlModes(self._check_for_error(self._lib.Solution_Get_ControlMode())) + return ControlModes(self._lib.Solution_Get_ControlMode()) @ControlMode.setter def ControlMode(self, Value: Union[int, ControlModes]): - self._check_for_error(self._lib.Solution_Set_ControlMode(Value)) + self._lib.Solution_Set_ControlMode(Value) @property def Converged(self) -> bool: @@ -181,11 +181,11 @@ def Converged(self) -> bool: Original COM help: https://opendss.epri.com/Converged.html ''' - return self._check_for_error(self._lib.Solution_Get_Converged()) != 0 + return self._lib.Solution_Get_Converged() @Converged.setter def Converged(self, Value: bool): - self._check_for_error(self._lib.Solution_Set_Converged(Value)) + self._lib.Solution_Set_Converged(Value) @property def DefaultDaily(self) -> str: @@ -194,14 +194,11 @@ def DefaultDaily(self) -> str: Original COM help: https://opendss.epri.com/DefaultDaily.html ''' - return self._get_string(self._check_for_error(self._lib.Solution_Get_DefaultDaily())) + return self._lib.Solution_Get_DefaultDaily() @DefaultDaily.setter def DefaultDaily(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Solution_Set_DefaultDaily(Value)) + self._lib.Solution_Set_DefaultDaily(Value) @property def DefaultYearly(self) -> str: @@ -210,14 +207,11 @@ def DefaultYearly(self) -> str: Original COM help: https://opendss.epri.com/DefaultYearly.html ''' - return self._get_string(self._check_for_error(self._lib.Solution_Get_DefaultYearly())) + return self._lib.Solution_Get_DefaultYearly() @DefaultYearly.setter def DefaultYearly(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Solution_Set_DefaultYearly(Value)) + self._lib.Solution_Set_DefaultYearly(Value) @property def EventLog(self) -> List[str]: @@ -226,7 +220,7 @@ def EventLog(self) -> List[str]: Original COM help: https://opendss.epri.com/EventLog.html ''' - return self._check_for_error(self._get_string_array(self._lib.Solution_Get_EventLog)) + return self._lib.Solution_Get_EventLog() @property def Frequency(self) -> float: @@ -235,11 +229,11 @@ def Frequency(self) -> float: Original COM help: https://opendss.epri.com/Frequency1.html ''' - return self._check_for_error(self._lib.Solution_Get_Frequency()) + return self._lib.Solution_Get_Frequency() @Frequency.setter def Frequency(self, Value: float): - self._check_for_error(self._lib.Solution_Set_Frequency(Value)) + self._lib.Solution_Set_Frequency(Value) @property def GenMult(self) -> float: @@ -248,11 +242,11 @@ def GenMult(self) -> float: Original COM help: https://opendss.epri.com/GenMult.html ''' - return self._check_for_error(self._lib.Solution_Get_GenMult()) + return self._lib.Solution_Get_GenMult() @GenMult.setter def GenMult(self, Value: float): - self._check_for_error(self._lib.Solution_Set_GenMult(Value)) + self._lib.Solution_Set_GenMult(Value) @property def GenPF(self) -> float: @@ -261,11 +255,11 @@ def GenPF(self) -> float: Original COM help: https://opendss.epri.com/GenPF.html ''' - return self._check_for_error(self._lib.Solution_Get_GenPF()) + return self._lib.Solution_Get_GenPF() @GenPF.setter def GenPF(self, Value: float): - self._check_for_error(self._lib.Solution_Set_GenPF(Value)) + self._lib.Solution_Set_GenPF(Value) @property def GenkW(self) -> float: @@ -274,11 +268,11 @@ def GenkW(self) -> float: Original COM help: https://opendss.epri.com/GenkW.html ''' - return self._check_for_error(self._lib.Solution_Get_GenkW()) + return self._lib.Solution_Get_GenkW() @GenkW.setter def GenkW(self, Value: float): - self._check_for_error(self._lib.Solution_Set_GenkW(Value)) + self._lib.Solution_Set_GenkW(Value) @property def Hour(self) -> int: @@ -287,22 +281,22 @@ def Hour(self) -> int: Original COM help: https://opendss.epri.com/Hour.html ''' - return self._check_for_error(self._lib.Solution_Get_Hour()) + return self._lib.Solution_Get_Hour() @Hour.setter def Hour(self, Value: int): - self._check_for_error(self._lib.Solution_Set_Hour(Value)) + self._lib.Solution_Set_Hour(Value) @property def IntervalHrs(self) -> float: ''' Get/Set the Solution.IntervalHrs variable used for devices that integrate / custom solution algorithms ''' - return self._check_for_error(self._lib.Solution_Get_IntervalHrs()) + return self._lib.Solution_Get_IntervalHrs() @IntervalHrs.setter def IntervalHrs(self, Value: float): - self._check_for_error(self._lib.Solution_Set_IntervalHrs(Value)) + self._lib.Solution_Set_IntervalHrs(Value) @property def Iterations(self) -> int: @@ -311,7 +305,7 @@ def Iterations(self) -> int: Original COM help: https://opendss.epri.com/Iterations.html ''' - return self._check_for_error(self._lib.Solution_Get_Iterations()) + return self._lib.Solution_Get_Iterations() @property def LDCurve(self) -> str: @@ -320,14 +314,11 @@ def LDCurve(self) -> str: Original COM help: https://opendss.epri.com/LDCurve.html ''' - return self._get_string(self._check_for_error(self._lib.Solution_Get_LDCurve())) + return self._lib.Solution_Get_LDCurve() @LDCurve.setter def LDCurve(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Solution_Set_LDCurve(Value)) + self._lib.Solution_Set_LDCurve(Value) @property def LoadModel(self) -> int: @@ -336,11 +327,11 @@ def LoadModel(self) -> int: Original COM help: https://opendss.epri.com/LoadModel.html ''' - return self._check_for_error(self._lib.Solution_Get_LoadModel()) + return self._lib.Solution_Get_LoadModel() @LoadModel.setter def LoadModel(self, Value: int): - self._check_for_error(self._lib.Solution_Set_LoadModel(Value)) + self._lib.Solution_Set_LoadModel(Value) @property def LoadMult(self) -> float: @@ -349,11 +340,11 @@ def LoadMult(self) -> float: Original COM help: https://opendss.epri.com/LoadMult.html ''' - return self._check_for_error(self._lib.Solution_Get_LoadMult()) + return self._lib.Solution_Get_LoadMult() @LoadMult.setter def LoadMult(self, Value: float): - self._check_for_error(self._lib.Solution_Set_LoadMult(Value)) + self._lib.Solution_Set_LoadMult(Value) @property def MaxControlIterations(self) -> int: @@ -362,11 +353,11 @@ def MaxControlIterations(self) -> int: Original COM help: https://opendss.epri.com/MaxControlIterations.html ''' - return self._check_for_error(self._lib.Solution_Get_MaxControlIterations()) + return self._lib.Solution_Get_MaxControlIterations() @MaxControlIterations.setter def MaxControlIterations(self, Value): - self._check_for_error(self._lib.Solution_Set_MaxControlIterations(Value)) + self._lib.Solution_Set_MaxControlIterations(Value) @property def MaxIterations(self) -> int: @@ -375,11 +366,11 @@ def MaxIterations(self) -> int: Original COM help: https://opendss.epri.com/MaxIterations.html ''' - return self._check_for_error(self._lib.Solution_Get_MaxIterations()) + return self._lib.Solution_Get_MaxIterations() @MaxIterations.setter def MaxIterations(self, Value: int): - self._check_for_error(self._lib.Solution_Set_MaxIterations(Value)) + self._lib.Solution_Set_MaxIterations(Value) @property def MinIterations(self) -> int: @@ -388,11 +379,11 @@ def MinIterations(self) -> int: Original COM help: https://opendss.epri.com/MinIterations.html ''' - return self._check_for_error(self._lib.Solution_Get_MinIterations()) + return self._lib.Solution_Get_MinIterations() @MinIterations.setter def MinIterations(self, Value: int): - self._check_for_error(self._lib.Solution_Set_MinIterations(Value)) + self._lib.Solution_Set_MinIterations(Value) @property def Mode(self) -> SolveModes: @@ -401,11 +392,11 @@ def Mode(self) -> SolveModes: Original COM help: https://opendss.epri.com/Mode2.html ''' - return SolveModes(self._check_for_error(self._lib.Solution_Get_Mode())) + return SolveModes(self._lib.Solution_Get_Mode()) @Mode.setter def Mode(self, Value: Union[int, SolveModes]): - self._check_for_error(self._lib.Solution_Set_Mode(Value)) + self._lib.Solution_Set_Mode(Value) @property def ModeID(self) -> str: @@ -414,7 +405,7 @@ def ModeID(self) -> str: Original COM help: https://opendss.epri.com/ModeID.html ''' - return self._get_string(self._check_for_error(self._lib.Solution_Get_ModeID())) + return self._lib.Solution_Get_ModeID() @property def MostIterationsDone(self) -> int: @@ -423,7 +414,7 @@ def MostIterationsDone(self) -> int: Original COM help: https://opendss.epri.com/MostIterationsDone.html ''' - return self._check_for_error(self._lib.Solution_Get_MostIterationsDone()) + return self._lib.Solution_Get_MostIterationsDone() @property def Number(self) -> int: @@ -432,11 +423,11 @@ def Number(self) -> int: Original COM help: https://opendss.epri.com/Number1.html ''' - return self._check_for_error(self._lib.Solution_Get_Number()) + return self._lib.Solution_Get_Number() @Number.setter def Number(self, Value: int): - self._check_for_error(self._lib.Solution_Set_Number(Value)) + self._lib.Solution_Set_Number(Value) @property def Process_Time(self) -> float: @@ -445,7 +436,7 @@ def Process_Time(self) -> float: Original COM help: https://opendss.epri.com/Process_Time.html ''' - return self._check_for_error(self._lib.Solution_Get_Process_Time()) + return self._lib.Solution_Get_Process_Time() @property def Random(self) -> int: @@ -454,11 +445,11 @@ def Random(self) -> int: Original COM help: https://opendss.epri.com/Random.html ''' - return self._check_for_error(self._lib.Solution_Get_Random()) + return self._lib.Solution_Get_Random() @Random.setter def Random(self, Value: int): - self._check_for_error(self._lib.Solution_Set_Random(Value)) + self._lib.Solution_Set_Random(Value) @property def Seconds(self) -> float: @@ -467,11 +458,11 @@ def Seconds(self) -> float: Original COM help: https://opendss.epri.com/Seconds.html ''' - return self._check_for_error(self._lib.Solution_Get_Seconds()) + return self._lib.Solution_Get_Seconds() @Seconds.setter def Seconds(self, Value: float): - self._check_for_error(self._lib.Solution_Set_Seconds(Value)) + self._lib.Solution_Set_Seconds(Value) @property def StepSize(self) -> float: @@ -480,11 +471,11 @@ def StepSize(self) -> float: Original COM help: https://opendss.epri.com/StepSize.html ''' - return self._check_for_error(self._lib.Solution_Get_StepSize()) + return self._lib.Solution_Get_StepSize() @StepSize.setter def StepSize(self, Value: float): - self._check_for_error(self._lib.Solution_Set_StepSize(Value)) + self._lib.Solution_Set_StepSize(Value) @property def SystemYChanged(self) -> bool: @@ -493,7 +484,7 @@ def SystemYChanged(self) -> bool: Original COM help: https://opendss.epri.com/SystemYChanged.html ''' - return self._check_for_error(self._lib.Solution_Get_SystemYChanged() != 0) + return self._lib.Solution_Get_SystemYChanged() @property def Time_of_Step(self) -> float: @@ -502,7 +493,7 @@ def Time_of_Step(self) -> float: Original COM help: https://opendss.epri.com/Time_of_Step.html ''' - return self._check_for_error(self._lib.Solution_Get_Time_of_Step()) + return self._lib.Solution_Get_Time_of_Step() @property def Tolerance(self) -> float: @@ -511,11 +502,11 @@ def Tolerance(self) -> float: Original COM help: https://opendss.epri.com/Tolerance.html ''' - return self._check_for_error(self._lib.Solution_Get_Tolerance()) + return self._lib.Solution_Get_Tolerance() @Tolerance.setter def Tolerance(self, Value: float): - self._check_for_error(self._lib.Solution_Set_Tolerance(Value)) + self._lib.Solution_Set_Tolerance(Value) @property def Total_Time(self) -> float: @@ -526,11 +517,11 @@ def Total_Time(self) -> float: Original COM help: https://opendss.epri.com/Total_Time.html ''' - return self._check_for_error(self._lib.Solution_Get_Total_Time()) + return self._lib.Solution_Get_Total_Time() @Total_Time.setter def Total_Time(self, Value: float): - self._check_for_error(self._lib.Solution_Set_Total_Time(Value)) + self._lib.Solution_Set_Total_Time(Value) @property def Totaliterations(self) -> int: @@ -539,7 +530,7 @@ def Totaliterations(self) -> int: Original COM help: https://opendss.epri.com/Totaliterations.html ''' - return self._check_for_error(self._lib.Solution_Get_Totaliterations()) + return self._lib.Solution_Get_Totaliterations() @property def Year(self) -> int: @@ -548,11 +539,11 @@ def Year(self) -> int: Original COM help: https://opendss.epri.com/Year.html ''' - return self._check_for_error(self._lib.Solution_Get_Year()) + return self._lib.Solution_Get_Year() @Year.setter def Year(self, Value: int): - self._check_for_error(self._lib.Solution_Set_Year(Value)) + self._lib.Solution_Set_Year(Value) @property def dblHour(self) -> float: @@ -561,11 +552,11 @@ def dblHour(self) -> float: Original COM help: https://opendss.epri.com/dblHour1.html ''' - return self._check_for_error(self._lib.Solution_Get_dblHour()) + return self._lib.Solution_Get_dblHour() @dblHour.setter def dblHour(self, Value: float): - self._check_for_error(self._lib.Solution_Set_dblHour(Value)) + self._lib.Solution_Set_dblHour(Value) @property def pctGrowth(self) -> float: @@ -574,11 +565,11 @@ def pctGrowth(self) -> float: Original COM help: https://opendss.epri.com/pctGrowth.html ''' - return self._check_for_error(self._lib.Solution_Get_pctGrowth()) + return self._lib.Solution_Get_pctGrowth() @pctGrowth.setter def pctGrowth(self, Value: float): - self._check_for_error(self._lib.Solution_Set_pctGrowth(Value)) + self._lib.Solution_Set_pctGrowth(Value) @property def StepsizeHr(self) -> float: @@ -587,7 +578,7 @@ def StepsizeHr(self) -> float: @StepsizeHr.setter def StepsizeHr(self, Value: float): - self._check_for_error(self._lib.Solution_Set_StepsizeHr(Value)) + self._lib.Solution_Set_StepsizeHr(Value) @property def StepsizeMin(self) -> float: @@ -596,7 +587,7 @@ def StepsizeMin(self) -> float: @StepsizeMin.setter def StepsizeMin(self, Value: float): - self._check_for_error(self._lib.Solution_Set_StepsizeMin(Value)) + self._lib.Solution_Set_StepsizeMin(Value) # The following are officially available only in v8 @property @@ -611,8 +602,7 @@ def BusLevels(self) -> Int32Array: Original COM help: https://opendss.epri.com/BusLevels.html ''' - self._check_for_error(self._lib.Solution_Get_BusLevels_GR()) - return self._get_int32_gr_array() + return self._lib.Solution_Get_BusLevels_GR() @property def IncMatrix(self) -> Int32Array: @@ -628,8 +618,7 @@ def IncMatrix(self) -> Int32Array: Original COM help: https://opendss.epri.com/IncMatrix.html ''' #TODO: expose as sparse matrix - self._check_for_error(self._lib.Solution_Get_IncMatrix_GR()) - return self._get_int32_gr_array() + return self._lib.Solution_Get_IncMatrix_GR() @property def IncMatrixCols(self) -> List[str]: @@ -638,7 +627,7 @@ def IncMatrixCols(self) -> List[str]: Original COM help: https://opendss.epri.com/IncMatrixCols.html ''' - return self._check_for_error(self._get_string_array(self._lib.Solution_Get_IncMatrixCols)) + return self._lib.Solution_Get_IncMatrixCols() @property def IncMatrixRows(self) -> List[str]: @@ -647,7 +636,7 @@ def IncMatrixRows(self) -> List[str]: Original COM help: https://opendss.epri.com/IncMatrixRows.html ''' - return self._check_for_error(self._get_string_array(self._lib.Solution_Get_IncMatrixRows)) + return self._lib.Solution_Get_IncMatrixRows() @property def Laplacian(self) -> Int32Array: @@ -664,8 +653,7 @@ def Laplacian(self) -> Int32Array: Original COM help: https://opendss.epri.com/Laplacian.html ''' #TODO: expose as sparse matrix - self._check_for_error(self._lib.Solution_Get_Laplacian_GR()) - return self._get_int32_gr_array() + return self._lib.Solution_Get_Laplacian_GR() def SolveAll(self): ''' @@ -673,5 +661,5 @@ def SolveAll(self): Original COM help: https://opendss.epri.com/SolveAll.html ''' - self._check_for_error(self._lib.Solution_SolveAll()) + self._lib.Solution_SolveAll() diff --git a/dss/IStorages.py b/dss/IStorages.py index a2de257c..c27c92be 100644 --- a/dss/IStorages.py +++ b/dss/IStorages.py @@ -47,22 +47,22 @@ class IStorages(Iterable): @property def puSOC(self) -> float: '''Per unit state of charge''' - return self._check_for_error(self._lib.Storages_Get_puSOC()) + return self._lib.Storages_Get_puSOC() @puSOC.setter def puSOC(self, Value: float): - self._check_for_error(self._lib.Storages_Set_puSOC(Value)) + self._lib.Storages_Set_puSOC(Value) @property def State(self) -> StorageStates: ''' Get/set state: 0=Idling; 1=Discharging; -1=Charging; ''' - return StorageStates(self._check_for_error(self._lib.Storages_Get_State())) + return StorageStates(self._lib.Storages_Get_State()) @State.setter def State(self, Value: Union[int, StorageStates]): - self._check_for_error(self._lib.Storages_Set_State(Value)) + self._lib.Storages_Set_State(Value) @property def RegisterNames(self) -> List[str]: @@ -71,90 +71,89 @@ def RegisterNames(self) -> List[str]: See also the enum `GeneratorRegisters`. ''' - return self._check_for_error(self._get_string_array(self._lib.Storages_Get_RegisterNames)) + return self._lib.Storages_Get_RegisterNames() @property def RegisterValues(self) -> Float64Array: '''Array of values in Storage registers.''' - self._check_for_error(self._lib.Storages_Get_RegisterValues_GR()) - return self._get_float64_gr_array() + return self._lib.Storages_Get_RegisterValues_GR() @property def AmpLimit(self) -> float: ''' Current limit per phase for the IBR when operating in GFM mode. ''' - return self._check_for_error(self._lib.Storages_Get_AmpLimit()) + return self._lib.Storages_Get_AmpLimit() @AmpLimit.setter def AmpLimit(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_AmpLimit(Value)) + self._lib.Storages_Set_AmpLimit(Value) @property def AmpLimitGain(self) -> float: ''' Use it for fine tuning the current limiter when active. ''' - return self._check_for_error(self._lib.Storages_Get_AmpLimitGain()) + return self._lib.Storages_Get_AmpLimitGain() @AmpLimitGain.setter def AmpLimitGain(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_AmpLimitGain(Value)) + self._lib.Storages_Set_AmpLimitGain(Value) @property def ChargeTrigger(self) -> float: ''' Dispatch trigger value for charging the Storage. ''' - return self._check_for_error(self._lib.Storages_Get_ChargeTrigger()) + return self._lib.Storages_Get_ChargeTrigger() @ChargeTrigger.setter def ChargeTrigger(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_ChargeTrigger(Value)) + self._lib.Storages_Set_ChargeTrigger(Value) @property def ControlMode(self) -> int: ''' Control mode for the inverter. It can be one of {GFM = 1 | GFL* = 0}. ''' - return self._check_for_error(self._lib.Storages_Get_ControlMode()) + return self._lib.Storages_Get_ControlMode() @ControlMode.setter def ControlMode(self, Value: int) -> None: - self._check_for_error(self._lib.Storages_Set_ControlMode(Value)) + self._lib.Storages_Set_ControlMode(Value) @property def DischargeTrigger(self) -> float: ''' Dispatch trigger value for discharging the Storage. ''' - return self._check_for_error(self._lib.Storages_Get_DischargeTrigger()) + return self._lib.Storages_Get_DischargeTrigger() @DischargeTrigger.setter def DischargeTrigger(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_DischargeTrigger(Value)) + self._lib.Storages_Set_DischargeTrigger(Value) @property def EffCharge(self) -> float: ''' Percentage efficiency for CHARGING the Storage element. ''' - return self._check_for_error(self._lib.Storages_Get_EffCharge()) + return self._lib.Storages_Get_EffCharge() @EffCharge.setter def EffCharge(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_EffCharge(Value)) + self._lib.Storages_Set_EffCharge(Value) @property def EffDischarge(self) -> float: ''' Percentage efficiency for DISCHARGING the Storage element. ''' - return self._check_for_error(self._lib.Storages_Get_EffDischarge()) + return self._lib.Storages_Get_EffDischarge() @EffDischarge.setter def EffDischarge(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_EffDischarge(Value)) + self._lib.Storages_Set_EffDischarge(Value) @property def Kp(self) -> float: @@ -162,88 +161,88 @@ def Kp(self) -> float: Proportional gain for the PI controller within the inverter. Use it to modify the controller response in dynamics simulation mode. ''' - return self._check_for_error(self._lib.Storages_Get_Kp()) + return self._lib.Storages_Get_Kp() @Kp.setter def Kp(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_Kp(Value)) + self._lib.Storages_Set_Kp(Value) @property def kV(self) -> float: ''' Nominal rated (1.0 per unit) voltage, kV, for Storage element. ''' - return self._check_for_error(self._lib.Storages_Get_kV()) + return self._lib.Storages_Get_kV() @kV.setter def kV(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_kV(Value)) + self._lib.Storages_Set_kV(Value) @property def kVA(self) -> float: ''' Inverter nameplate capability (in kVA). Used as the base for Dynamics mode and Harmonics mode values. ''' - return self._check_for_error(self._lib.Storages_Get_kVA()) + return self._lib.Storages_Get_kVA() @kVA.setter def kVA(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_kVA(Value)) + self._lib.Storages_Set_kVA(Value) @property def kvar(self) -> float: ''' Get/set the requested kvar value. Final kvar is subjected to the inverter ratings. Sets inverter to operate in constant kvar mode. ''' - return self._check_for_error(self._lib.Storages_Get_kvar()) + return self._lib.Storages_Get_kvar() @kvar.setter def kvar(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_kvar(Value)) + self._lib.Storages_Set_kvar(Value) @property def kVDC(self) -> float: ''' Rated voltage (kV) at the input of the inverter while the storage is discharging ''' - return self._check_for_error(self._lib.Storages_Get_kVDC()) + return self._lib.Storages_Get_kVDC() @kVDC.setter def kVDC(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_kVDC(Value)) + self._lib.Storages_Set_kVDC(Value) @property def kW(self) -> float: ''' Get/set the requested kW value. Final kW is subjected to the inverter ratings. ''' - return self._check_for_error(self._lib.Storages_Get_kW()) + return self._lib.Storages_Get_kW() @kW.setter def kW(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_kW(Value)) + self._lib.Storages_Set_kW(Value) @property def kWhRated(self) -> float: ''' Rated Storage capacity in kWh. ''' - return self._check_for_error(self._lib.Storages_Get_kWhRated()) + return self._lib.Storages_Get_kWhRated() @kWhRated.setter def kWhRated(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_kWhRated(Value)) + self._lib.Storages_Set_kWhRated(Value) @property def kWRated(self) -> float: ''' kW rating of power output. Base for Loadshapes when DispMode=Follow. Sets kVA property if it has not been specified yet. ''' - return self._check_for_error(self._lib.Storages_Get_kWRated()) + return self._lib.Storages_Get_kWRated() @kWRated.setter def kWRated(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_kWRated(Value)) + self._lib.Storages_Set_kWRated(Value) @property def LimitCurrent(self) -> bool: @@ -251,72 +250,72 @@ def LimitCurrent(self) -> bool: Limits current magnitude to Vminpu value for both 1-phase and 3-phase Storage similar to Generator Model 7. For 3-phase, limits the positive-sequence current but not the negative-sequence." ''' - return self._check_for_error(self._lib.Storages_Get_LimitCurrent()) != 0 + return self._lib.Storages_Get_LimitCurrent() @LimitCurrent.setter def LimitCurrent(self, Value: bool) -> None: - self._check_for_error(self._lib.Storages_Set_LimitCurrent(Value)) + self._lib.Storages_Set_LimitCurrent(Value) @property def PF(self) -> float: ''' Get/set the requested PF value. ''' - return self._check_for_error(self._lib.Storages_Get_PF()) + return self._lib.Storages_Get_PF() @PF.setter def PF(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_PF(Value)) + self._lib.Storages_Set_PF(Value) @property def PITol(self) -> float: ''' Tolerance (%) for the closed loop controller of the inverter ''' - return self._check_for_error(self._lib.Storages_Get_PITol()) + return self._lib.Storages_Get_PITol() @PITol.setter def PITol(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_PITol(Value)) + self._lib.Storages_Set_PITol(Value) @property def SafeMode(self) -> int: ''' (Read only) Indicates whether the inverter entered (Yes) or not (No) into Safe Mode. ''' - return self._check_for_error(self._lib.Storages_Get_SafeMode()) + return self._lib.Storages_Get_SafeMode() @property def SafeVoltage(self) -> float: ''' Indicates the voltage level (%) respect to the base voltage level for which the Inverter will operate. ''' - return self._check_for_error(self._lib.Storages_Get_SafeVoltage()) + return self._lib.Storages_Get_SafeVoltage() @SafeVoltage.setter def SafeVoltage(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_SafeVoltage(Value)) + self._lib.Storages_Set_SafeVoltage(Value) @property def TimeChargeTrig(self) -> float: ''' Time of day in fractional hours (0230 = 2.5) at which Storage element will automatically go into charge state. ''' - return self._check_for_error(self._lib.Storages_Get_TimeChargeTrig()) + return self._lib.Storages_Get_TimeChargeTrig() @TimeChargeTrig.setter def TimeChargeTrig(self, Value: float) -> None: - self._check_for_error(self._lib.Storages_Set_TimeChargeTrig(Value)) + self._lib.Storages_Set_TimeChargeTrig(Value) @property def VarFollowInverter(self) -> int: ''' Indicates if the reactive power generation/absorption does not respect the inverter status ''' - return self._check_for_error(self._lib.Storages_Get_VarFollowInverter()) + return self._lib.Storages_Get_VarFollowInverter() @VarFollowInverter.setter def VarFollowInverter(self, Value: int) -> None: - self._check_for_error(self._lib.Storages_Set_VarFollowInverter(Value)) + self._lib.Storages_Set_VarFollowInverter(Value) diff --git a/dss/ISwtControls.py b/dss/ISwtControls.py index b1e6a689..720e74b9 100644 --- a/dss/ISwtControls.py +++ b/dss/ISwtControls.py @@ -22,7 +22,7 @@ class ISwtControls(Iterable): ] def Reset(self): - self._check_for_error(self._lib.SwtControls_Reset()) + self._lib.SwtControls_Reset() @property def Action(self) -> int: @@ -31,11 +31,11 @@ def Action(self) -> int: Original COM help: https://opendss.epri.com/Action1.html ''' - return self._check_for_error(self._lib.SwtControls_Get_Action()) + return self._lib.SwtControls_Get_Action() @Action.setter def Action(self, Value: int): - self._check_for_error(self._lib.SwtControls_Set_Action(Value)) + self._lib.SwtControls_Set_Action(Value) @property def Delay(self) -> float: @@ -44,11 +44,11 @@ def Delay(self) -> float: Original COM help: https://opendss.epri.com/Delay3.html ''' - return self._check_for_error(self._lib.SwtControls_Get_Delay()) + return self._lib.SwtControls_Get_Delay() @Delay.setter def Delay(self, Value: float): - self._check_for_error(self._lib.SwtControls_Set_Delay(Value)) + self._lib.SwtControls_Set_Delay(Value) @property def IsLocked(self) -> bool: @@ -57,22 +57,22 @@ def IsLocked(self) -> bool: Original COM help: https://opendss.epri.com/IsLocked.html ''' - return self._check_for_error(self._lib.SwtControls_Get_IsLocked()) != 0 + return self._lib.SwtControls_Get_IsLocked() @IsLocked.setter def IsLocked(self, Value: bool): - self._check_for_error(self._lib.SwtControls_Set_IsLocked(Value)) + self._lib.SwtControls_Set_IsLocked(Value) @property def NormalState(self) -> ActionCodes: ''' Get/set Normal state of switch (see ActionCodes) dssActionOpen or dssActionClose ''' - return ActionCodes(self._check_for_error(self._lib.SwtControls_Get_NormalState())) + return ActionCodes(self._lib.SwtControls_Get_NormalState()) @NormalState.setter def NormalState(self, Value: Union[int, ActionCodes]): - self._check_for_error(self._lib.SwtControls_Set_NormalState(Value)) + self._lib.SwtControls_Set_NormalState(Value) @property def State(self) -> int: @@ -81,11 +81,11 @@ def State(self) -> int: Original COM help: https://opendss.epri.com/State.html ''' - return self._check_for_error(self._lib.SwtControls_Get_State()) + return self._lib.SwtControls_Get_State() @State.setter def State(self, Value: int): - self._check_for_error(self._lib.SwtControls_Set_State(Value)) + self._lib.SwtControls_Set_State(Value) @property def SwitchedObj(self) -> str: @@ -94,14 +94,11 @@ def SwitchedObj(self) -> str: Original COM help: https://opendss.epri.com/SwitchedObj3.html ''' - return self._get_string(self._check_for_error(self._lib.SwtControls_Get_SwitchedObj())) + return self._lib.SwtControls_Get_SwitchedObj() @SwitchedObj.setter def SwitchedObj(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.SwtControls_Set_SwitchedObj(Value)) + self._lib.SwtControls_Set_SwitchedObj(Value) @property def SwitchedTerm(self) -> int: @@ -110,9 +107,9 @@ def SwitchedTerm(self) -> int: Original COM help: https://opendss.epri.com/SwitchedTerm3.html ''' - return self._check_for_error(self._lib.SwtControls_Get_SwitchedTerm()) + return self._lib.SwtControls_Get_SwitchedTerm() @SwitchedTerm.setter def SwitchedTerm(self, Value: int): - self._check_for_error(self._lib.SwtControls_Set_SwitchedTerm(Value)) + self._lib.SwtControls_Set_SwitchedTerm(Value) diff --git a/dss/ITSData.py b/dss/ITSData.py index d3ca9799..13a4a8b5 100644 --- a/dss/ITSData.py +++ b/dss/ITSData.py @@ -37,137 +37,137 @@ class ITSData(Iterable): @property def EmergAmps(self) -> float: '''Emergency ampere rating''' - return self._check_for_error(self._lib.TSData_Get_EmergAmps()) + return self._lib.TSData_Get_EmergAmps() @EmergAmps.setter def EmergAmps(self, Value: float): - self._check_for_error(self._lib.TSData_Set_EmergAmps(Value)) + self._lib.TSData_Set_EmergAmps(Value) @property def NormAmps(self) -> float: '''Normal Ampere rating''' - return self._check_for_error(self._lib.TSData_Get_NormAmps()) + return self._lib.TSData_Get_NormAmps() @NormAmps.setter def NormAmps(self, Value: float): - self._check_for_error(self._lib.TSData_Set_NormAmps(Value)) + self._lib.TSData_Set_NormAmps(Value) @property def Rdc(self) -> float: - return self._check_for_error(self._lib.TSData_Get_Rdc()) + return self._lib.TSData_Get_Rdc() @Rdc.setter def Rdc(self, Value: float): - self._check_for_error(self._lib.TSData_Set_Rdc(Value)) + self._lib.TSData_Set_Rdc(Value) @property def Rac(self) -> float: - return self._check_for_error(self._lib.TSData_Get_Rac()) + return self._lib.TSData_Get_Rac() @Rac.setter def Rac(self, Value: float): - self._check_for_error(self._lib.TSData_Set_Rac(Value)) + self._lib.TSData_Set_Rac(Value) @property def GMRac(self) -> float: - return self._check_for_error(self._lib.TSData_Get_GMRac()) + return self._lib.TSData_Get_GMRac() @GMRac.setter def GMRac(self, Value: float): - self._check_for_error(self._lib.TSData_Set_GMRac(Value)) + self._lib.TSData_Set_GMRac(Value) @property def GMRUnits(self) -> int: - return self._check_for_error(self._lib.TSData_Get_GMRUnits()) + return self._lib.TSData_Get_GMRUnits() @GMRUnits.setter def GMRUnits(self, Value: int): - self._check_for_error(self._lib.TSData_Set_GMRUnits(Value)) + self._lib.TSData_Set_GMRUnits(Value) @property def Radius(self) -> float: - return self._check_for_error(self._lib.TSData_Get_Radius()) + return self._lib.TSData_Get_Radius() @Radius.setter def Radius(self, Value: float): - self._check_for_error(self._lib.TSData_Set_Radius(Value)) + self._lib.TSData_Set_Radius(Value) @property def RadiusUnits(self) -> int: - return self._check_for_error(self._lib.TSData_Get_RadiusUnits()) + return self._lib.TSData_Get_RadiusUnits() @RadiusUnits.setter def RadiusUnits(self, Value: int): - self._check_for_error(self._lib.TSData_Set_RadiusUnits(Value)) + self._lib.TSData_Set_RadiusUnits(Value) @property def ResistanceUnits(self) -> int: - return self._check_for_error(self._lib.TSData_Get_ResistanceUnits()) + return self._lib.TSData_Get_ResistanceUnits() @ResistanceUnits.setter def ResistanceUnits(self, Value: int): - self._check_for_error(self._lib.TSData_Set_ResistanceUnits(Value)) + self._lib.TSData_Set_ResistanceUnits(Value) @property def Diameter(self) -> float: - return self._check_for_error(self._lib.TSData_Get_Diameter()) + return self._lib.TSData_Get_Diameter() @Diameter.setter def Diameter(self, Value: float): - self._check_for_error(self._lib.TSData_Set_Diameter(Value)) + self._lib.TSData_Set_Diameter(Value) @property def EpsR(self) -> float: - return self._check_for_error(self._lib.TSData_Get_EpsR()) + return self._lib.TSData_Get_EpsR() @EpsR.setter def EpsR(self, Value: float): - self._check_for_error(self._lib.TSData_Set_EpsR(Value)) + self._lib.TSData_Set_EpsR(Value) @property def InsLayer(self) -> float: - return self._check_for_error(self._lib.TSData_Get_InsLayer()) + return self._lib.TSData_Get_InsLayer() @InsLayer.setter def InsLayer(self, Value: float): - self._check_for_error(self._lib.TSData_Set_InsLayer(Value)) + self._lib.TSData_Set_InsLayer(Value) @property def DiaIns(self) -> float: - return self._check_for_error(self._lib.TSData_Get_DiaIns()) + return self._lib.TSData_Get_DiaIns() @DiaIns.setter def DiaIns(self, Value: float): - self._check_for_error(self._lib.TSData_Set_DiaIns(Value)) + self._lib.TSData_Set_DiaIns(Value) @property def DiaCable(self) -> float: - return self._check_for_error(self._lib.TSData_Get_DiaCable()) + return self._lib.TSData_Get_DiaCable() @DiaCable.setter def DiaCable(self, Value: float): - self._check_for_error(self._lib.TSData_Set_DiaCable(Value)) + self._lib.TSData_Set_DiaCable(Value) @property def DiaShield(self) -> float: - return self._check_for_error(self._lib.TSData_Get_DiaShield()) + return self._lib.TSData_Get_DiaShield() @DiaShield.setter def DiaShield(self, Value: float): - self._check_for_error(self._lib.TSData_Set_DiaShield(Value)) + self._lib.TSData_Set_DiaShield(Value) @property def TapeLayer(self) -> float: - return self._check_for_error(self._lib.TSData_Get_TapeLayer()) + return self._lib.TSData_Get_TapeLayer() @TapeLayer.setter def TapeLayer(self, Value: float): - self._check_for_error(self._lib.TSData_Set_TapeLayer(Value)) + self._lib.TSData_Set_TapeLayer(Value) @property def TapeLap(self) -> float: - return self._check_for_error(self._lib.TSData_Get_TapeLap()) + return self._lib.TSData_Get_TapeLap() @TapeLap.setter def TapeLap(self, Value: float): - self._check_for_error(self._lib.TSData_Set_TapeLap(Value)) + self._lib.TSData_Set_TapeLap(Value) diff --git a/dss/IText.py b/dss/IText.py index 8faee582..b34fa88b 100644 --- a/dss/IText.py +++ b/dss/IText.py @@ -14,14 +14,11 @@ def Command(self) -> str: Original COM help: https://opendss.epri.com/Command1.html ''' - return self._get_string(self._check_for_error(self._lib.Text_Get_Command())) + return self._lib.Text_Get_Command() @Command.setter def Command(self, Value: str): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Text_Set_Command(Value)) + self._lib.Text_Set_Command(Value) @property def Result(self) -> str: @@ -30,7 +27,7 @@ def Result(self) -> str: Original COM help: https://opendss.epri.com/Result.html ''' - return self._get_string(self._check_for_error(self._lib.Text_Get_Result())) + return self._lib.Text_Get_Result() def Commands(self, Value: Union[AnyStr, List[AnyStr]]): ''' @@ -42,10 +39,7 @@ def Commands(self, Value: Union[AnyStr, List[AnyStr]]): **(API Extension)** ''' if isinstance(Value, str) or isinstance(Value, bytes): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Text_CommandBlock(Value)) + self._lib.Text_CommandBlock(Value) else: - self._check_for_error(self._set_string_array(self._lib.Text_CommandArray, Value)) + self._set_string_array(self._lib.Text_CommandArray, Value) diff --git a/dss/ITopology.py b/dss/ITopology.py index c85f2d80..44d66422 100644 --- a/dss/ITopology.py +++ b/dss/ITopology.py @@ -26,7 +26,7 @@ def ActiveBranch(self) -> int: Original COM help: https://opendss.epri.com/ActiveBranch.html ''' - return self._check_for_error(self._lib.Topology_Get_ActiveBranch()) + return self._lib.Topology_Get_ActiveBranch() @property def ActiveLevel(self) -> int: @@ -35,7 +35,7 @@ def ActiveLevel(self) -> int: Original COM help: https://opendss.epri.com/ActiveLevel.html ''' - return self._check_for_error(self._lib.Topology_Get_ActiveLevel()) + return self._lib.Topology_Get_ActiveLevel() @property def AllIsolatedBranches(self) -> List[str]: @@ -44,7 +44,7 @@ def AllIsolatedBranches(self) -> List[str]: Original COM help: https://opendss.epri.com/AllIsolatedBranches.html ''' - return self._check_for_error(self._get_string_array(self._lib.Topology_Get_AllIsolatedBranches)) + return self._lib.Topology_Get_AllIsolatedBranches() @property def AllIsolatedLoads(self) -> List[str]: @@ -53,7 +53,7 @@ def AllIsolatedLoads(self) -> List[str]: Original COM help: https://opendss.epri.com/AllIsolatedLoads.html ''' - return self._check_for_error(self._get_string_array(self._lib.Topology_Get_AllIsolatedLoads)) + return self._lib.Topology_Get_AllIsolatedLoads() @property def AllLoopedPairs(self) -> List[str]: @@ -62,7 +62,7 @@ def AllLoopedPairs(self) -> List[str]: Original COM help: https://opendss.epri.com/AllLoopedPairs.html ''' - return self._check_for_error(self._get_string_array(self._lib.Topology_Get_AllLoopedPairs)) + return self._lib.Topology_Get_AllLoopedPairs() @property def BackwardBranch(self) -> int: @@ -71,7 +71,7 @@ def BackwardBranch(self) -> int: Original COM help: https://opendss.epri.com/BackwardBranch.html ''' - return self._check_for_error(self._lib.Topology_Get_BackwardBranch()) + return self._lib.Topology_Get_BackwardBranch() @property def BranchName(self) -> str: @@ -80,14 +80,11 @@ def BranchName(self) -> str: Original COM help: https://opendss.epri.com/BranchName.html ''' - return self._get_string(self._check_for_error(self._lib.Topology_Get_BranchName())) + return self._lib.Topology_Get_BranchName() @BranchName.setter def BranchName(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Topology_Set_BranchName(Value)) + self._lib.Topology_Set_BranchName(Value) @property def BusName(self) -> str: @@ -96,14 +93,11 @@ def BusName(self) -> str: Original COM help: https://opendss.epri.com/BusName.html ''' - return self._get_string(self._check_for_error(self._lib.Topology_Get_BusName())) + return self._lib.Topology_Get_BusName() @BusName.setter def BusName(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Topology_Set_BusName(Value)) + self._lib.Topology_Set_BusName(Value) @property def First(self) -> int: @@ -112,7 +106,7 @@ def First(self) -> int: Original COM help: https://opendss.epri.com/First19.html ''' - return self._check_for_error(self._lib.Topology_Get_First()) + return self._lib.Topology_Get_First() @property def FirstLoad(self) -> int: @@ -121,7 +115,7 @@ def FirstLoad(self) -> int: Original COM help: https://opendss.epri.com/FirstLoad.html ''' - return self._check_for_error(self._lib.Topology_Get_FirstLoad()) + return self._lib.Topology_Get_FirstLoad() @property def ForwardBranch(self) -> int: @@ -130,7 +124,7 @@ def ForwardBranch(self) -> int: Original COM help: https://opendss.epri.com/ForwardBranch.html ''' - return self._check_for_error(self._lib.Topology_Get_ForwardBranch()) + return self._lib.Topology_Get_ForwardBranch() @property def LoopedBranch(self) -> int: @@ -139,7 +133,7 @@ def LoopedBranch(self) -> int: Original COM help: https://opendss.epri.com/LoopedBranch.html ''' - return self._check_for_error(self._lib.Topology_Get_LoopedBranch()) + return self._lib.Topology_Get_LoopedBranch() @property def Next(self) -> int: @@ -148,7 +142,7 @@ def Next(self) -> int: Original COM help: https://opendss.epri.com/Next18.html ''' - return self._check_for_error(self._lib.Topology_Get_Next()) + return self._lib.Topology_Get_Next() @property def NextLoad(self) -> int: @@ -157,7 +151,7 @@ def NextLoad(self) -> int: Original COM help: https://opendss.epri.com/NextLoad.html ''' - return self._check_for_error(self._lib.Topology_Get_NextLoad()) + return self._lib.Topology_Get_NextLoad() @property def NumIsolatedBranches(self) -> int: @@ -166,7 +160,7 @@ def NumIsolatedBranches(self) -> int: Original COM help: https://opendss.epri.com/NumIsolatedBranches.html ''' - return self._check_for_error(self._lib.Topology_Get_NumIsolatedBranches()) + return self._lib.Topology_Get_NumIsolatedBranches() @property def NumIsolatedLoads(self) -> int: @@ -175,7 +169,7 @@ def NumIsolatedLoads(self) -> int: Original COM help: https://opendss.epri.com/NumIsolatedLoads.html ''' - return self._check_for_error(self._lib.Topology_Get_NumIsolatedLoads()) + return self._lib.Topology_Get_NumIsolatedLoads() @property def NumLoops(self) -> int: @@ -184,7 +178,7 @@ def NumLoops(self) -> int: Original COM help: https://opendss.epri.com/NumLoops.html ''' - return self._check_for_error(self._lib.Topology_Get_NumLoops()) + return self._lib.Topology_Get_NumLoops() @property def ParallelBranch(self) -> int: @@ -193,5 +187,5 @@ def ParallelBranch(self) -> int: Original COM help: https://opendss.epri.com/ParallelBranch.html ''' - return self._check_for_error(self._lib.Topology_Get_ParallelBranch()) + return self._lib.Topology_Get_ParallelBranch() diff --git a/dss/ITransformers.py b/dss/ITransformers.py index c07055ea..5ae5f9d2 100644 --- a/dss/ITransformers.py +++ b/dss/ITransformers.py @@ -43,11 +43,11 @@ def IsDelta(self) -> bool: Original COM help: https://opendss.epri.com/IsDelta3.html ''' - return self._check_for_error(self._lib.Transformers_Get_IsDelta()) != 0 + return self._lib.Transformers_Get_IsDelta() @IsDelta.setter def IsDelta(self, Value: bool): - self._check_for_error(self._lib.Transformers_Set_IsDelta(Value)) + self._lib.Transformers_Set_IsDelta(Value) @property def MaxTap(self) -> float: @@ -56,11 +56,11 @@ def MaxTap(self) -> float: Original COM help: https://opendss.epri.com/MaxTap.html ''' - return self._check_for_error(self._lib.Transformers_Get_MaxTap()) + return self._lib.Transformers_Get_MaxTap() @MaxTap.setter def MaxTap(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_MaxTap(Value)) + self._lib.Transformers_Set_MaxTap(Value) @property def MinTap(self) -> float: @@ -69,11 +69,11 @@ def MinTap(self) -> float: Original COM help: https://opendss.epri.com/MinTap.html ''' - return self._check_for_error(self._lib.Transformers_Get_MinTap()) + return self._lib.Transformers_Get_MinTap() @MinTap.setter def MinTap(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_MinTap(Value)) + self._lib.Transformers_Set_MinTap(Value) @property def NumTaps(self) -> int: @@ -82,11 +82,11 @@ def NumTaps(self) -> int: Original COM help: https://opendss.epri.com/NumTaps.html ''' - return self._check_for_error(self._lib.Transformers_Get_NumTaps()) + return self._lib.Transformers_Get_NumTaps() @NumTaps.setter def NumTaps(self, Value: int): - self._check_for_error(self._lib.Transformers_Set_NumTaps(Value)) + self._lib.Transformers_Set_NumTaps(Value) @property def NumWindings(self) -> int: @@ -95,11 +95,11 @@ def NumWindings(self) -> int: Original COM help: https://opendss.epri.com/NumWindings.html ''' - return self._check_for_error(self._lib.Transformers_Get_NumWindings()) + return self._lib.Transformers_Get_NumWindings() @NumWindings.setter def NumWindings(self, Value: int): - self._check_for_error(self._lib.Transformers_Set_NumWindings(Value)) + self._lib.Transformers_Set_NumWindings(Value) @property def R(self) -> float: @@ -108,11 +108,11 @@ def R(self) -> float: Original COM help: https://opendss.epri.com/R.html ''' - return self._check_for_error(self._lib.Transformers_Get_R()) + return self._lib.Transformers_Get_R() @R.setter def R(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_R(Value)) + self._lib.Transformers_Set_R(Value) @property def Rneut(self) -> float: @@ -121,11 +121,11 @@ def Rneut(self) -> float: Original COM help: https://opendss.epri.com/Rneut1.html ''' - return self._check_for_error(self._lib.Transformers_Get_Rneut()) + return self._lib.Transformers_Get_Rneut() @Rneut.setter def Rneut(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_Rneut(Value)) + self._lib.Transformers_Set_Rneut(Value) @property def Tap(self) -> float: @@ -134,11 +134,11 @@ def Tap(self) -> float: Original COM help: https://opendss.epri.com/Tap.html ''' - return self._check_for_error(self._lib.Transformers_Get_Tap()) + return self._lib.Transformers_Get_Tap() @Tap.setter def Tap(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_Tap(Value)) + self._lib.Transformers_Set_Tap(Value) @property def Wdg(self) -> int: @@ -147,11 +147,11 @@ def Wdg(self) -> int: Original COM help: https://opendss.epri.com/Wdg.html ''' - return self._check_for_error(self._lib.Transformers_Get_Wdg()) + return self._lib.Transformers_Get_Wdg() @Wdg.setter def Wdg(self, Value: int): - self._check_for_error(self._lib.Transformers_Set_Wdg(Value)) + self._lib.Transformers_Set_Wdg(Value) @property def XfmrCode(self) -> str: @@ -160,14 +160,11 @@ def XfmrCode(self) -> str: Original COM help: https://opendss.epri.com/XfmrCode1.html ''' - return self._get_string(self._check_for_error(self._lib.Transformers_Get_XfmrCode())) + return self._lib.Transformers_Get_XfmrCode() @XfmrCode.setter def XfmrCode(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.Transformers_Set_XfmrCode(Value)) + self._lib.Transformers_Set_XfmrCode(Value) @property def Xhl(self) -> float: @@ -176,11 +173,11 @@ def Xhl(self) -> float: Original COM help: https://opendss.epri.com/Xhl.html ''' - return self._check_for_error(self._lib.Transformers_Get_Xhl()) + return self._lib.Transformers_Get_Xhl() @Xhl.setter def Xhl(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_Xhl(Value)) + self._lib.Transformers_Set_Xhl(Value) @property def Xht(self) -> float: @@ -189,11 +186,11 @@ def Xht(self) -> float: Original COM help: https://opendss.epri.com/Xht.html ''' - return self._check_for_error(self._lib.Transformers_Get_Xht()) + return self._lib.Transformers_Get_Xht() @Xht.setter def Xht(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_Xht(Value)) + self._lib.Transformers_Set_Xht(Value) @property def Xlt(self) -> float: @@ -202,11 +199,11 @@ def Xlt(self) -> float: Original COM help: https://opendss.epri.com/Xlt.html ''' - return self._check_for_error(self._lib.Transformers_Get_Xlt()) + return self._lib.Transformers_Get_Xlt() @Xlt.setter def Xlt(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_Xlt(Value)) + self._lib.Transformers_Set_Xlt(Value) @property def Xneut(self) -> float: @@ -215,11 +212,11 @@ def Xneut(self) -> float: Original COM help: https://opendss.epri.com/Xneut1.html ''' - return self._check_for_error(self._lib.Transformers_Get_Xneut()) + return self._lib.Transformers_Get_Xneut() @Xneut.setter def Xneut(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_Xneut(Value)) + self._lib.Transformers_Set_Xneut(Value) @property def kV(self) -> float: @@ -228,11 +225,11 @@ def kV(self) -> float: Original COM help: https://opendss.epri.com/kV3.html ''' - return self._check_for_error(self._lib.Transformers_Get_kV()) + return self._lib.Transformers_Get_kV() @kV.setter def kV(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_kV(Value)) + self._lib.Transformers_Set_kV(Value) @property def kVA(self) -> float: @@ -241,11 +238,11 @@ def kVA(self) -> float: Original COM help: https://opendss.epri.com/kva1.html ''' - return self._check_for_error(self._lib.Transformers_Get_kVA()) + return self._lib.Transformers_Get_kVA() @kVA.setter def kVA(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_kVA(Value)) + self._lib.Transformers_Set_kVA(Value) kva = kVA @@ -259,8 +256,7 @@ def WdgVoltages(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/WdgVoltages.html ''' - self._check_for_error(self._lib.Transformers_Get_WdgVoltages_GR()) - return self._get_complex128_gr_array() + return self._lib.Transformers_Get_WdgVoltages_GR() @property def WdgCurrents(self) -> Float64ArrayOrComplexArray: @@ -272,8 +268,7 @@ def WdgCurrents(self) -> Float64ArrayOrComplexArray: Original COM help: https://opendss.epri.com/WdgCurrents.html ''' - self._check_for_error(self._lib.Transformers_Get_WdgCurrents_GR()) - return self._get_complex128_gr_array() + return self._lib.Transformers_Get_WdgCurrents_GR() @property def strWdgCurrents(self) -> str: @@ -283,7 +278,7 @@ def strWdgCurrents(self) -> str: **WARNING:** If the transformer has open terminal(s), results may be wrong, i.e. avoid using this in those situations. For more information, see https://github.com/dss-extensions/dss-extensions/issues/24 ''' - return self._get_string(self._check_for_error(self._lib.Transformers_Get_strWdgCurrents())) + return self._lib.Transformers_Get_strWdgCurrents() @property def CoreType(self) -> TransformerCoreType: @@ -292,11 +287,11 @@ def CoreType(self) -> TransformerCoreType: Original COM help: https://opendss.epri.com/CoreType.html ''' - return TransformerCoreType(self._check_for_error(self._lib.Transformers_Get_CoreType())) + return TransformerCoreType(self._lib.Transformers_Get_CoreType()) @CoreType.setter def CoreType(self, Value: Union[int, TransformerCoreType]): - self._check_for_error(self._lib.Transformers_Set_CoreType(Value)) + self._lib.Transformers_Set_CoreType(Value) @property def RdcOhms(self) -> float: @@ -305,11 +300,11 @@ def RdcOhms(self) -> float: Original COM help: https://opendss.epri.com/RdcOhms.html ''' - return self._check_for_error(self._lib.Transformers_Get_RdcOhms()) + return self._lib.Transformers_Get_RdcOhms() @RdcOhms.setter def RdcOhms(self, Value: float): - self._check_for_error(self._lib.Transformers_Set_RdcOhms(Value)) + self._lib.Transformers_Set_RdcOhms(Value) @property def LossesByType(self) -> Float64ArrayOrComplexArray: @@ -318,8 +313,7 @@ def LossesByType(self) -> Float64ArrayOrComplexArray: **(API Extension)** ''' - self._check_for_error(self._lib.Transformers_Get_LossesByType_GR()) - return self._get_complex128_gr_array() + return self._lib.Transformers_Get_LossesByType_GR() @property def AllLossesByType(self) -> Float64ArrayOrComplexArray: @@ -328,5 +322,4 @@ def AllLossesByType(self) -> Float64ArrayOrComplexArray: **(API Extension)** ''' - self._check_for_error(self._lib.Transformers_Get_AllLossesByType_GR()) - return self._get_complex128_gr_array() + return self._lib.Transformers_Get_AllLossesByType_GR() diff --git a/dss/IVsources.py b/dss/IVsources.py index d2e9c6a9..b7579b7b 100644 --- a/dss/IVsources.py +++ b/dss/IVsources.py @@ -24,11 +24,11 @@ def AngleDeg(self) -> float: Original COM help: https://opendss.epri.com/AngleDeg1.html ''' - return self._check_for_error(self._lib.Vsources_Get_AngleDeg()) + return self._lib.Vsources_Get_AngleDeg() @AngleDeg.setter def AngleDeg(self, Value: float): - self._check_for_error(self._lib.Vsources_Set_AngleDeg(Value)) + self._lib.Vsources_Set_AngleDeg(Value) @property def BasekV(self) -> float: @@ -37,11 +37,11 @@ def BasekV(self) -> float: Original COM help: https://opendss.epri.com/BasekV.html ''' - return self._check_for_error(self._lib.Vsources_Get_BasekV()) + return self._lib.Vsources_Get_BasekV() @BasekV.setter def BasekV(self, Value: float): - self._check_for_error(self._lib.Vsources_Set_BasekV(Value)) + self._lib.Vsources_Set_BasekV(Value) @property def Frequency(self) -> float: @@ -50,11 +50,11 @@ def Frequency(self) -> float: Original COM help: https://opendss.epri.com/Frequency2.html ''' - return self._check_for_error(self._lib.Vsources_Get_Frequency()) + return self._lib.Vsources_Get_Frequency() @Frequency.setter def Frequency(self, Value: float): - self._check_for_error(self._lib.Vsources_Set_Frequency(Value)) + self._lib.Vsources_Set_Frequency(Value) @property def Phases(self) -> int: @@ -63,11 +63,11 @@ def Phases(self) -> int: Original COM help: https://opendss.epri.com/Phases3.html ''' - return self._check_for_error(self._lib.Vsources_Get_Phases()) + return self._lib.Vsources_Get_Phases() @Phases.setter def Phases(self, Value: int): - self._check_for_error(self._lib.Vsources_Set_Phases(Value)) + self._lib.Vsources_Set_Phases(Value) @property def pu(self) -> float: @@ -76,8 +76,8 @@ def pu(self) -> float: Original COM help: https://opendss.epri.com/pu.html ''' - return self._check_for_error(self._lib.Vsources_Get_pu()) + return self._lib.Vsources_Get_pu() @pu.setter def pu(self, Value: float): - self._check_for_error(self._lib.Vsources_Set_pu(Value)) + self._lib.Vsources_Set_pu(Value) diff --git a/dss/IWindGens.py b/dss/IWindGens.py index c233d998..4942b481 100644 --- a/dss/IWindGens.py +++ b/dss/IWindGens.py @@ -55,255 +55,254 @@ def RegisterNames(self) -> List[str]: See also the enum `GeneratorRegisters`. ''' - return self._check_for_error(self._get_string_array(self._lib.WindGens_Get_RegisterNames)) + return self._lib.WindGens_Get_RegisterNames() @property def RegisterValues(self) -> Float64Array: '''Array of values in Storage registers.''' - self._check_for_error(self._lib.WindGens_Get_RegisterValues_GR()) - return self._get_float64_gr_array() + return self._lib.WindGens_Get_RegisterValues_GR() @property def kV(self) -> float: ''' Nominal rated (1.0 per unit) voltage for the active WindGen, in kV. ''' - return self._check_for_error(self._lib.WindGens_Get_kV()) + return self._lib.WindGens_Get_kV() @kV.setter def kV(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_kV(Value)) + self._lib.WindGens_Set_kV(Value) @property def kvar(self) -> float: ''' Base kvar for the active WindGen. ''' - return self._check_for_error(self._lib.WindGens_Get_kvar()) + return self._lib.WindGens_Get_kvar() @kvar.setter def kvar(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_kvar(Value)) + self._lib.WindGens_Set_kvar(Value) @property def kW(self) -> float: ''' Total base kW for the active WindGen. ''' - return self._check_for_error(self._lib.WindGens_Get_kW()) + return self._lib.WindGens_Get_kW() @kW.setter def kW(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_kW(Value)) + self._lib.WindGens_Set_kW(Value) @property def PF(self) -> float: ''' WindGen power factor. Power factor (pos. = producing vars). ''' - return self._check_for_error(self._lib.WindGens_Get_PF()) + return self._lib.WindGens_Get_PF() @PF.setter def PF(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_PF(Value)) + self._lib.WindGens_Set_PF(Value) @property def kVA(self) -> float: ''' KVA rating of the electrical machine in the WindGen. ''' - return self._check_for_error(self._lib.WindGens_Get_kVA()) + return self._lib.WindGens_Get_kVA() @kVA.setter def kVA(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_kVA(Value)) + self._lib.WindGens_Set_kVA(Value) @property def Ag(self) -> float: ''' Gearbox ratio ''' - return self._check_for_error(self._lib.WindGens_Get_Ag()) + return self._lib.WindGens_Get_Ag() @Ag.setter def Ag(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_Ag(Value)) + self._lib.WindGens_Set_Ag(Value) @property def Cp(self) -> float: ''' Turbine performance coefficient. ''' - return self._check_for_error(self._lib.WindGens_Get_Cp()) + return self._lib.WindGens_Get_Cp() @Cp.setter def Cp(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_Cp(Value)) + self._lib.WindGens_Set_Cp(Value) @property def Lamda(self) -> float: ''' Tip speed ratio ''' - return self._check_for_error(self._lib.WindGens_Get_Lamda()) + return self._lib.WindGens_Get_Lamda() @Lamda.setter def Lamda(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_Lamda(Value)) + self._lib.WindGens_Set_Lamda(Value) @property def N_WTG(self) -> int: ''' Number of WTG in aggregation ''' - return self._check_for_error(self._lib.WindGens_Get_N_WTG()) + return self._lib.WindGens_Get_N_WTG() @N_WTG.setter def N_WTG(self, Value: int) -> None: - self._check_for_error(self._lib.WindGens_Set_N_WTG(Value)) + self._lib.WindGens_Set_N_WTG(Value) @property def NPoles(self) -> int: ''' Number of pole pairs of the induction generator ''' - return self._check_for_error(self._lib.WindGens_Get_NPoles()) + return self._lib.WindGens_Get_NPoles() @NPoles.setter def NPoles(self, Value: int) -> None: - self._check_for_error(self._lib.WindGens_Set_NPoles(Value)) + self._lib.WindGens_Set_NPoles(Value) @property def pd(self) -> float: ''' Air density in kg/m3 ''' - return self._check_for_error(self._lib.WindGens_Get_pd()) + return self._lib.WindGens_Get_pd() @pd.setter def pd(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_pd(Value)) + self._lib.WindGens_Set_pd(Value) @property def PSS(self) -> float: ''' Steady state output real power. ''' - return self._check_for_error(self._lib.WindGens_Get_PSS()) + return self._lib.WindGens_Get_PSS() @PSS.setter def PSS(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_PSS(Value)) + self._lib.WindGens_Set_PSS(Value) @property def QFlag(self) -> int: ''' Non-zero values enable reactive power and voltage control in the dynamic model. ''' - return self._check_for_error(self._lib.WindGens_Get_QFlag()) + return self._lib.WindGens_Get_QFlag() @QFlag.setter def QFlag(self, Value: int) -> None: - self._check_for_error(self._lib.WindGens_Set_QFlag(Value)) + self._lib.WindGens_Set_QFlag(Value) @property def QMode(self) -> int: ''' Q control mode (0:Q, 1:PF, 2:VV). ''' - return self._check_for_error(self._lib.WindGens_Get_QMode()) + return self._lib.WindGens_Get_QMode() @QMode.setter def QMode(self, Value: int) -> None: - self._check_for_error(self._lib.WindGens_Set_QMode(Value)) + self._lib.WindGens_Set_QMode(Value) @property def QSS(self) -> float: ''' Steady state output reactive power. ''' - return self._check_for_error(self._lib.WindGens_Get_QSS()) + return self._lib.WindGens_Get_QSS() @QSS.setter def QSS(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_QSS(Value)) + self._lib.WindGens_Set_QSS(Value) @property def Rad(self) -> float: ''' Rotor radius in meters ''' - return self._check_for_error(self._lib.WindGens_Get_Rad()) + return self._lib.WindGens_Get_Rad() @Rad.setter def Rad(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_Rad(Value)) + self._lib.WindGens_Set_Rad(Value) @property def RThev(self) -> float: ''' Per unit Thevenin equivalent resistance (R). ''' - return self._check_for_error(self._lib.WindGens_Get_RThev()) + return self._lib.WindGens_Get_RThev() @RThev.setter def RThev(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_RThev(Value)) + self._lib.WindGens_Set_RThev(Value) @property def VCutIn(self) -> float: ''' Cut-in speed for the wind generator ''' - return self._check_for_error(self._lib.WindGens_Get_VCutIn()) + return self._lib.WindGens_Get_VCutIn() @VCutIn.setter def VCutIn(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_VCutIn(Value)) + self._lib.WindGens_Set_VCutIn(Value) @property def VCutOut(self) -> float: ''' Cut-out speed for the wind generator ''' - return self._check_for_error(self._lib.WindGens_Get_VCutOut()) + return self._lib.WindGens_Get_VCutOut() @VCutOut.setter def VCutOut(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_VCutOut(Value)) + self._lib.WindGens_Set_VCutOut(Value) @property def Vss(self) -> float: ''' Steady state voltage magnitude. ''' - return self._check_for_error(self._lib.WindGens_Get_Vss()) + return self._lib.WindGens_Get_Vss() @Vss.setter def Vss(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_Vss(Value)) + self._lib.WindGens_Set_Vss(Value) @property def WindSpeed(self) -> float: ''' Wind speed in m/s ''' - return self._check_for_error(self._lib.WindGens_Get_WindSpeed()) + return self._lib.WindGens_Get_WindSpeed() @WindSpeed.setter def WindSpeed(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_WindSpeed(Value)) + self._lib.WindGens_Set_WindSpeed(Value) @property def XThev(self) -> float: ''' Per unit Thevenin equivalent reactance (X). ''' - return self._check_for_error(self._lib.WindGens_Get_XThev()) + return self._lib.WindGens_Get_XThev() @XThev.setter def XThev(self, Value: float) -> None: - self._check_for_error(self._lib.WindGens_Set_XThev(Value)) + self._lib.WindGens_Set_XThev(Value) @property def Phases(self) -> int: @@ -312,7 +311,7 @@ def Phases(self) -> int: (API Extension) ''' - return self._check_for_error(self._lib.WindGens_Get_Phases()) + return self._lib.WindGens_Get_Phases() @Phases.setter def Phases(self, Value: int) -> None: @@ -321,7 +320,7 @@ def Phases(self, Value: int) -> None: (API Extension) ''' - self._check_for_error(self._lib.WindGens_Set_Phases(Value)) + self._lib.WindGens_Set_Phases(Value) @property def daily(self) -> str: @@ -330,14 +329,11 @@ def daily(self) -> str: (API Extension) ''' - return self._get_string(self._check_for_error(self._lib.WindGens_Get_daily())) + return self._lib.WindGens_Get_daily() @daily.setter def daily(self, Value: AnyStr) -> None: - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.WindGens_Set_daily(Value)) + self._lib.WindGens_Set_daily(Value) @property def duty(self) -> str: @@ -346,14 +342,11 @@ def duty(self) -> str: (API Extension) ''' - return self._get_string(self._check_for_error(self._lib.WindGens_Get_duty())) + return self._lib.WindGens_Get_duty() @duty.setter def duty(self, Value: AnyStr) -> None: - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.WindGens_Set_duty(Value)) + self._lib.WindGens_Set_duty(Value) @property def Yearly(self) -> str: @@ -362,14 +355,11 @@ def Yearly(self) -> str: (API Extension) ''' - return self._get_string(self._check_for_error(self._lib.WindGens_Get_Yearly())) + return self._lib.WindGens_Get_Yearly() @Yearly.setter def Yearly(self, Value: AnyStr) -> None: - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.WindGens_Set_Yearly(Value)) + self._lib.WindGens_Set_Yearly(Value) @property def IsDelta(self) -> bool: @@ -378,11 +368,11 @@ def IsDelta(self) -> bool: (API Extension) ''' - return self._check_for_error(self._lib.WindGens_Get_IsDelta()) != 0 + return self._lib.WindGens_Get_IsDelta() @IsDelta.setter def IsDelta(self, Value: bool) -> None: - self._check_for_error(self._lib.WindGens_Set_IsDelta(Value)) + self._lib.WindGens_Set_IsDelta(Value) @property def Class(self) -> int: @@ -391,11 +381,11 @@ def Class(self) -> int: (API Extension) ''' - return self._check_for_error(self._lib.WindGens_Get_Class_()) + return self._lib.WindGens_Get_Class_() @Class.setter def Class(self, Value: int) -> None: - self._check_for_error(self._lib.WindGens_Set_Class_(Value)) + self._lib.WindGens_Set_Class_(Value) @property def Bus1(self) -> str: @@ -404,11 +394,8 @@ def Bus1(self) -> str: (API Extension) ''' - return self._get_string(self._check_for_error(self._lib.WindGens_Get_Bus1())) + return self._lib.WindGens_Get_Bus1() @Bus1.setter def Bus1(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._lib.WindGens_Set_Bus1(Value)) + self._lib.WindGens_Set_Bus1(Value) diff --git a/dss/IWireData.py b/dss/IWireData.py index 36e3e5e7..084e0443 100644 --- a/dss/IWireData.py +++ b/dss/IWireData.py @@ -33,90 +33,90 @@ class IWireData(Iterable): @property def EmergAmps(self) -> float: '''Emergency ampere rating''' - return self._check_for_error(self._lib.WireData_Get_EmergAmps()) + return self._lib.WireData_Get_EmergAmps() @EmergAmps.setter def EmergAmps(self, Value: float): - self._check_for_error(self._lib.WireData_Set_EmergAmps(Value)) + self._lib.WireData_Set_EmergAmps(Value) @property def NormAmps(self) -> float: '''Normal Ampere rating''' - return self._check_for_error(self._lib.WireData_Get_NormAmps()) + return self._lib.WireData_Get_NormAmps() @NormAmps.setter def NormAmps(self, Value: float): - self._check_for_error(self._lib.WireData_Set_NormAmps(Value)) + self._lib.WireData_Set_NormAmps(Value) @property def Rdc(self) -> float: - return self._check_for_error(self._lib.WireData_Get_Rdc()) + return self._lib.WireData_Get_Rdc() @Rdc.setter def Rdc(self, Value: float): - self._check_for_error(self._lib.WireData_Set_Rdc(Value)) + self._lib.WireData_Set_Rdc(Value) @property def Rac(self) -> float: - return self._check_for_error(self._lib.WireData_Get_Rac()) + return self._lib.WireData_Get_Rac() @Rac.setter def Rac(self, Value: float): - self._check_for_error(self._lib.WireData_Set_Rac(Value)) + self._lib.WireData_Set_Rac(Value) @property def GMRac(self) -> float: - return self._check_for_error(self._lib.WireData_Get_GMRac()) + return self._lib.WireData_Get_GMRac() @GMRac.setter def GMRac(self, Value: float): - self._check_for_error(self._lib.WireData_Set_GMRac(Value)) + self._lib.WireData_Set_GMRac(Value) @property def GMRUnits(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.WireData_Get_GMRUnits())) + return LineUnits(self._lib.WireData_Get_GMRUnits()) @GMRUnits.setter def GMRUnits(self, Value: Union[int, LineUnits]): - self._check_for_error(self._lib.WireData_Set_GMRUnits(Value)) + self._lib.WireData_Set_GMRUnits(Value) @property def Radius(self) -> float: - return self._check_for_error(self._lib.WireData_Get_Radius()) + return self._lib.WireData_Get_Radius() @Radius.setter def Radius(self, Value: float): - self._check_for_error(self._lib.WireData_Set_Radius(Value)) + self._lib.WireData_Set_Radius(Value) @property def RadiusUnits(self) -> int: - return self._check_for_error(self._lib.WireData_Get_RadiusUnits()) + return self._lib.WireData_Get_RadiusUnits() @RadiusUnits.setter def RadiusUnits(self, Value: int): - self._check_for_error(self._lib.WireData_Set_RadiusUnits(Value)) + self._lib.WireData_Set_RadiusUnits(Value) @property def ResistanceUnits(self) -> LineUnits: - return LineUnits(self._check_for_error(self._lib.WireData_Get_ResistanceUnits())) + return LineUnits(self._lib.WireData_Get_ResistanceUnits()) @ResistanceUnits.setter def ResistanceUnits(self, Value: Union[int, LineUnits]): - self._check_for_error(self._lib.WireData_Set_ResistanceUnits(Value)) + self._lib.WireData_Set_ResistanceUnits(Value) @property def Diameter(self) -> float: - return self._check_for_error(self._lib.WireData_Get_Diameter()) + return self._lib.WireData_Get_Diameter() @Diameter.setter def Diameter(self, Value: float): - self._check_for_error(self._lib.WireData_Set_Diameter(Value)) + self._lib.WireData_Set_Diameter(Value) @property def CapRadius(self) -> float: '''Equivalent conductor radius for capacitance calcs. Specify this for bundled conductors. Defaults to same value as radius.''' - return self._check_for_error(self._lib.WireData_Get_CapRadius()) + return self._lib.WireData_Get_CapRadius() @CapRadius.setter def CapRadius(self, Value: float): - self._check_for_error(self._lib.WireData_Set_CapRadius(Value)) + self._lib.WireData_Set_CapRadius(Value) diff --git a/dss/IXYCurves.py b/dss/IXYCurves.py index 6b350461..cd981a8b 100644 --- a/dss/IXYCurves.py +++ b/dss/IXYCurves.py @@ -28,11 +28,11 @@ def Npts(self) -> int: Original COM help: https://opendss.epri.com/Npts1.html ''' - return self._check_for_error(self._lib.XYCurves_Get_Npts()) + return self._lib.XYCurves_Get_Npts() @Npts.setter def Npts(self, Value: int): - self._check_for_error(self._lib.XYCurves_Set_Npts(Value)) + self._lib.XYCurves_Set_Npts(Value) @property def Xarray(self) -> Float64Array: @@ -41,13 +41,12 @@ def Xarray(self) -> Float64Array: Original COM help: https://opendss.epri.com/Xarray.html ''' - self._check_for_error(self._lib.XYCurves_Get_Xarray_GR()) - return self._get_float64_gr_array() + return self._lib.XYCurves_Get_Xarray_GR() @Xarray.setter def Xarray(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.XYCurves_Set_Xarray(ValuePtr, ValueCount)) + self._lib.XYCurves_Set_Xarray(ValuePtr, ValueCount) @property def Xscale(self) -> float: @@ -56,11 +55,11 @@ def Xscale(self) -> float: Original COM help: https://opendss.epri.com/Xscale.html ''' - return self._check_for_error(self._lib.XYCurves_Get_Xscale()) + return self._lib.XYCurves_Get_Xscale() @Xscale.setter def Xscale(self, Value: float): - self._check_for_error(self._lib.XYCurves_Set_Xscale(Value)) + self._lib.XYCurves_Set_Xscale(Value) @property def Xshift(self) -> float: @@ -69,11 +68,11 @@ def Xshift(self) -> float: Original COM help: https://opendss.epri.com/Xshift.html ''' - return self._check_for_error(self._lib.XYCurves_Get_Xshift()) + return self._lib.XYCurves_Get_Xshift() @Xshift.setter def Xshift(self, Value: float): - self._check_for_error(self._lib.XYCurves_Set_Xshift(Value)) + self._lib.XYCurves_Set_Xshift(Value) @property def Yarray(self) -> Float64Array: @@ -82,13 +81,12 @@ def Yarray(self) -> Float64Array: Original COM help: https://opendss.epri.com/Yarray.html ''' - self._check_for_error(self._lib.XYCurves_Get_Yarray_GR()) - return self._get_float64_gr_array() + return self._lib.XYCurves_Get_Yarray_GR() @Yarray.setter def Yarray(self, Value: Float64Array): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) - self._check_for_error(self._lib.XYCurves_Set_Yarray(ValuePtr, ValueCount)) + self._lib.XYCurves_Set_Yarray(ValuePtr, ValueCount) @property def Yscale(self) -> float: @@ -97,11 +95,11 @@ def Yscale(self) -> float: Original COM help: https://opendss.epri.com/Yscale.html ''' - return self._check_for_error(self._lib.XYCurves_Get_Yscale()) + return self._lib.XYCurves_Get_Yscale() @Yscale.setter def Yscale(self, Value: float): - self._check_for_error(self._lib.XYCurves_Set_Yscale(Value)) + self._lib.XYCurves_Set_Yscale(Value) @property def Yshift(self) -> float: @@ -110,11 +108,11 @@ def Yshift(self) -> float: Original COM help: https://opendss.epri.com/Yshift.html ''' - return self._check_for_error(self._lib.XYCurves_Get_Yshift()) + return self._lib.XYCurves_Get_Yshift() @Yshift.setter def Yshift(self, Value: float): - self._check_for_error(self._lib.XYCurves_Set_Yshift(Value)) + self._lib.XYCurves_Set_Yshift(Value) @property def x(self) -> float: @@ -123,11 +121,11 @@ def x(self) -> float: Original COM help: https://opendss.epri.com/x4.html ''' - return self._check_for_error(self._lib.XYCurves_Get_x()) + return self._lib.XYCurves_Get_x() @x.setter def x(self, Value: float): - self._check_for_error(self._lib.XYCurves_Set_x(Value)) + self._lib.XYCurves_Set_x(Value) @property def y(self) -> float: @@ -136,8 +134,8 @@ def y(self) -> float: Original COM help: https://opendss.epri.com/y1.html ''' - return self._check_for_error(self._lib.XYCurves_Get_y()) + return self._lib.XYCurves_Get_y() @y.setter def y(self, Value: float): - self._check_for_error(self._lib.XYCurves_Set_y(Value)) + self._lib.XYCurves_Set_y(Value) diff --git a/dss/IYMatrix.py b/dss/IYMatrix.py index ca0fd23c..8913a3f8 100644 --- a/dss/IYMatrix.py +++ b/dss/IYMatrix.py @@ -32,7 +32,8 @@ def GetCompressedYMatrix(self, factor: bool = True) -> Tuple[ComplexArray, Int32 RowIdxPtr = ffi.new('int32_t**') cValsPtr = ffi.new('double**') - self._lib.YMatrix_GetCompressedYMatrix(factor, nBus, nNz, ColPtr, RowIdxPtr, cValsPtr) + lib = self._api_util.lib_unpatched # use the raw CFFI version + lib.YMatrix_GetCompressedYMatrix(factor, nBus, nNz, ColPtr, RowIdxPtr, cValsPtr) if not nBus[0] or not nNz[0]: res = None @@ -44,39 +45,39 @@ def GetCompressedYMatrix(self, factor: bool = True) -> Tuple[ComplexArray, Int32 np.frombuffer(ffi.buffer(ColPtr[0], (nBus[0] + 1) * 4), dtype=np.int32).copy() ) - self._lib.DSS_Dispose_PInteger(ColPtr) - self._lib.DSS_Dispose_PInteger(RowIdxPtr) - self._lib.DSS_Dispose_PDouble(cValsPtr) + lib.DSS_Dispose_PInteger(ColPtr) + lib.DSS_Dispose_PInteger(RowIdxPtr) + lib.DSS_Dispose_PDouble(cValsPtr) - self._check_for_error() + self._api_util._check_for_error() return res def ZeroInjCurr(self): - self._check_for_error(self._lib.YMatrix_ZeroInjCurr()) + self._lib.YMatrix_ZeroInjCurr() def GetSourceInjCurrents(self): - self._check_for_error(self._lib.YMatrix_GetSourceInjCurrents()) + self._lib.YMatrix_GetSourceInjCurrents() def GetPCInjCurr(self): - self._check_for_error(self._lib.YMatrix_GetPCInjCurr()) + self._lib.YMatrix_GetPCInjCurr() def BuildYMatrixD(self, BuildOps: int, AllocateVI: bool): - self._check_for_error(self._lib.YMatrix_BuildYMatrixD(BuildOps, AllocateVI)) + self._lib.YMatrix_BuildYMatrixD(BuildOps, AllocateVI) def AddInAuxCurrents(self, SType): - self._check_for_error(self._lib.YMatrix_AddInAuxCurrents(SType)) + self._lib.YMatrix_AddInAuxCurrents(SType) def GetIPointer(self): '''Get access to the internal Current pointer''' IvectorPtr = self._api_util.ffi.new('double**') - self._check_for_error(self._lib.YMatrix_getIpointer(IvectorPtr)) + self._lib.YMatrix_getIpointer(IvectorPtr) return IvectorPtr[0] def GetVPointer(self): '''Get access to the internal Voltage pointer''' VvectorPtr = self._api_util.ffi.new('double**') - self._check_for_error(self._lib.YMatrix_getVpointer(VvectorPtr)) + self._lib.YMatrix_getVpointer(VvectorPtr) return VvectorPtr[0] def SolveSystem(self, NodeV=None) -> int: @@ -88,24 +89,23 @@ def SolveSystem(self, NodeV=None) -> int: else: NodeVPtr = self._api_util.ffi.cast("double *", NodeV.ctypes.data) - result = self._check_for_error(self._lib.YMatrix_SolveSystem(NodeVPtr)) - return result + return self._lib.YMatrix_SolveSystem(NodeVPtr) @property def SystemYChanged(self) -> bool: - return self._check_for_error(self._lib.YMatrix_Get_SystemYChanged() != 0) + return self._lib.YMatrix_Get_SystemYChanged() @SystemYChanged.setter def SystemYChanged(self, value: bool): - self._check_for_error(self._lib.YMatrix_Set_SystemYChanged(value)) + self._lib.YMatrix_Set_SystemYChanged(value) @property def UseAuxCurrents(self) -> bool: - return self._check_for_error(self._lib.YMatrix_Get_UseAuxCurrents() != 0) + return self._lib.YMatrix_Get_UseAuxCurrents() @UseAuxCurrents.setter def UseAuxCurrents(self, value: bool): - self._check_for_error(self._lib.YMatrix_Set_UseAuxCurrents(value)) + self._lib.YMatrix_Set_UseAuxCurrents(value) # for better compatibility with OpenDSSDirect.py getYSparse = GetCompressedYMatrix @@ -122,39 +122,39 @@ def SolverOptions(self, Value: int): def getI(self) -> List[float]: '''Get the data from the internal Current pointer''' IvectorPtr = self.GetIPointer() - return self._api_util.ffi.unpack(IvectorPtr, 2 * (self._check_for_error(self._lib.Circuit_Get_NumNodes() + 1))) + return self._api_util.ffi.unpack(IvectorPtr, 2 * (self._lib.Circuit_Get_NumNodes() + 1)) def getV(self) -> List[float]: '''Get the data from the internal Voltage pointer''' VvectorPtr = self.GetVPointer() - return self._api_util.ffi.unpack(VvectorPtr, 2 * (self._check_for_error(self._lib.Circuit_Get_NumNodes() + 1))) + return self._api_util.ffi.unpack(VvectorPtr, 2 * (self._lib.Circuit_Get_NumNodes() + 1)) def CheckConvergence(self) -> bool: - return self._check_for_error(self._lib.YMatrix_CheckConvergence() != 0) + return self._lib.YMatrix_CheckConvergence() def SetGeneratordQdV(self): - self._check_for_error(self._lib.YMatrix_SetGeneratordQdV()) + self._lib.YMatrix_SetGeneratordQdV() @property def LoadsNeedUpdating(self) -> bool: - return self._check_for_error(self._lib.YMatrix_Get_LoadsNeedUpdating() != 0) + return self._lib.YMatrix_Get_LoadsNeedUpdating() @LoadsNeedUpdating.setter def LoadsNeedUpdating(self, value: bool): - self._check_for_error(self._lib.YMatrix_Set_LoadsNeedUpdating(value)) + self._lib.YMatrix_Set_LoadsNeedUpdating(value) @property def SolutionInitialized(self) -> bool: - return self._check_for_error(self._lib.YMatrix_Get_SolutionInitialized() != 0) + return self._lib.YMatrix_Get_SolutionInitialized() @SolutionInitialized.setter def SolutionInitialized(self, value: bool): - self._check_for_error(self._lib.YMatrix_Set_SolutionInitialized(value)) + self._lib.YMatrix_Set_SolutionInitialized(value) @property def Iteration(self) -> int: - return self._check_for_error(self._lib.YMatrix_Get_Iteration()) + return self._lib.YMatrix_Get_Iteration() @Iteration.setter def Iteration(self, value: int): - self._check_for_error(self._lib.YMatrix_Set_Iteration(value)) + self._lib.YMatrix_Set_Iteration(value) diff --git a/dss/IZIP.py b/dss/IZIP.py index e61f0389..1b342d1d 100644 --- a/dss/IZIP.py +++ b/dss/IZIP.py @@ -28,10 +28,7 @@ def Open(self, FileName: AnyStr): **(API Extension)** ''' - if not isinstance(FileName, bytes): - FileName = FileName.encode(self._api_util.codec) - - self._check_for_error(self._lib.ZIP_Open(FileName)) + self._lib.ZIP_Open(FileName) def Close(self): ''' @@ -39,7 +36,7 @@ def Close(self): **(API Extension)** ''' - self._check_for_error(self._lib.ZIP_Close()) + self._lib.ZIP_Close() def Redirect(self, FileInZip: AnyStr): ''' @@ -50,10 +47,7 @@ def Redirect(self, FileInZip: AnyStr): **(API Extension)** ''' - if not isinstance(FileInZip, bytes): - FileInZip = FileInZip.encode(self._api_util.codec) - - self._check_for_error(self._lib.ZIP_Redirect(FileInZip)) + self._lib.ZIP_Redirect(FileInZip) def Extract(self, FileName: AnyStr) -> bytes: ''' @@ -66,11 +60,13 @@ def Extract(self, FileName: AnyStr) -> bytes: if not isinstance(FileName, bytes): FileName = FileName.encode(api_util.codec) - self._check_for_error(self._lib.ZIP_Extract_GR(FileName)) + api_util.lib_unpatched.ZIP_Extract_GR(FileName) + api_util._check_for_error() ptr, cnt = api_util.gr_int8_pointers return bytes(api_util.ffi.buffer(ptr[0], cnt[0])) - def List(self, regexp: Optional[AnyStr]=None) -> List[str]: + + def List(self, regexp: AnyStr='') -> List[str]: ''' List of strings consisting of all names match the regular expression provided in regexp. If no expression is provided, all names in the current open ZIP are returned. @@ -81,12 +77,9 @@ def List(self, regexp: Optional[AnyStr]=None) -> List[str]: **(API Extension)** ''' if regexp is None or not regexp: - regexp = self._api_util.ffi.NULL - else: - if not isinstance(regexp, bytes): - regexp = regexp.encode(self._api_util.codec) + regexp = b'' - return self._check_for_error(self._get_string_array(self._lib.ZIP_List, regexp)) + return self._lib.ZIP_List(regexp) def Contains(self, Name: AnyStr) -> bool: ''' @@ -94,10 +87,7 @@ def Contains(self, Name: AnyStr) -> bool: **(API Extension)** ''' - if not isinstance(Name, bytes): - Name = Name.encode(self._api_util.codec) - - return self._check_for_error(self._lib.ZIP_Contains(Name)) != 0 + return self._lib.ZIP_Contains(Name) def __getitem__(self, FileName) -> bytes: return self.Extract(FileName) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 2e906aa1..9e4def99 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -1,14 +1,29 @@ from __future__ import annotations import warnings -from functools import partial +from functools import partial, wraps from weakref import ref, WeakKeyDictionary import numpy as np from ._types import Float64Array, Int32Array, Int8Array, ComplexArray, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex -from typing import Any, AnyStr, Callable, List, Union, Iterator +from typing import Any, AnyStr, Callable, List, Union, Iterator, Optional, TYPE_CHECKING from .enums import AltDSSEvent from dss_python_backend.events import get_manager_for_ctx -# UTF8 under testing +if TYPE_CHECKING: + try: + from altdss import DSSObject, Bus as AltBus, AltDSS + except: + pass + +try: + # Try to import the fast backend + from dss_python_backend._fastdss import AltDSS_PyContext +except: + import dss_python_backend._func_info as _func_info + AltDSS_PyContext = None + + +# Assumed UTF8; unless the fast C extension (dss_python_backend._fast_strs) is not +# used, this now has no effect but left to avoid breaking it for downstream users. codec = 'UTF8' interface_classes = set() @@ -36,8 +51,8 @@ def set_case_insensitive_attributes(use: bool = True, warn: bool = False): - AltDSS-Python (`altdss` package): done to allow users to employ the case-insensitive mechanism to address DSS properties in Python code. - Since there is a small performance overhead, users are recommended to use this - mechanism as a transition before adjusting the code. + Since there is a small performance overhead, users are recommended to enable this + mechanism during a transition period, before adjusting the code. ''' if use: global warn_wrong_case @@ -67,14 +82,142 @@ def __str__(self): DssException = DSSException use_com_compat = set_case_insensitive_attributes + class CtxLib: ''' Exposes a CFFI Lib object pre-binding the DSSContext (`ctx`) object to the `ctx_*` functions. ''' + def _get_strs_ctx(self, errorPtr, ctx, func: Callable, *args: Any) -> List[str]: + ffi = self._ffi + codec = self._api_util.codec + ptr = ffi.new('char***') + cnt = ffi.new('int32_t[4]') + func(ctx, ptr, cnt, *args) + if errorPtr[0] and Base._use_exceptions: + error_num = errorPtr[0] + errorPtr[0] = 0 + self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) + raise DSSException(error_num, self.Error_Get_Description()) + + if not cnt[0]: + res = [] + else: + actual_ptr = ptr[0] + if actual_ptr == ffi.NULL: + res = [] + else: + str_ptrs = ffi.unpack(actual_ptr, cnt[0]) + res = [(ffi.string(str_ptr).decode(codec) if (str_ptr) else '') for str_ptr in str_ptrs] + + self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) + return res + + + def _get_str_ctx(self, errorPtr, ctx, func: Callable, *args): + codec = self._api_util.codec + ffi = self._ffi + result = func(ctx, *args) + if errorPtr[0] and Base._use_exceptions: + error_num = errorPtr[0] + errorPtr[0] = 0 + raise DSSException(error_num, self.Error_Get_Description()) + + if result: + return ffi.string(result).decode(codec) + + return '' + + + def _str_arg_wrapper(self, f: Callable) -> Callable: + @wraps(f) + def f_wrapper(s, *args): + if not isinstance(s, bytes): + s = s.encode(self._api_util.codec) + + return f(s, *args) + + return f_wrapper + + + def _prepare_api_functions_slow(self, done): + ''' + Wrap the C functions with a Python-level function to converter + strings and lists of strings from C. + (slow in CPython) + ''' + ctx = self._ctx + lib = self._lib + errorPtr = self._errorPtr + t = _func_info.t + api_util = self._api_util + + wrappers = { + t.fastdss_types_str: ('', self._get_str_ctx,), + t.fastdss_types_strs: ('', self._get_strs_ctx,), + t.fastdss_types_gr_f64s: ('_GR', self._error_checked_ctx_gr, api_util.get_float64_gr_array), + t.fastdss_types_gr_i32s: ('_GR', self._error_checked_ctx_gr, api_util.get_int32_gr_array), + t.fastdss_types_gr_i8s: ('_GR', self._error_checked_ctx_gr, api_util.get_int8_gr_array), + t.fastdss_types_gr_z128: ('_GR', self._error_checked_ctx_gr, api_util.get_complex128_gr_simple), + t.fastdss_types_gr_z128s: ('_GR', self._error_checked_ctx_gr, api_util.get_complex128_gr_array), + } + + arg_no_wrapper = lambda f: f + default_wrapper = ('', self._error_checked_ctx, ) + + for res_type, arg_type, ctx_names in _func_info.funcs: + arg_wrapper = arg_no_wrapper + if arg_type == t.fastdss_types_str: + arg_wrapper = self._str_arg_wrapper + + suffix, wrapper, *wrapper_args = wrappers.get(res_type, default_wrapper) + for ctx_name in ctx_names: + if ctx_name in done: + continue + + name = ctx_name[4:] + if name in done: + continue + + name += suffix + if name in done: + continue + + ctx_name += suffix + + func = getattr(lib, ctx_name) + + prepared_func = arg_wrapper(partial(wrapper, errorPtr, ctx, func, *wrapper_args)) + setattr(self, name, prepared_func) + + done.update(vars(self).keys()) + + + def _prepare_api_functions(self, done): + if AltDSS_PyContext is None: + self._prepare_api_functions_slow(done) + return + + ctx = self._ctx + ffi = self._ffi + ctx_int = int(ffi.cast('uintptr_t', ctx)) + self._settings_ptr = self._api_util.settings_ptr + settings_ptr_int = int(ffi.cast('uintptr_t', self._settings_ptr)) + + if not self._api_util._is_odd: + self._fast = AltDSS_PyContext(ctx_int, settings_ptr_int, DSSException, done, self) + else: + try: + from dss_python_backend._fastdss_oddie import AltDSS_PyContext as AltDSS_PyContext_Oddie + except: + AltDSS_PyContext_Oddie = None + + self._fast = AltDSS_PyContext_Oddie(ctx_int, settings_ptr_int, DSSException, done, self) + + def _get_string(self, b) -> str: - if b != self._ffi.NULL: + if b: return self._ffi.string(b).decode() return '' @@ -87,34 +230,72 @@ def _error_checked(self, _errorPtr, f, *args): return result - def __init__(self, ctx, ffi, lib): - self._ctx = ctx - self._ffi = ffi + def _error_checked_ctx(self, _errorPtr, ctx, f, *args): + result = f(ctx, *args) + if _errorPtr[0] and Base._use_exceptions: + error_num = _errorPtr[0] + _errorPtr[0] = 0 + raise DSSException(error_num, self._get_string(self.Error_Get_Description())) + + return result + + def _error_checked_ctx_gr(self, _errorPtr, ctx, f, _res_func, *args): + f(ctx, *args) + if _errorPtr[0] and Base._use_exceptions: + error_num = _errorPtr[0] + _errorPtr[0] = 0 + raise DSSException(error_num, self._get_string(self.Error_Get_Description())) + + return _res_func() + + def __init__(self, api_util): + self._api_util = api_util # this is not ready, don't use it yet + lib = self._lib =api_util.lib_unpatched + ctx = self._ctx = api_util.ctx + ffi = self._ffi = api_util.ffi + self._errorPtr = _errorPtr = lib.ctx_Error_Get_NumberPtr(ctx) + #TODO: test if a pointer is better than keeping this + self._prepared_funcs = [] - done = set() + # Wrap most of the API to provide simpler Python access + done = set(('ctx_Error_Get_Description', 'ctx_Error_Get_Number')) + self._prepare_api_functions(done) + self.Error_Get_Description = lambda: lib.ctx_Error_Get_Description(ctx) + + skip_funcs = {'ctx_New', 'ctx_Dispose', 'ctx_Get_Prime', 'ctx_Set_Prime', 'ctx_Error_Set_Description', 'ctx_Error_Get_NumberPtr', 'ctx_ZIP_Extract_GR'} # First, process all `ctx_*`` functions for name, value in vars(lib).items(): is_ctx = name.startswith('ctx_') - if not is_ctx and not name.startswith(('Batch_Create', 'Batch_Filter', )): + if (not is_ctx and not name.startswith(('Batch_Create', 'Batch_Filter', ))) or (name in done): continue # Keep the basic management functions alone - if name in {'ctx_New', 'ctx_Dispose', 'ctx_Get_Prime', 'ctx_Set_Prime', 'ctx_Error_Set_Description'}: - if name == 'ctx_Error_Set_Description': + if name in skip_funcs: + if name.startswith('ctx_DSSEvents_') or name == 'ctx_Error_Set_Description': name = name[4:] setattr(self, name, partial(value, ctx)) else: setattr(self, name, value) - elif is_ctx: - name = name[4:] - setattr(self, name, partial(value, ctx)) - # setattr(self, name, partial(self._error_checked, _errorPtr, partial(value, ctx))) - else: - setattr(self, name, partial(value, ctx)) - # setattr(self, name, partial(self._error_checked, _errorPtr, partial(value, ctx))) + done.add(name) + continue + + if is_ctx: + name = name[4:] + if name in done: + continue + + if name.endswith('_GR'): + # A few GR functions that don't have dedicated low-level mapping + wrapper_func, res_func = self._error_checked_ctx_gr, api_util.get_float64_gr_array + setattr(self, name, partial(wrapper_func, _errorPtr, ctx, value, res_func)) + done.add(name) + continue + + # General functions and array setters are only error checked, no special handling yet + setattr(self, name, partial(self._error_checked, _errorPtr, partial(value, ctx))) done.add(name) # Then the new Alt_* family @@ -218,7 +399,7 @@ def __init__(self, api_util, prefer_lists=False): self._prepare_float64_array = api_util.prepare_float64_array self._prepare_int32_array = api_util.prepare_int32_array self._prepare_string_array = api_util.prepare_string_array - self._errorPtr = self._lib.Error_Get_NumberPtr() + self._errorPtr = self._api_util._errorPtr cls = type(self) @@ -264,7 +445,7 @@ def _check_for_error(self, result=None): if self._errorPtr[0] and Base._use_exceptions: error_num = self._errorPtr[0] self._errorPtr[0] = 0 - raise DSSException(error_num, self._get_string(self._lib.Error_Get_Description())) + raise DSSException(error_num, self._lib.Error_Get_Description()) return result @@ -328,13 +509,18 @@ def altdss_python_util_callback(ctx, event_code, step, ptr): return -class CffiApiUtil(object): +class CffiApiUtil: ''' An internal class with various API and DSSContext management functions and structures. ''' _ctx_to_util = WeakKeyDictionary() + _altdss: AltDSS + def __init__(self, ffi, lib, ctx=None, is_odd=False): + self._opendssdirect = None + self._dss_python = None + self._altdss = None self._is_odd = is_odd self.owns_ctx = True self.codec = codec @@ -350,14 +536,16 @@ def __init__(self, ffi, lib, ctx=None, is_odd=False): self.lib = lib ctx = lib.ctx_Get_Prime() self.ctx = ctx - else: - self.lib = CtxLib(ctx, ffi, lib) + + self.init_buffers() + self.settings_ptr = ffi.new('int32_t*') + self.settings_ptr[0] = 0 + self.lib = CtxLib(self) CffiApiUtil._ctx_to_util[ctx] = self self._allow_complex = False self.track_objects = True - self.init_buffers() self.register_callbacks() @@ -405,7 +593,7 @@ def reprocess_buses_callback(self, step: int): # Now try to remap the objects; on exception, just invalidate everything try: ptrs = self.lib.Alt_Bus_GetListPtr() - names = self._check_for_error(self.get_string_array(self.lib.Circuit_Get_AllBusNames)) + names = self.lib.Circuit_Get_AllBusNames() except: for bus_ref in self._bus_refs: bus_ref()._invalidate_ptr() @@ -512,6 +700,7 @@ def track_obj(self, obj): self._obj_refs.append(ref(obj)) def init_buffers(self): + lib = self.lib_unpatched tmp_string_pointers = (self.ffi.new('char****'), self.ffi.new('int32_t**')) tmp_float64_pointers = (self.ffi.new('double***'), self.ffi.new('int32_t**')) tmp_int32_pointers = (self.ffi.new('int32_t***'), self.ffi.new('int32_t**')) @@ -523,7 +712,7 @@ def init_buffers(self): for ptrs in zip(tmp_string_pointers, tmp_float64_pointers, tmp_int32_pointers, tmp_int8_pointers) for ptr in ptrs ] - self.lib.DSS_GetGRPointers(*ptr_args) + lib.ctx_DSS_GetGRPointers(self.ctx, *ptr_args) # we don't need to keep the extra indirections self.gr_string_pointers = (tmp_string_pointers[0][0], tmp_string_pointers[1][0]) @@ -534,7 +723,7 @@ def init_buffers(self): # also keep a casted version for complex floats self.gr_cfloat64_pointers = (self.ffi.cast('double _Complex**', tmp_float64_pointers[0][0]), tmp_float64_pointers[1][0]) - self._errorPtr = self.lib.Error_Get_NumberPtr() + self._errorPtr = lib.ctx_Error_Get_NumberPtr(self.ctx) def clear_buffers(self): @@ -543,9 +732,9 @@ def clear_buffers(self): self.init_buffers() def get_string(self, b) -> str: - if b != self.ffi.NULL: + if b: return self.ffi.string(b).decode(self.codec) - return u'' + return '' def get_float64_array(self, func, *args) -> Float64Array: ptr = self.ffi.new('double**') @@ -655,7 +844,7 @@ def get_complex128_simple2(self, func, *args) -> List[Union[complex, float]]: def get_float64_gr_array(self) -> Float64Array: ptr, cnt = self.gr_float64_pointers if self._allow_complex and cnt[3]: - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy().reshape((cnt[2], cnt[3]), order='F') + return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy() @@ -940,6 +1129,43 @@ def prepare_string_array(self, value: List[AnyStr]): return value_enc or value, ptrs, len(ptrs) + def get_dss_obj(self, ptr) -> Optional[DSSObject]: + ''' + Get an AltDSS DSSObj instance. For internal use, but might be useful for advanced users. + The user must ensure the pointer is valid. + ''' + if not ptr: + return None + + from AltDSS import DSSObj + if self._altdss is not None: + altdss = self._altdss + else: + from AltDSS import AltDSS + altdss = AltDSS._get_instance(ctx=self.ctx, api_util=self) + + cls_idx = self._lib.Obj_GetClassIdx(ptr) + pycls = DSSObj._idx_to_cls[cls_idx] + return pycls(self, ptr) + + def get_bus_obj(self, ptr) -> Optional[AltBus]: + ''' + Get an AltDSS Bus instance. For internal use, but might be useful for advanced users. + The user must ensure the pointer is valid. + ''' + from AltDSS import Bus as AltBus + if self._altdss is not None: + altdss = self._altdss + else: + from AltDSS import AltDSS + altdss = AltDSS._get_instance(ctx=self.ctx, api_util=self) + + return AltBus(self, ptr) + + +def _oddie_not_impl(): + raise NotImplementedError("This API requires is not implemented in the official OpenDSS engine or it is available in Oddie.") + class Iterable(Base): __slots__ = [ '_Get_First', @@ -949,39 +1175,41 @@ class Iterable(Base): '_Get_Name', '_Set_Name', '_Get_idx', - '_Set_idx' + '_Set_idx', + '_Get_Pointer', ] def __init__(self, api_util): Base.__init__(self, api_util) prefix = type(self).__name__[1:] - self._Get_First = getattr(self._lib, '{}_Get_First'.format(prefix)) - self._Get_Next = getattr(self._lib, '{}_Get_Next'.format(prefix)) - self._Get_Count = getattr(self._lib, '{}_Get_Count'.format(prefix)) - self._Get_AllNames = getattr(self._lib, '{}_Get_AllNames'.format(prefix)) - self._Get_Name = getattr(self._lib, '{}_Get_Name'.format(prefix)) - self._Set_Name = getattr(self._lib, '{}_Set_Name'.format(prefix)) - self._Get_idx = getattr(self._lib, '{}_Get_idx'.format(prefix)) - self._Set_idx = getattr(self._lib, '{}_Set_idx'.format(prefix)) + self._Get_First = getattr(self._lib, '{}_Get_First'.format(prefix), _oddie_not_impl) + self._Get_Next = getattr(self._lib, '{}_Get_Next'.format(prefix), _oddie_not_impl) + self._Get_Count = getattr(self._lib, '{}_Get_Count'.format(prefix), _oddie_not_impl) + self._Get_AllNames = getattr(self._lib, '{}_Get_AllNames'.format(prefix), _oddie_not_impl) + self._Get_Name = getattr(self._lib, '{}_Get_Name'.format(prefix), _oddie_not_impl) + self._Set_Name = getattr(self._lib, '{}_Set_Name'.format(prefix), _oddie_not_impl) + self._Get_idx = getattr(self._lib, '{}_Get_idx'.format(prefix), _oddie_not_impl) + self._Set_idx = getattr(self._lib, '{}_Set_idx'.format(prefix), _oddie_not_impl) + self._Get_Pointer = getattr(self._lib, '{}_Get_Pointer'.format(prefix), _oddie_not_impl) @property def First(self) -> int: '''Sets the first object of this type active. Returns 0 if none.''' - return self._check_for_error(self._Get_First()) + return self._Get_First() @property def Next(self) -> int: '''Sets next object of this type active. Returns 0 if no more.''' - return self._check_for_error(self._Get_Next()) + return self._Get_Next() @property def Count(self) -> int: '''Number of objects of this type''' - return self._check_for_error(self._Get_Count()) + return self._Get_Count() def __len__(self) -> int: - return self._check_for_error(self._Get_Count()) + return self._Get_Count() def __iter__(self) -> Iterator[Iterable]: ''' @@ -996,27 +1224,24 @@ def __iter__(self) -> Iterator[Iterable]: **(API Extension)** ''' - idx = self._check_for_error(self._Get_First()) + idx = self._Get_First() while idx != 0: yield self - idx = self._check_for_error(self._Get_Next()) + idx = self._Get_Next() @property def AllNames(self) -> List[str]: '''Array of all names of this object type''' - return self._check_for_error(self._get_string_array(self._Get_AllNames)) + return self._Get_AllNames() @property def Name(self) -> str: '''Gets the current name or sets the active object of this type by name''' - return self._get_string(self._check_for_error(self._Get_Name())) + return self._Get_Name() @Name.setter def Name(self, Value: AnyStr): - if not isinstance(Value, bytes): - Value = Value.encode(self._api_util.codec) - - self._check_for_error(self._check_for_error(self._Set_Name(Value))) + self._Set_Name(Value) @property def idx(self) -> int: @@ -1043,9 +1268,17 @@ def idx(self) -> int: **(API Extension)** ''' - return self._check_for_error(self._Get_idx()) + return self._Get_idx() - @idx.setter - def idx(self, Value: int): - self._check_for_error(self._Set_idx(Value)) + def to_altdss(self) -> DSSObject: + ''' + Returns a Python object for the current active DSS object in this interface. + + Requires AltDSS-Python. + + *Available only for the AltDSS engine.* + **(API Extension)** + ''' + ptr = self._Get_Pointer() + return self._api_util.get_dss_obj(ptr) From 86a3286ddfd01ac05b83b17553aafe6c89112a6b Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 27 Jul 2024 15:56:40 -0300 Subject: [PATCH 06/82] Circuit.Save: adjust argument name to match docstring. --- dss/ICircuit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 14003fe4..384596ef 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -648,7 +648,7 @@ def FromJSON(self, data: Union[AnyStr, dict], options: DSSJSONFlags = 0): self._lib.Circuit_FromJSON(data, options) - def Save(self, dirOrFilePath: AnyStr, options: DSSSaveFlags) -> str: + def Save(self, dirOrFilePath: AnyStr, saveFlags: DSSSaveFlags) -> str: ''' Equivalent of the "save circuit" DSS command, but allows customization through the `saveFlags` argument, which is a set of bit flags. @@ -670,6 +670,8 @@ def Save(self, dirOrFilePath: AnyStr, options: DSSSaveFlags) -> str: **(API Extension)** ''' - return self._get_string(self._lib.Circuit_Save(dirOrFilePath, options)) + if not isinstance(dirOrFilePath, bytes): + dirOrFilePath = dirOrFilePath.encode() + return self._get_string(self._lib.Circuit_Save(dirOrFilePath, saveFlags)) From 89ea4584e734fe81ec918c51ff48e828c06adf0a Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 27 Jul 2024 16:10:50 -0300 Subject: [PATCH 07/82] WindGens: fix a couple of docstrings --- dss/IWindGens.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/dss/IWindGens.py b/dss/IWindGens.py index 4942b481..4f3d9af1 100644 --- a/dss/IWindGens.py +++ b/dss/IWindGens.py @@ -4,7 +4,6 @@ from ._cffi_api_util import Iterable from ._types import Float64Array from typing import List, Union, AnyStr -from .enums import StorageStates class IWindGens(Iterable): '''WindGen objects''' @@ -51,7 +50,7 @@ class IWindGens(Iterable): @property def RegisterNames(self) -> List[str]: ''' - Array of Storage energy meter register names + Array of WindGen energy meter register names See also the enum `GeneratorRegisters`. ''' @@ -59,7 +58,7 @@ def RegisterNames(self) -> List[str]: @property def RegisterValues(self) -> Float64Array: - '''Array of values in Storage registers.''' + '''Array of values in WindGen registers.''' return self._lib.WindGens_Get_RegisterValues_GR() @property @@ -315,11 +314,6 @@ def Phases(self) -> int: @Phases.setter def Phases(self, Value: int) -> None: - ''' - Number of phases - - (API Extension) - ''' self._lib.WindGens_Set_Phases(Value) @property From 121cb13ae5249361efefdadcfdad240b2cb3c783 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sun, 28 Jul 2024 22:17:49 -0300 Subject: [PATCH 08/82] IDSSimComs: mark as deprecated --- dss/IDSSimComs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dss/IDSSimComs.py b/dss/IDSSimComs.py index febf9c9c..623e4874 100644 --- a/dss/IDSSimComs.py +++ b/dss/IDSSimComs.py @@ -3,14 +3,20 @@ # Copyright (c) 2018-2024 DSS-Extensions contributors from ._cffi_api_util import Base from ._types import Float64Array +import warnings class IDSSimComs(Base): + ''' + **Deprecated**; use `DSS.ActiveCircuit.ActiveBus` API or the AltDSS alternatives instead + ''' __slots__ = [] def BusVoltage(self, Index: int) -> Float64Array: + warnings.warn('Use ActiveCircuit.ActiveBus or the AltDSS (AltDSS-Python) alternatives.', DeprecationWarning, stacklevel=2) return self._lib.DSSimComs_BusVoltage_GR(Index) def BusVoltagepu(self, Index: int) -> Float64Array: + warnings.warn('Use ActiveCircuit.ActiveBus or the AltDSS (AltDSS-Python) alternatives.', DeprecationWarning, stacklevel=2) return self._lib.DSSimComs_BusVoltagepu_GR(Index) From 49116ad2d458ae25b9e47593e4b34b82c72e97d8 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:24:09 -0300 Subject: [PATCH 09/82] Backend: allow using different Python API settings in ODD.py and DSS-Python. --- dss/IDSS.py | 19 ++------ dss/IText.py | 2 +- dss/_cffi_api_util.py | 106 +++++++++++++++++++++++++++++++----------- tests/test_general.py | 2 +- 4 files changed, 87 insertions(+), 42 deletions(-) diff --git a/dss/IDSS.py b/dss/IDSS.py index 6cc944c1..bf176064 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -219,7 +219,7 @@ def Start(self, code: int) -> bool: Original COM help: https://opendss.epri.com/Start.html ''' - return self._lib.DSS_Start(code) != 0 + return self._lib.DSS_Start(code) @property def Classes(self) -> List[str]: @@ -415,7 +415,7 @@ def COMErrorResults(self) -> bool: - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should be compatible with the return value of the official COM interface. - Defaults to True/1 (enabled state) in the v0.12.x series. This will change to false in future series. + Defaults to False/0 (disabled state), starting DSS-Python v0.16. This can also be set through the environment variable `DSS_CAPI_COM_DEFAULTS`. Setting it to 0 disables the legacy/COM behavior. The value can be toggled through the API at any time. @@ -446,7 +446,7 @@ def NewContext(self) -> IDSS: lib = self._api_util.lib_unpatched new_ctx = ffi.gc(lib.ctx_New(), lib.ctx_Dispose) new_api_util = CffiApiUtil(ffi, lib, new_ctx) - new_api_util._allow_complex = self._api_util._allow_complex + new_api_util._advanced_types = self._api_util._advanced_types return IDSS(new_api_util) def __call__(self, cmds: Union[AnyStr, List[AnyStr]]): @@ -512,20 +512,11 @@ def AdvancedTypes(self) -> bool: **(API Extension)** ''' - arr_dim = self._lib.DSS_Get_EnableArrayDimensions() - allow_complex = self._api_util._allow_complex - return arr_dim and allow_complex + return self._api_util._advanced_types @AdvancedTypes.setter def AdvancedTypes(self, Value: bool): - self._lib.DSS_Set_EnableArrayDimensions(Value) - _AdvancedTypes = 2 - if Value: - self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] | _AdvancedTypes - else: - self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] & ~_AdvancedTypes - - self._api_util._allow_complex = bool(Value) + self._api_util._advanced_types = bool(Value) @property def CompatFlags(self) -> int: diff --git a/dss/IText.py b/dss/IText.py index b34fa88b..70b73947 100644 --- a/dss/IText.py +++ b/dss/IText.py @@ -38,7 +38,7 @@ def Commands(self, Value: Union[AnyStr, List[AnyStr]]): **(API Extension)** ''' - if isinstance(Value, str) or isinstance(Value, bytes): + if isinstance(Value, (str, bytes)): self._lib.Text_CommandBlock(Value) else: self._set_string_array(self._lib.Text_CommandArray, Value) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 9e4def99..6b1df9cc 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -30,6 +30,11 @@ warn_wrong_case = False +_AdvancedTypes = 1 << 1 +_ODDPyStrings = 1 << 2 # TODO: check if we still need this with the new defaults +_UseLists = 1 << 3 + + def set_case_insensitive_attributes(use: bool = True, warn: bool = False): ''' This function is provided to allow easier migration from `win32com.client`. @@ -194,7 +199,7 @@ def _prepare_api_functions_slow(self, done): done.update(vars(self).keys()) - def _prepare_api_functions(self, done): + def _prepare_api_functions(self, done, settings_ptr): if AltDSS_PyContext is None: self._prepare_api_functions_slow(done) return @@ -202,7 +207,7 @@ def _prepare_api_functions(self, done): ctx = self._ctx ffi = self._ffi ctx_int = int(ffi.cast('uintptr_t', ctx)) - self._settings_ptr = self._api_util.settings_ptr + self._settings_ptr = settings_ptr settings_ptr_int = int(ffi.cast('uintptr_t', self._settings_ptr)) if not self._api_util._is_odd: @@ -248,9 +253,9 @@ def _error_checked_ctx_gr(self, _errorPtr, ctx, f, _res_func, *args): return _res_func() - def __init__(self, api_util): + def __init__(self, api_util, settings_ptr): self._api_util = api_util # this is not ready, don't use it yet - lib = self._lib =api_util.lib_unpatched + lib = self._lib = api_util.lib_unpatched ctx = self._ctx = api_util.ctx ffi = self._ffi = api_util.ffi @@ -261,7 +266,7 @@ def __init__(self, api_util): # Wrap most of the API to provide simpler Python access done = set(('ctx_Error_Get_Description', 'ctx_Error_Get_Number')) - self._prepare_api_functions(done) + self._prepare_api_functions(done, settings_ptr) self.Error_Get_Description = lambda: lib.ctx_Error_Get_Description(ctx) skip_funcs = {'ctx_New', 'ctx_Dispose', 'ctx_Get_Prime', 'ctx_Set_Prime', 'ctx_Error_Set_Description', 'ctx_Error_Get_NumberPtr', 'ctx_ZIP_Extract_GR'} @@ -353,10 +358,10 @@ class Base: ] _use_exceptions = True + _oddpy = False def __init__(self, api_util, prefer_lists=False): object.__setattr__(self, '_frozen_attrs', False) - self._lib = api_util.lib self._api_util = api_util self._get_string = api_util.get_string @@ -364,6 +369,9 @@ def __init__(self, api_util, prefer_lists=False): self._get_fcomplex128_array = api_util.get_fcomplex128_array self._get_fcomplex128_simple = api_util.get_fcomplex128_simple self._get_fcomplex128_gr_simple = api_util.get_fcomplex128_gr_simple + + self._lib = api_util._get_lib(prefer_lists, self._oddpy) + if not prefer_lists: # Use NumPy arrays for most functions self._get_float64_array = api_util.get_float64_array @@ -540,13 +548,59 @@ def __init__(self, ffi, lib, ctx=None, is_odd=False): self.init_buffers() self.settings_ptr = ffi.new('int32_t*') self.settings_ptr[0] = 0 - self.lib = CtxLib(self) - - CffiApiUtil._ctx_to_util[ctx] = self + self.lib = CtxLib(self, self.settings_ptr) + if ctx not in CffiApiUtil._ctx_to_util: + CffiApiUtil._ctx_to_util[ctx] = self - self._allow_complex = False self.track_objects = True self.register_callbacks() + self.lib_odd = None + + @property + def _advanced_types(self) -> bool: + return (self.settings_ptr[0] & _AdvancedTypes) != 0 + + @_advanced_types.setter + def _advanced_types(self, value: bool): + if value: + self.settings_ptr[0] = self.settings_ptr[0] | _AdvancedTypes + else: + self.settings_ptr[0] = self.settings_ptr[0] & ~_AdvancedTypes + + def _get_lib(self, prefer_lists: bool, oddpy: bool): + ''' + Returns a context prepared for OpenDSSDirect.py + + This should be removed as we unify settings across the modules later. + ''' + if AltDSS_PyContext is None or not oddpy: + # If the fast module is not available, nothing to do + + if prefer_lists: + self.settings_ptr[0] = self.settings_ptr[0] | _UseLists + else: + self.settings_ptr[0] = self.settings_ptr[0] & (~_UseLists) + + return self.lib + + if self.lib_odd is not None: + # We already have a prepared object, just ensure the settings are OK + + if prefer_lists: + self.settings_oddpy_ptr[0] = self.settings_oddpy_ptr[0] | _ODDPyStrings | _UseLists + else: + self.settings_oddpy_ptr[0] = (self.settings_oddpy_ptr[0] | _ODDPyStrings) & (~_UseLists) + + return self.lib_odd + + self.settings_oddpy_ptr = self.ffi.new('int32_t*') + if prefer_lists: + self.settings_oddpy_ptr[0] = self.settings_ptr[0] | _ODDPyStrings | _UseLists + else: + self.settings_oddpy_ptr[0] = (self.settings_ptr[0] | _ODDPyStrings) & (~_UseLists) + + self.lib_odd = CtxLib(self, self.settings_oddpy_ptr) + return self.lib_odd def _check_for_error(self, result=None): @@ -743,7 +797,7 @@ def get_float64_array(self, func, *args) -> Float64Array: res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy() self.lib.DSS_Dispose_PDouble(ptr) - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: # If the last element is filled, we have a matrix. Otherwise, the # matrix feature is disabled or the result is indeed a vector return res.reshape((cnt[2], cnt[3]), order='F') @@ -751,7 +805,7 @@ def get_float64_array(self, func, *args) -> Float64Array: return res def get_complex128_array(self, func, *args) -> Float64ArrayOrComplexArray: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_array(func, *args) # Currently we use the same as API as get_float64_array, may change later @@ -787,7 +841,7 @@ def get_fcomplex128_array(self, func, *args) -> Union[ComplexArray, None]: return res def get_complex128_array2(self, func, *args) -> Float64ArrayOrComplexArray: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_array2(func, *args) # Currently we use the same as API as get_float64_array, may change later @@ -801,7 +855,7 @@ def get_complex128_array2(self, func, *args) -> Float64ArrayOrComplexArray: def get_complex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_array(func, *args) # Currently we use the same as API as get_float64_array, may change later @@ -827,7 +881,7 @@ def get_fcomplex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: def get_complex128_simple2(self, func, *args) -> List[Union[complex, float]]: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_array2(func, *args) # Currently we use the same as API as get_float64_array, may change later @@ -843,19 +897,19 @@ def get_complex128_simple2(self, func, *args) -> List[Union[complex, float]]: def get_float64_gr_array(self) -> Float64Array: ptr, cnt = self.gr_float64_pointers - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy() def get_complex128_gr_array(self) -> ComplexArray: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_gr_array() # Currently we use the same as API as get_float64_array, may change later ptr, cnt = self.gr_float64_pointers - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() @@ -864,14 +918,14 @@ def get_complex128_gr_array(self) -> ComplexArray: def get_fcomplex128_gr_array(self) -> ComplexArray: # Currently we use the same as API as get_float64_array, may change later ptr, cnt = self.gr_float64_pointers - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() def get_complex128_gr_array2(self) -> List[Union[complex, float]]: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_gr_array2() # Currently we use the same as API as get_float64_array, may change later @@ -881,7 +935,7 @@ def get_complex128_gr_array2(self) -> List[Union[complex, float]]: def get_complex128_gr_simple(self) -> Float64ArrayOrSimpleComplex: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_gr_array() # Currently we use the same as API as get_float64_array, may change later @@ -898,7 +952,7 @@ def get_fcomplex128_gr_simple(self) -> complex: def get_complex128_gr_simple2(self) -> List[Union[complex, float]]: - if not self._allow_complex: + if not self._advanced_types: return self.get_float64_gr_array2() # Currently we use the same as API as get_float64_array, may change later @@ -914,7 +968,7 @@ def get_int32_array(self, func: Callable, *args) -> Int32Array: res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() self.lib.DSS_Dispose_PInteger(ptr) - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: # If the last element is filled, we have a matrix. Otherwise, the # matrix feature is disabled or the result is indeed a vector return res.reshape((cnt[2], cnt[3])) @@ -933,7 +987,7 @@ def get_ptr_array(self, func: Callable, *args): def get_int32_gr_array(self) -> Int32Array: ptr, cnt = self.gr_int32_pointers - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy().reshape((cnt[2], cnt[3])) return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() @@ -946,7 +1000,7 @@ def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() self.lib.DSS_Dispose_PByte(ptr) - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: # If the last element is filled, we have a matrix. Otherwise, the # matrix feature is disabled or the result is indeed a vector return res.reshape((cnt[2], cnt[3])) @@ -956,7 +1010,7 @@ def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: def get_int8_gr_array(self) -> Int8Array: ptr, cnt = self.gr_int8_pointers - if self._allow_complex and cnt[3]: + if cnt[3] and self._advanced_types: return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy().reshape((cnt[2], cnt[3]), order='F') return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() diff --git a/tests/test_general.py b/tests/test_general.py index 1c84b80b..ce42fb6d 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -25,7 +25,7 @@ def setup_function(): DSS.AllowEditor = False DSS.AdvancedTypes = False DSS.AllowChangeDir = True - DSS.COMErrorResults = True # TODO: change to False + DSS.COMErrorResults = False DSS.CompatFlags = 0 DSS.AllowForms = False From b2f91682c3c6f354080e9c3f3569510aa835476f Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:34:44 -0300 Subject: [PATCH 10/82] Wrappers: wrap bools, ignore missing functions with Oddie; tweak imports for Oddie with _fastdss. --- dss/_cffi_api_util.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 6b1df9cc..c3a53a02 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -120,6 +120,15 @@ def _get_strs_ctx(self, errorPtr, ctx, func: Callable, *args: Any) -> List[str]: return res + def _get_bool_ctx(self, errorPtr, ctx, func: Callable, *args): + result = func(ctx, *args) + if errorPtr[0] and Base._use_exceptions: + error_num = errorPtr[0] + errorPtr[0] = 0 + raise DSSException(error_num, self.Error_Get_Description()) + + return result != 0 + def _get_str_ctx(self, errorPtr, ctx, func: Callable, *args): codec = self._api_util.codec ffi = self._ffi @@ -157,8 +166,10 @@ def _prepare_api_functions_slow(self, done): errorPtr = self._errorPtr t = _func_info.t api_util = self._api_util + is_odd = api_util._is_odd wrappers = { + t.fastdss_types_b16: ('', self._get_bool_ctx,), t.fastdss_types_str: ('', self._get_str_ctx,), t.fastdss_types_strs: ('', self._get_strs_ctx,), t.fastdss_types_gr_f64s: ('_GR', self._error_checked_ctx_gr, api_util.get_float64_gr_array), @@ -191,7 +202,13 @@ def _prepare_api_functions_slow(self, done): ctx_name += suffix - func = getattr(lib, ctx_name) + try: + func = getattr(lib, ctx_name) + except AttributeError: + if is_odd: + continue + + raise prepared_func = arg_wrapper(partial(wrapper, errorPtr, ctx, func, *wrapper_args)) setattr(self, name, prepared_func) @@ -213,11 +230,7 @@ def _prepare_api_functions(self, done, settings_ptr): if not self._api_util._is_odd: self._fast = AltDSS_PyContext(ctx_int, settings_ptr_int, DSSException, done, self) else: - try: - from dss_python_backend._fastdss_oddie import AltDSS_PyContext as AltDSS_PyContext_Oddie - except: - AltDSS_PyContext_Oddie = None - + from dss_python_backend._fastdss_oddie import AltDSS_PyContext as AltDSS_PyContext_Oddie self._fast = AltDSS_PyContext_Oddie(ctx_int, settings_ptr_int, DSSException, done, self) From db26e4d5d95d5787c7d63d83053932953997aaef Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:26:44 -0300 Subject: [PATCH 11/82] Wrappers: Ensure Error_Get_Description is wrapped --- dss/_cffi_api_util.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index c3a53a02..8bfc5f80 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -244,7 +244,7 @@ def _error_checked(self, _errorPtr, f, *args): if _errorPtr[0] and Base._use_exceptions: error_num = _errorPtr[0] _errorPtr[0] = 0 - raise DSSException(error_num, self._get_string(self.Error_Get_Description())) + raise DSSException(error_num, self.Error_Get_Description()) return result @@ -253,7 +253,7 @@ def _error_checked_ctx(self, _errorPtr, ctx, f, *args): if _errorPtr[0] and Base._use_exceptions: error_num = _errorPtr[0] _errorPtr[0] = 0 - raise DSSException(error_num, self._get_string(self.Error_Get_Description())) + raise DSSException(error_num, self.Error_Get_Description()) return result @@ -262,7 +262,7 @@ def _error_checked_ctx_gr(self, _errorPtr, ctx, f, _res_func, *args): if _errorPtr[0] and Base._use_exceptions: error_num = _errorPtr[0] _errorPtr[0] = 0 - raise DSSException(error_num, self._get_string(self.Error_Get_Description())) + raise DSSException(error_num, self.Error_Get_Description()) return _res_func() @@ -277,10 +277,10 @@ def __init__(self, api_util, settings_ptr): self._prepared_funcs = [] # Wrap most of the API to provide simpler Python access - done = set(('ctx_Error_Get_Description', 'ctx_Error_Get_Number')) + done = set(('ctx_Error_Get_Description', 'ctx_Error_Get_Number', 'Error_Get_Description', 'Error_Get_Number')) self._prepare_api_functions(done, settings_ptr) - self.Error_Get_Description = lambda: lib.ctx_Error_Get_Description(ctx) + self.Error_Get_Description = lambda: self._get_string(lib.ctx_Error_Get_Description(ctx)) skip_funcs = {'ctx_New', 'ctx_Dispose', 'ctx_Get_Prime', 'ctx_Set_Prime', 'ctx_Error_Set_Description', 'ctx_Error_Get_NumberPtr', 'ctx_ZIP_Extract_GR'} # First, process all `ctx_*`` functions @@ -631,7 +631,7 @@ def _check_for_error(self, result=None): if self._errorPtr[0] and Base._use_exceptions: error_num = self._errorPtr[0] self._errorPtr[0] = 0 - raise DSSException(error_num, self.get_string(self.lib.Error_Get_Description())) + raise DSSException(error_num, self.lib.Error_Get_Description()) return result From 2f52a7226394e9e5307ce6e5371ccb2cd2a15d77 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 7 Aug 2024 20:39:01 -0300 Subject: [PATCH 12/82] Monitors: use the raw function for the bytestream --- dss/IMonitors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dss/IMonitors.py b/dss/IMonitors.py index bc1cb6b8..7fe33b8f 100644 --- a/dss/IMonitors.py +++ b/dss/IMonitors.py @@ -44,7 +44,7 @@ def Channel(self, Index: int) -> Float32Array: ffi = self._api_util.ffi api_util = self._api_util - api_util.lib_unpatched.Monitors_Get_ByteStream_GR() + api_util.lib_unpatched.ctx_Monitors_Get_ByteStream_GR(api_util.ctx) api_util._check_for_error() ptr, cnt = api_util.gr_int8_pointers cnt = cnt[0] @@ -67,7 +67,7 @@ def AsMatrix(self) -> Float64Array: ffi = self._api_util.ffi api_util = self._api_util - api_util.lib_unpatched.Monitors_Get_ByteStream_GR() + api_util.lib_unpatched.ctx_Monitors_Get_ByteStream_GR(api_util.ctx) api_util._check_for_error() ptr, cnt = api_util.gr_int8_pointers cnt = cnt[0] From 56a233d6d52502907890997ac56eac6f0f500d63 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 15 Aug 2024 17:05:26 -0300 Subject: [PATCH 13/82] CtrlQueue: add type hints for ActionHandle --- dss/ICtrlQueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dss/ICtrlQueue.py b/dss/ICtrlQueue.py index 4f77df5f..b20b6dec 100644 --- a/dss/ICtrlQueue.py +++ b/dss/ICtrlQueue.py @@ -31,7 +31,7 @@ def ClearQueue(self): ''' self._lib.CtrlQueue_ClearQueue() - def Delete(self, ActionHandle): + def Delete(self, ActionHandle: int): ''' Delete an Action from the DSS Control Queue by the handle that is returned when the action is added. @@ -94,7 +94,7 @@ def NumActions(self) -> int: ''' return self._lib.CtrlQueue_Get_NumActions() - def Push(self, Hour: int, Seconds: float, ActionCode: int, DeviceHandle: int): + def Push(self, Hour: int, Seconds: float, ActionCode: int, DeviceHandle: int) -> int: ''' Push a control action onto the DSS control queue by time, action code, and device handle (user defined). Returns Control Queue handle. From d1408ee83217af83f05d1f0d0cd8113d460736b8 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:34:10 -0300 Subject: [PATCH 14/82] Plot: generalize `get_gic_line_data`; tests pending. --- dss/plot.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index baa4d8b7..fad722a0 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -5,9 +5,9 @@ This is not a complete implementation and there are known limitations, but should suffice for many use-cases. We'd like to add another backend later. """ -import warnings -from typing import List -import os +from __future__ import annotations +import os, re, json, sys, warnings +from typing import List, TYPE_CHECKING from . import api_util from . import DSS as DSSPrime from ._cffi_api_util import CffiApiUtil @@ -25,7 +25,8 @@ except: raise ImportError("SciPy and matplotlib are required to use this module.") -import re, json, sys, warnings +if TYPE_CHECKING: + from altdss.AltDSS import IAltDSS try: from IPython import get_ipython @@ -768,8 +769,8 @@ def dss_profile_plot(DSS, params): -def get_gic_line_data(DSS: IDSS, bus_coords, single_ph_line_style=1, three_ph_line_style=1): - branch_objects = DSS.Obj.GICLine +def _get_gic_line_data_altdss(altdss: IAltDSS, bus_coords, single_ph_line_style=1, three_ph_line_style=1): + branch_objects = altdss.GICLine line_count = len(branch_objects)# if not idxs else len(idxs) lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) lines.fill(np.nan) @@ -780,7 +781,7 @@ def get_gic_line_data(DSS: IDSS, bus_coords, single_ph_line_style=1, three_ph_li # skip = set() # GIC lines are not exposed nicely in the classic API, so we'll use the new Obj API - for gic_line in DSS.Obj.GICLine: + for gic_line in altdss.GICLine: if not gic_line.enabled: continue @@ -797,7 +798,55 @@ def get_gic_line_data(DSS: IDSS, bus_coords, single_ph_line_style=1, three_ph_li lines[offset, 1] = to lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style - max_current = DSS._lib.Obj_CktElement_MaxCurrent(gic_line._ptr, 1) + values[offset] = gic_line.MaxCurrent(1) + offset += 1 + + return lines[:offset], values[:offset], lines_styles[:offset] + + +def get_gic_line_data(DSS: IDSS, bus_coords, single_ph_line_style=1, three_ph_line_style=1): + try: + return _get_gic_line_data_altdss( + DSS.to_altdss(), + bus_coords, + single_ph_line_style=single_ph_line_style, + three_ph_line_style=three_ph_line_style + ) + except: + pass + + # Fallback for Oddie and COM + DSS.ActiveCircuit.SetActiveClass('GICLine') + aclass = DSS.ActiveCircuit.ActiveClass + line_count = aclass.Count# if not idxs else len(idxs) + lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) + lines.fill(np.nan) + values = np.empty(shape=(line_count, ), dtype=np.float64) + values.fill(np.nan) + lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) + offset = 0 + # skip = set() + + # GIC lines are not exposed nicely in the classic API + elem = DSS.ActiveCircuit.ActiveCktElement + idx = aclass.First + while idx != 0: + buses = elem.BusNames + b1 = remove_nodes(buses[0]) + b2 = remove_nodes(buses[1]) + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None: + # skip.add(idx) + continue + + lines[offset, 0] = fr + lines[offset, 1] = to + + lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style + currents = np.abs(np.asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(current[:elem.NumConductors]) values[offset] = max_current offset += 1 From 75435fe8e46774511a68468dd8fb0923cb4558ff Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 16 Aug 2024 01:41:51 -0300 Subject: [PATCH 15/82] Plot: add alternative path when `element.IsIsolated` is not available. --- dss/plot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dss/plot.py b/dss/plot.py index fad722a0..967d81fe 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -428,6 +428,13 @@ def get_branch_data(DSS, branch_objects, bus_coords, do_values=pqNone, do_switch if do_switches: switch_idxs = [] isolated_idxs = [] + try: + element.IsIsolated + has_is_isolated = True + except: + has_is_isolated = False + isolated_names = set(name.lower() for name in DSS.ActiveCircuit.Topology.AllIsolatedBranches) + extra = [switch_idxs, isolated_idxs] else: extra = [] @@ -523,8 +530,10 @@ def get_branch_data(DSS, branch_objects, bus_coords, do_values=pqNone, do_switch continue if do_switches: - if element.IsIsolated: + if ((has_is_isolated and element.IsIsolated) or + ((not has_is_isolated) and (element.Name.lower() in isolated_names))): isolated_idxs.append(offset) + if l.IsSwitch: #skip.add(i) switch_idxs.append(offset) From 84e6592d9860315be3d9048018df14bd2709daa9 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:59:08 -0300 Subject: [PATCH 16/82] Plot: refactoring, part 1 (typing) --- dss/plot.py | 561 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 437 insertions(+), 124 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index 967d81fe..f7f5a8d4 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -7,12 +7,15 @@ """ from __future__ import annotations import os, re, json, sys, warnings -from typing import List, TYPE_CHECKING +from typing import List, TYPE_CHECKING, Optional, Tuple, Dict +from typing_extensions import TypedDict, Unpack from . import api_util from . import DSS as DSSPrime from ._cffi_api_util import CffiApiUtil from .IDSS import IDSS from .IBus import IBus +from ._cffi_api_util import Iterable as DSSIterable +from enum import Enum, IntEnum try: import numpy as np from matplotlib import pyplot as plt @@ -28,6 +31,199 @@ if TYPE_CHECKING: from altdss.AltDSS import IAltDSS + +class DSSPlotType(Enum): + AutoAddLog = 'AutoAddLog' + Circuit = 'Circuit' + Daisy = 'Daisy' + Energy = 'Energy' + Evolution = 'Evolution' + GeneralData = 'GeneralData' + LoadShape = 'LoadShape' + Matrix = 'Matrix' + MeterZones = 'MeterZones' + Monitor = 'Monitor' + PhaseVoltage = 'PhaseVoltage' + PriceShape = 'PriceShape' + Profile = 'Profile' + Scatter = 'Scatter' + TShape = 'TShape' + + +(pqVoltage, pqCurrent, pqPower, pqLosses, pqCapacity, pqNone) = range(6) + +class DSSPlotQuantity(Enum): + Capacities = 'Capacities' + Currents = 'Currents' + Losses = 'Losses' + Powers = 'Powers' + Voltages = 'Voltages' + none = 'None' + + +class ProfileScale(Enum): + pukm = 'pukm' + kft120 = '120kft' + + +class ObjMarkers(TypedDict): + NodeMarkerCode: Optional[int] + NodeMarkerWidth: Optional[float] + + MarkTransformers: Optional[bool] + TransMarkerCode: Optional[int] + TransMarkerSize: Optional[float] + + MarkCapacitors: Optional[bool] + CapMarkerCode: Optional[int] + CapMarkerSize: Optional[float] + + MarkPVSystems: Optional[bool] + PVMarkerCode: Optional[int] + PVMarkerSize: Optional[float] + + MarkFuses: Optional[bool] + FuseMarkerCode: Optional[int] + FuseMarkerSize: Optional[float] + + MarkReclosers: Optional[bool] + RecloserMarkerCode: Optional[int] + RecloserMarkerSize: Optional[float] + + MarkRegulators: Optional[bool] + RegMarkerCode: Optional[int] + RegMarkerSize: Optional[float] + + MarkRelays: Optional[bool] + RelayMarkerCode: Optional[int] + RelayMarkerSize: Optional[float] + + MarkStorage: Optional[bool] + StoreMarkerCode: Optional[int] + StoreMarkerSize: Optional[float] + + MarkSwitches: Optional[bool] + SwitchMarkerCode: Optional[int] + + +class BusMarker(TypedDict): + Name: str + Color: str + Code: int + Size: float + + +class DSSPlotPhases(IntEnum): + PROFILE3PH = -1 # Default + PROFILEALL = -2 # All + PROFILEALLPRI = -3 # Primary + PROFILELL3PH = -4 # LL3Ph + PROFILELLALL = -5 # LLAll + PROFILELLPRI = -6 # LLPrimary + + +class PlotParams(TypedDict): + PlotType: DSSPlotType + MatrixType: str + MaxScale: float + MinScale: float + Dots: bool + Labels: bool + ShowLoops: bool + ShowSubs: bool + Quantity: str + ObjectName: str + PlotId: str #TODO + ValueIndex: int + PhasesToPlot: DSSPlotPhases + ProfileScale: str + Channels: List[int] + Bases: Optional[List[float]] + SinglePhLineStyle: int + ThreePhLineStyle: int + Color1: str + Color2: str + Color3: str + TriColorMax: float + TriColorMid: float + MaxScaleIsSpecified: bool + MinScaleIsSpecified: bool + DaisyBusList: List[str] + DaisySize: float + MaxLineThickness: float + MarkerParams: Optional[ObjMarkers] + BusMarkers: Optional[List[BusMarker]] + + Registers: List[int] + PeakDay: bool + MeterName: str + CaseName: str + CaseYear: int + +DEFAULT_MARKER_PARAMS = ObjMarkers( + MarkTransformers=False + # TransMarkerCode: Optional[int] + # TransMarkerSize: Optional[float] + + # MarkCapacitors: Optional[bool] + # CapMarkerCode: Optional[int] + # CapMarkerSize: Optional[float] + + # MarkPVSystems: Optional[bool] + # PVMarkerCode: Optional[int] + # PVMarkerSize: Optional[float] + + # MarkStorage: Optional[bool] + # StoreMarkerCode: Optional[int] + # StoreMarkerSize: Optional[float] + + # MarkSwitches: Optional[bool] + # SwitchMarkerCode: Optional[int] + + # MarkFuses: Optional[bool] + # FuseMarkerCode: Optional[int] + # FuseMarkerSize: Optional[float] + + # MarkRegulators: Optional[bool] + # RegMarkerCode: Optional[int] + # RegMarkerSize: Optional[float] + + # MarkRelays: Optional[bool] + # RelayMarkerCode: Optional[int] + # RelayMarkerSize: Optional[float] + + # MarkReclosers: Optional[bool] + # RecloserMarkerCode: Optional[int] + # RecloserMarkerSize: Optional[float] +) + +DEFAULT_PLOT_PARAMS = PlotParams( + PlotType=DSSPlotType.Circuit, + Quantity=DSSPlotQuantity.Powers, + Channels=[1, 3, 5], + MarkerParams=DEFAULT_MARKER_PARAMS, + BusMarkers=[], + Color1='#0000FF', + Color2='#008000', + Color3='#FF0000', + TriColorMax=0.85, + TriColorMid=0.50, + ThreePhLineStyle=1, + SinglePhLineStyle=1, + ProfileScale=ProfileScale.pukm, + PhasesToPlot=DSSPlotPhases.PROFILE3PH, + DaisyBusList=[], + MaxLineThickness=10, + Dots=False, + Labels=False, + ShowLoops=False, + ShowSubs=False, + MinScaleIsSpecified=False, + MaxScaleIsSpecified=False, + MinScale=0.0, + MaxScale=None, +) + try: from IPython import get_ipython from IPython.display import FileLink, display, display_html, HTML @@ -73,15 +269,6 @@ def show(text): include_3d = '2d' # '2d' (default), '3d' (prefer 3d), 'both' -PROFILE3PH = -1 # Default -PROFILEALL = -2 # All -PROFILEALLPRI = -3 # Primary -PROFILELL3PH = -4 # LL3Ph -PROFILELLALL = -5 # LLAll -PROFILELLPRI = -6 # LLPrimary - -(pqVoltage, pqCurrent, pqPower, pqLosses, pqCapacity, pqNone) = range(6) - str_to_pq = { 'Voltages': pqVoltage, 'Currents': pqCurrent, @@ -224,20 +411,26 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._dss.AdvancedTypes = self._previous -def dss_monitor_plot(DSS: IDSS, params): +def dss_monitor_plot(DSS: IDSS, + *, + ObjectName: str = None, + Channels: List[int] = None, # TODO: allow channel names too + Bases: List[float] = None, + **kwargs: Unpack[PlotParams] +): monitor = DSS.ActiveCircuit.Monitors - monitor.Name = params['ObjectName'] + monitor.Name = ObjectName data = monitor.AsMatrix() if data is None or len(data) == 0: raise ValueError("There is not data to plot in the monitor. Hint: check the solution mode, solve the circuit and retry.") - channels = params['Channels'] + channels = Channels num_ch = monitor.NumChannels channels = [ch for ch in channels if ch >= 1 and ch <= num_ch] if len(channels) == 0: raise IndexError("No valid channel numbers were specified.") - bases = params['Bases'] + bases = Bases header = monitor.Header if len(monitor.dblHour) < len(monitor.dblFreq): header.insert(0, 'Frequency') @@ -278,14 +471,18 @@ def dss_monitor_plot(DSS: IDSS, params): ax.legend() ax.set_ylabel('Mag') # Where "Mag" comes from? - ax.set_title(params['ObjectName']) + ax.set_title(ObjectName) ax.set_xlabel(xlabel) - -def dss_tshape_plot(DSS, params): +def dss_tshape_plot(DSS: IDSS, + *, + ObjectName: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] +): # There is no dedicated API yet but we can move to the Obj API - name = params['ObjectName'] + name = ObjectName DSS.Text.Command = f'? tshape.{name}.temp' p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') try: @@ -300,7 +497,7 @@ def dss_tshape_plot(DSS, params): except: interval = 1 - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"TShape.{params['ObjectName']}") + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"TShape.{ObjectName}") if not h.size: h = interval * np.array(range(len(p))) @@ -310,9 +507,9 @@ def dss_tshape_plot(DSS, params): h *= 3600 x_unit = 's' - color1 = params['Color1'] + color1 = Color1 ax.plot(h, p, color=color1, label="Price") - ax.set_title(f"TShape = {params['ObjectName']}") + ax.set_title(f"TShape = {ObjectName}") ax.set_xlabel(f'Time ({x_unit})') ax.set_ylabel('Temperature') @@ -321,9 +518,14 @@ def dss_tshape_plot(DSS, params): -def dss_priceshape_plot(DSS, params): +def dss_priceshape_plot(DSS: IDSS, + *, + ObjectName: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] +): # There is no dedicated API yet but we can move to the Obj API - name = params['ObjectName'] + name = ObjectName DSS.Text.Command = f'? priceshape.{name}.price' p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') try: @@ -338,7 +540,7 @@ def dss_priceshape_plot(DSS, params): except: interval = 1 - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"PriceShape.{params['ObjectName']}") + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"PriceShape.{ObjectName}") if not h.size: h = interval * np.array(range(len(p))) @@ -348,10 +550,10 @@ def dss_priceshape_plot(DSS, params): h *= 3600 x_unit = 's' - color1 = params['Color1'] + color1 = Color1 ax.plot(h, p, color=color1, label="Price") - ax.set_title(f"PriceShape = {params['ObjectName']}") + ax.set_title(f"PriceShape = {ObjectName}") ax.set_xlabel(f'Time ({x_unit})') ax.set_ylabel('Price') @@ -359,16 +561,22 @@ def dss_priceshape_plot(DSS, params): plt.tight_layout() -def dss_loadshape_plot(DSS, params): -# pprint(params) +def dss_loadshape_plot(DSS: IDSS, + *, + ObjectName: str = None, + Color1: str = None, + Color2: str = None, + **kwargs: Unpack[PlotParams] +): +# pprint(kwargs) ls = DSS.ActiveCircuit.LoadShapes - ls.Name = params['ObjectName'] + ls.Name = ObjectName h = ls.TimeArray p = ls.Pmult q = ls.Qmult - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"LoadShape.{params['ObjectName']}") + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"LoadShape.{ObjectName}") if not h.size or h is None or len(h) != len(p): h = ls.HrInterval * np.array(range(len(p))) @@ -378,14 +586,14 @@ def dss_loadshape_plot(DSS, params): h *= 3600 x_unit = 's' - color1 = params['Color1'] - color2 = params['Color2'] + color1 = Color1 + color2 = Color2 ax.plot(h, p, color=color1, label="Pmult") if q.size == p.size: ax.plot(h, q, color=color2, label="Qmult") - ax.set_title(f"LoadShape = {params['ObjectName']}") + ax.set_title(f"LoadShape = {ObjectName}") ax.set_xlabel(f'Time ({x_unit})') if ls.UseActual: if q.size == p.size: @@ -403,7 +611,6 @@ def dss_loadshape_plot(DSS, params): node_re = re.compile(r'(.*?)(\.[0-9])*$') - def remove_nodes(bus): match = node_re.match(bus) return match.group(1) @@ -415,7 +622,15 @@ def remove_nodes(bus): # return bus[:dot_pos] -def get_branch_data(DSS, branch_objects, bus_coords, do_values=pqNone, do_switches=False, idxs=None, single_ph_line_style =1, three_ph_line_style=1): +def get_branch_data(DSS: IDSS, + branch_objects: DSSIterable, + bus_coords: Dict[str, Tuple[float, float, float]], + do_values=pqNone, + do_switches=False, + idxs=None, + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 +): line_count = branch_objects.Count if not idxs else len(idxs) lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) lines.fill(np.nan) @@ -584,7 +799,11 @@ def get_branch_data(DSS, branch_objects, bus_coords, do_values=pqNone, do_switch return [lines[:offset], values[:offset], lines_styles[:offset]] + extra -def get_point_data(DSS: IDSS, point_objects, bus_coords, do_values=False): +def get_point_data(DSS: IDSS, + point_objects: Union[str, Iterable], + bus_coords: Dict[str, Tuple[float, float, float]], + do_values: bool = False +): if isinstance(point_objects, str): cls = point_objects DSS.SetActiveClass(cls) @@ -627,12 +846,14 @@ def get_point_data(DSS: IDSS, point_objects, bus_coords, do_values=False): return points[:offset], values[:offset] -def dss_profile_plot(DSS, params): +def dss_profile_plot(DSS: IDSS, + *, + PhasesToPlot: int = None, + ProfileScale: float = None, + **kwargs: Unpack[PlotParams] +): if len(DSS.ActiveCircuit.Meters) == 0: raise RuntimeError(f"An EnergyMeter is required to use 'plot profile'") - - PhasesToPlot = params['PhasesToPlot'] - ProfileScale = params['ProfileScale'] vmin = DSS.ActiveCircuit.Settings.NormVminpu vmax = DSS.ActiveCircuit.Settings.NormVmaxpu @@ -660,8 +881,8 @@ def dss_profile_plot(DSS, params): colors = [] linestyles = [] seg_phases = [] - pri_only = (PhasesToPlot == PROFILEALLPRI) - if PhasesToPlot in [PROFILEALL, PROFILEALLPRI, PROFILE3PH]: + pri_only = (PhasesToPlot == DSSPlotPhases.PROFILEALLPRI) + if PhasesToPlot in [DSSPlotPhases.PROFILEALL, DSSPlotPhases.PROFILEALLPRI, DSSPlotPhases.PROFILE3PH]: phases = (1, 2, 3) else: phases = PhasesToPlot @@ -682,7 +903,7 @@ def dss_profile_plot(DSS, params): DSS.ActiveCircuit.Lines.Name = br[len('Line.'):] - if PROFILE3PH == PhasesToPlot and DSS.ActiveCircuit.Lines.Phases < 3: + if DSSPlotPhases.PROFILE3PH == PhasesToPlot and DSS.ActiveCircuit.Lines.Phases < 3: continue bus1 = nodot(DSS.ActiveCircuit.Lines.Bus1) @@ -714,7 +935,7 @@ def dss_profile_plot(DSS, params): ax = fig.add_subplot(1, 1, 1) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) - if PhasesToPlot in (PROFILELL3PH, PROFILELLALL, PROFILELLPRI): + if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): ax.set_title('L-L Voltage Profile') else: ax.set_title('L-N Voltage Profile') @@ -737,7 +958,7 @@ def dss_profile_plot(DSS, params): ax2 = fig2.add_subplot(1, 1, 1, projection='3d') ax2.set_xlabel(xlabel) ax2.set_ylabel(ylabel) - if PhasesToPlot in (PROFILELL3PH, PROFILELLALL, PROFILELLPRI): + if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): ax2.set_title('L-L Voltage Profile') else: ax2.set_title('L-N Voltage Profile') @@ -778,7 +999,12 @@ def dss_profile_plot(DSS, params): -def _get_gic_line_data_altdss(altdss: IAltDSS, bus_coords, single_ph_line_style=1, three_ph_line_style=1): +def _get_gic_line_data_altdss( + altdss: IAltDSS, + bus_coords: Dict[str, Tuple[float, float, float]], + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 +): branch_objects = altdss.GICLine line_count = len(branch_objects)# if not idxs else len(idxs) lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) @@ -813,7 +1039,11 @@ def _get_gic_line_data_altdss(altdss: IAltDSS, bus_coords, single_ph_line_style= return lines[:offset], values[:offset], lines_styles[:offset] -def get_gic_line_data(DSS: IDSS, bus_coords, single_ph_line_style=1, three_ph_line_style=1): +def get_gic_line_data(DSS: IDSS, + bus_coords: Dict[str, Tuple[float, float]], + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 +): try: return _get_gic_line_data_altdss( DSS.to_altdss(), @@ -861,18 +1091,40 @@ def get_gic_line_data(DSS: IDSS, bus_coords, single_ph_line_style=1, three_ph_li return lines[:offset], values[:offset], lines_styles[:offset] -def dss_circuit_plot(DSS: IDSS, params={}, fig=None, ax=None, is3d=False): - quantity = str_to_pq.get(params.get('Quantity', None), pqNone) - dots = params.get('Dots', False) - color1 = params.pop('Color1', Colors[0]) - color2 = params.pop('Color2', Colors[1]) - color3 = params.pop('Color3', Colors[2]) - single_ph_line_style = params.get('SinglePhLineStyle', 1) - three_ph_line_style = params.get('ThreePhLineStyle', 1) - max_lw = params.get('MaxLineThickness', 5) - bus_markers = params.get('BusMarkers', []) - do_labels = params.get('Labels', False) +def dss_circuit_plot(DSS: IDSS, + *, + fig=None, + ax=None, + is3d=False, + Quantity: str = None, + Dots: bool = False, + Color1: str = None, + Color2: str = None, + Color3: str = None, + SinglePhLineStyle: int = None, + ThreePhLineStyle: int = None, + MaxLineThickness: float = None, + BusMarkers: List[BusMarker] = None, + Labels: bool = None, + Markers: ObjMarkers = None, + MaxScale: float = None, + MaxScaleIsSpecified: bool = None, + **kwargs: Unpack[PlotParams] +): + if not MaxScaleIsSpecified: + MaxScale = None + + quantity = str_to_pq.get(Quantity, pqNone) + dots = Dots + color1 = Color1 + color2 = Color2 + color3 = Color3 + single_ph_line_style = SinglePhLineStyle + three_ph_line_style = ThreePhLineStyle + max_lw = MaxLineThickness + bus_markers = BusMarkers or [] + do_labels = Labels norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu @@ -917,10 +1169,7 @@ def dss_circuit_plot(DSS: IDSS, params={}, fig=None, ax=None, is3d=False): switch_idxs = set(switch_idxs) isolated_idxs = set(isolated_idxs) #lc_lines = LineCollection(lines_lines, linewidths=0.5, color=color1)# + 3 * lines_values / np.max(lines_values), linestyle='solid', color=color1) - try: - quantity_max_value = params.pop('MaxScale') - except: - quantity_max_value = 0 + quantity_max_value = MaxScale if MaxScale is not None else 0.0 quantity_suffix = '' @@ -1068,7 +1317,7 @@ def dss_circuit_plot(DSS: IDSS, params={}, fig=None, ax=None, is3d=False): ('MarkStorage', 'StoreMarkerCode', 'StoreMarkerSize', 'Storage', None), ] - pmarkers = params.pop('Markers', None) + pmarkers = Markers if pmarkers is not None: for (mark_opt, code_opt, size_opt, objs, idxs) in branch_marker_options: # print(mark_opt, pmarkers[mark_opt]) @@ -1153,7 +1402,9 @@ def dss_circuit_plot(DSS: IDSS, params={}, fig=None, ax=None, is3d=False): -def dss_scatter_plot(DSS, params): +def dss_scatter_plot(DSS: IDSS, + **kwargs: Unpack[PlotParams] +): x = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) y = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) vcomplex = np.empty(shape=(DSS.ActiveCircuit.NumBuses, 3), dtype=complex) @@ -1176,7 +1427,7 @@ def dss_scatter_plot(DSS, params): if include_3d in ('both', '2d'): fig, ax = plt.subplots(1, 1, constrained_layout=True)#, figsize=(8, 7)) - dss_circuit_plot(DSS, fig=fig, ax=ax, params={}) + dss_circuit_plot(DSS, fig=fig, ax=ax) ax.get_xaxis().get_major_formatter().set_scientific(False) ax.get_yaxis().get_major_formatter().set_scientific(False) sc = ax.scatter(x, y, c=vmean) @@ -1191,7 +1442,7 @@ def dss_scatter_plot(DSS, params): fig = plt.figure()#figsize=(7, 7)) ax = fig.add_subplot(projection='3d') - dss_circuit_plot(DSS, fig=fig, ax=ax, params={}, is3d=True) + dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True) ax.get_xaxis().get_major_formatter().set_scientific(False) ax.get_yaxis().get_major_formatter().set_scientific(False) @@ -1227,10 +1478,16 @@ def dss_scatter_plot(DSS, params): ax.set_title('{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude')) -def dss_visualize_plot(DSS, params): +def dss_visualize_plot(DSS: IDSS, + *, + Quantity: str = None, + ElementType: str = None, + ElementName: str = None, + **kwargs: Unpack[PlotParams] +): XMAX = 300 - #pprint(params) - quantity = params['Quantity'] + #pprint(kwargs) + quantity = Quantity # Fix for backend v0.13.1 quantity = { @@ -1240,13 +1497,13 @@ def dss_visualize_plot(DSS, params): }.get(quantity, quantity) element = DSS.ActiveCircuit.ActiveCktElement - etype, ename = params['ElementType'], params['ElementName'] + etype, ename = ElementType, ElementName nconds = element.NumConductors # nphases = element.NumPhases buses = element.BusNames[:2] # max 2 terminals vbases = [max(1, 1000 * DSS.ActiveCircuit.Buses[nodot(b)].kVBase) for b in buses] - # assert DSS.ActiveCircuit.ActiveCktElement.Name == params['ElementType'] + '.' + params['ElementName'] + # assert DSS.ActiveCircuit.ActiveCktElement.Name == ElementType + '.' + ElementName fig, ax = plt.subplots(1, gridspec_kw=dict(left=0.05, right=0.95, bottom=0.05, top=0.92))#, figsize=(8.6, 7)) ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) @@ -1348,14 +1605,33 @@ def get_text(): ax.set_ylim(-15, y + 5) -def dss_general_data_plot(DSS, params): - is_general = params['PlotType'] == 'GeneralData' - ValueIndex = max(1, params['ValueIndex'] - 1) - fn = params['ObjectName'] - MaxScaleIsSpecified = params['MaxScaleIsSpecified'] - MinScaleIsSpecified = params['MinScaleIsSpecified'] - MaxScale = params['MaxScale'] - MinScale = params['MinScale'] +def dss_general_data_plot(DSS: IDSS, + *, + PlotType: str = None, + ObjectName: str = None, + ValueIndex: int = None, + Color1: str = None, + Color2: str = None, + Labels: bool = None, + MinScaleIsSpecified: bool = None, + MaxScaleIsSpecified: bool = None, + MinScale: float = None, + MaxScale: float = None, + + **kwargs: Unpack[PlotParams] +): + if not MaxScaleIsSpecified: + MaxScale = None + + if not MinScaleIsSpecified: + MinScale = None + + is_general = PlotType == 'GeneralData' + ValueIndex = max(1, ValueIndex - 1) + fn = ObjectName + do_labels = Labels + color1 = Color1 + color2 = Color2 # Whenever we add Pandas as a dependency, this could be # rewritten to avoid all the extra/slow work @@ -1402,10 +1678,9 @@ def dss_general_data_plot(DSS, params): bus: IBus = DSS.ActiveCircuit.ActiveBus data = [] labels = [] - do_labels = params['Labels'] colors = [] - c1 = np.asarray(matplotlib.colors.colorConverter.to_rgb(params['Color1'])) - c2 = np.asarray(matplotlib.colors.colorConverter.to_rgb(params['Color2'])) + c1 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color1)) + c2 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color2)) for i in sidxs: name, val = names[i], vals[i] if DSS.ActiveCircuit.SetActiveBus(name) <= 0 or not bus.Coorddefined: @@ -1426,7 +1701,7 @@ def dss_general_data_plot(DSS, params): data = np.asarray(data) - dss_circuit_plot(DSS, params) + dss_circuit_plot(DSS, **kwargs) #fig = plt.figure(figsize=(8, 7)) plt.title(f'{field}, Max={max_val:.3g}') @@ -1442,8 +1717,6 @@ def dss_general_data_plot(DSS, params): #ax.get_yaxis().get_major_formatter().set_scientific(False) #plt.tight_layout() - - # marker_code = MarkerIdx # NodeMarkerWidth: int @@ -1456,9 +1729,14 @@ def dss_general_data_plot(DSS, params): #MarkSpecialClasses -def dss_matrix_plot(DSS, params): - # plot_id = params.get('PlotId', None) - if params['MatrixType'] == 'IncMatrix': +def dss_matrix_plot(DSS: IDSS, + *, + MatrixType: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] +): + # plot_id = kwargs.get('PlotId', None) + if MatrixType == 'IncMatrix': title = 'Incidence matrix' data = DSS.ActiveCircuit.Solution.IncMatrix[:-1] else: @@ -1473,7 +1751,7 @@ def dss_matrix_plot(DSS, params): fig = plt.figure(constrained_layout=True)#, num=plot_id) #, figsize=(8.6, 8.6)) ax = fig.add_subplot(1, 1, 1) ax.grid(True) - ax.spy(m, marker='s', markersize=1, color=params['Color1']) + ax.spy(m, marker='s', markersize=1, color=Color1) ax.set_xlabel('Column') ax.set_ylabel('Row') ax.set_title(title) @@ -1487,17 +1765,24 @@ def dss_matrix_plot(DSS, params): ax2.set_zlabel('Value') -def dss_daisy_plot(DSS, params): - dss_circuit_plot(DSS, params) +def dss_daisy_plot(DSS: IDSS, + *, + DaisyBusList: List[str] = None, + Quantity: str = None, + Labels: bool = None, + DaisySize: float = None, + **kwargs: Unpack[PlotParams] +): + dss_circuit_plot(DSS, **kwargs) # print(params['DaisySize']) ax = plt.gca() XMIN, XMAX = ax.get_xlim() - quantity = str_to_pq.get(params.get('Quantity', None), pqNone) - daisy_bus_list = params['DaisyBusList'] - do_labels = params['Labels'] - daisy_size = params['DaisySize'] + quantity = str_to_pq.get(Quantity, pqNone) + daisy_bus_list = DaisyBusList + do_labels = Labels + daisy_size = DaisySize ax.set_title(f'Device Locations / {quantity_str[quantity]}') element = DSS.ActiveCircuit.ActiveCktElement @@ -1555,9 +1840,17 @@ def unquote(field: str): return field -def dss_di_plot(DSS: IDSS, params): - caseYear, caseName, meterName = params['CaseYear'], params['CaseName'], params['MeterName'] - plotRegisters, peakDay = params['Registers'], params['PeakDay'] +def dss_di_plot(DSS: IDSS, + *, + CaseName: str = None, + MeterName: str = None, + Registers: List[int] = None, + CaseYear: str = None, + PeakDay: bool = None, + **kwargs: Unpack[PlotParams] +): + caseYear, caseName, meterName = CaseYear, CaseName, MeterName + plotRegisters, peakDay = Registers, PeakDay fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', meterName + '.csv') @@ -1679,14 +1972,19 @@ def _plot_yearly_case(DSS: IDSS, caseName: str, meterName: str, plotRegisters: L return icolor -def dss_yearly_curve_plot(DSS: IDSS, params): - caseNames, meterName, plotRegisters = params['CaseNames'], params['MeterName'], params['Registers'] +def dss_yearly_curve_plot(DSS: IDSS, *, + MeterName: str = None, + CaseNames: List[str] = None, + Registers: List[str] = None, + **kwargs: Unpack[PlotParams] +): + caseNames, meterName, plotRegisters = CaseNames, MeterName, Registers fig, ax = plt.subplots(1) icolor = 0 registerNames = [] for caseName in caseNames: - icolor = _plot_yearly_case(DSS, caseName, meterName, plotRegisters, icolor, ax, registerNames) + icolor = _plot_yearly_case(DSS, caseName, MeterName, plotRegisters, icolor, ax, registerNames) if icolor == 0: plt.close(fig) @@ -1700,24 +1998,39 @@ def dss_yearly_curve_plot(DSS: IDSS, params): ax.grid() -def dss_comparecases_plot(DSS: IDSS, params): - print('TODO: dss_comparecases_plot', params) - -def dss_zone_plot(DSS: IDSS, params): - obj_name = params['ObjectName'] - show_loops = params['ShowLoops'] - color1 = params['Color1'] - color3 = params['Color3'] - single_ph_line_style = LINES_STYLE_CODE.get(params.get('SinglePhLineStyle', 1)) - three_ph_line_style = LINES_STYLE_CODE.get(params.get('ThreePhLineStyle', 1)) - dots = params.get('Dots', False) - do_labels = params['Labels'] - quantity = str_to_pq.get(params.get('Quantity', None), pqNone) - max_lw = params.get('MaxLineThickness', 5) - - try: - quantity_max_value = params.pop('MaxScale') - except: +def dss_comparecases_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): + print('TODO: dss_comparecases_plot', kwargs) + + +def dss_zone_plot(DSS: IDSS, + *, + ObjectName: str, + Quantity: DSSPlotQuantity = DEFAULT_PLOT_PARAMS['Quantity'], + ShowLoops: bool = DEFAULT_PLOT_PARAMS['ShowLoops'], + Dots: bool = DEFAULT_PLOT_PARAMS['Dots'], + Labels: bool = DEFAULT_PLOT_PARAMS['Labels'], + Color1: str = DEFAULT_PLOT_PARAMS['Color1'], + Color3: str = DEFAULT_PLOT_PARAMS['Color3'], + SinglePhLineStyle: int = DEFAULT_PLOT_PARAMS['SinglePhLineStyle'], + ThreePhLineStyle: int = DEFAULT_PLOT_PARAMS['ThreePhLineStyle'], + MaxLineThickness: float = DEFAULT_PLOT_PARAMS['MaxLineThickness'], + MaxScale: float = DEFAULT_PLOT_PARAMS['MaxScale'], + **kwargs: Unpack[PlotParams] +): + obj_name = ObjectName + show_loops = ShowLoops + color1 = Color1 + color3 = Color3 + single_ph_line_style = LINES_STYLE_CODE.get(SinglePhLineStyle) + three_ph_line_style = LINES_STYLE_CODE.get(ThreePhLineStyle) + dots = Dots + do_labels = Labels + quantity = str_to_pq.get(Quantity, pqNone) + max_lw = MaxLineThickness + + if MaxScale is not None: + quantity_max_value = MaxScale + else: quantity_max_value = 0 @@ -1904,20 +2217,20 @@ def _add_line(element, color): 'MeterZones': dss_zone_plot } -def dss_plot(DSS, params): +def dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): try: - ptype = params['PlotType'] + ptype = kwargs['PlotType'] if ptype not in dss_plot_funcs: raise NotImplementedError(f'ERROR: not implemented plot type "{ptype}"') return -1 with ToggleAdvancedTypes(DSS, False), warnings.catch_warnings(): warnings.simplefilter("ignore") - dss_plot_funcs.get(ptype)(DSS, params) + dss_plot_funcs.get(ptype)(DSS, **kwargs) except Exception as ex: from traceback import format_exc - # print('DSS: Error while plotting. Parameters:', params, file=sys.stderr) + # print('DSS: Error while plotting. Parameters:', kwargs, file=sys.stderr) DSS._errorPtr[0] = 777 DSS._lib.Error_Set_Description(f"Error in the plot backend: {ex}\n{format_exc()}".encode()) return 777 @@ -2002,7 +2315,7 @@ def dss_python_cb_plot(ctx, paramsStr): result = 0 try: DSS = IDSS._get_instance(ctx=ctx) - result = dss_plot(DSS, params) + result = dss_plot(DSS, **params) if _do_show: plt.show() except: From 2e78f675b69c42244512cc3daa55c439ceb36ff8 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 17 Aug 2024 01:27:17 -0300 Subject: [PATCH 17/82] patch_dss_com: Adjust iteration for `Buses` --- dss/patch_dss_com.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dss/patch_dss_com.py b/dss/patch_dss_com.py index 87ee2ba1..89d6921e 100644 --- a/dss/patch_dss_com.py +++ b/dss/patch_dss_com.py @@ -77,7 +77,7 @@ def Load_Set_Phases(self, value): def custom_bus_iter(self): for i in range(obj.ActiveCircuit.NumBuses): obj.ActiveCircuit.SetActiveBusi(i) - yield self + yield obj.ActiveCircuit.ActiveBus def custom_bus_len(self): return obj.ActiveCircuit.NumBuses From 66137a2a0d2eefee55f48005ff6359dd80ed2178 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 17 Aug 2024 01:48:58 -0300 Subject: [PATCH 18/82] patch_dss_com: Tentative IsSwitch impl. --- dss/patch_dss_com.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/dss/patch_dss_com.py b/dss/patch_dss_com.py index 89d6921e..7f9d09c2 100644 --- a/dss/patch_dss_com.py +++ b/dss/patch_dss_com.py @@ -92,6 +92,27 @@ def custom_dss_call(self, cmds): self.Text.Command = cmds + def Lines_Get_IsSwitch(self): + lines = obj.ActiveCircuit.Lines + elem = obj.ActiveCircuit.ActiveCktElement + name = lines.Name + if not name: + return False + + obj.ActiveCircuit.SetActiveElement(f'Line.{name}') + return elem.Properties['Switch'].Val.lower() in ('y', 't') + + def Lines_Set_IsSwitch(self, Value): + lines = obj.ActiveCircuit.Lines + elem = obj.ActiveCircuit.ActiveCktElement + name = lines.Name + if not name: + return + + obj.ActiveCircuit.SetActiveElement(f'Line.{name}') + elem.Properties['Switch'].Val = 'y' if Value else 'n' + + # Callable DSS type(obj).__call__ = custom_dss_call @@ -100,7 +121,10 @@ def custom_dss_call(self, cmds): # Load Phases type(obj.ActiveCircuit.Loads).Phases = property(Load_Phases, Load_Set_Phases) - + + # Line IsSwitch + type(obj.ActiveCircuit.Lines).IsSwitch = property(Lines_Get_IsSwitch, Lines_Set_IsSwitch) + # Bus iterator and len type(obj.ActiveCircuit.ActiveBus).__iter__ = custom_bus_iter type(obj.ActiveCircuit.ActiveBus).__len__ = custom_bus_len From 92ec52cb9c1b1d732f0b870f946254f7bd5dada2 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 17 Aug 2024 01:19:49 -0300 Subject: [PATCH 19/82] Plot: refactored, adding COM/Oddie compat, DSV parser, some minor fixes. --- dss/plot.py | 374 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 345 insertions(+), 29 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index f7f5a8d4..27f7c422 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -10,14 +10,17 @@ from typing import List, TYPE_CHECKING, Optional, Tuple, Dict from typing_extensions import TypedDict, Unpack from . import api_util -from . import DSS as DSSPrime +from . import DSS as DSSPlotCtx from ._cffi_api_util import CffiApiUtil from .IDSS import IDSS from .IBus import IBus from ._cffi_api_util import Iterable as DSSIterable from enum import Enum, IntEnum +import numpy as np +from numpy import asarray +from numpy.testing import suppress_warnings +from pathlib import Path as FilePath try: - import numpy as np from matplotlib import pyplot as plt from matplotlib.path import Path from matplotlib.collections import LineCollection @@ -248,9 +251,17 @@ def show(text): @register_cell_magic def dss(line, cell): - DSSPrime.Text.Commands(cell) - - DSSPrime.AllowChangeDir = False + if isinstance(DSSPlotCtx, IDSS): + DSSPlotCtx.Text.Commands(cell) + else: + for line in cell.split('\n'): + DSSPlotCtx(line) + res = DSSPlotCtx.Text.Result + if res.endswith('.DSV'): + if _enabled and FilePath(res).exists(): + plot_dsv(res) + + DSSPlotCtx.AllowChangeDir = False except: def link_file(fn): print(f'Output file: "{fn}"') @@ -431,7 +442,7 @@ def dss_monitor_plot(DSS: IDSS, raise IndexError("No valid channel numbers were specified.") bases = Bases - header = monitor.Header + header = list(monitor.Header) if len(monitor.dblHour) < len(monitor.dblFreq): header.insert(0, 'Frequency') header.insert(1, 'Harmonic') @@ -572,9 +583,9 @@ def dss_loadshape_plot(DSS: IDSS, ls = DSS.ActiveCircuit.LoadShapes ls.Name = ObjectName - h = ls.TimeArray - p = ls.Pmult - q = ls.Qmult + h = asarray(ls.TimeArray) + p = asarray(ls.Pmult) + q = asarray(ls.Qmult) fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"LoadShape.{ObjectName}") @@ -648,7 +659,7 @@ def get_branch_data(DSS: IDSS, has_is_isolated = True except: has_is_isolated = False - isolated_names = set(name.lower() for name in DSS.ActiveCircuit.Topology.AllIsolatedBranches) + isolated_names = set(name.lower() for name in DSS.ActiveCircuit.Topology.AllIsolatedBranches if name) extra = [switch_idxs, isolated_idxs] else: @@ -667,12 +678,33 @@ def get_branch_data(DSS: IDSS, vbs = None if do_values == pqCurrent: + # Currently the same as pqCapacity to match the OpenDSS impl.; the correct would be: #max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllMaxCurrents(True))) - max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + try: + max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + except: + max_currents = {} + elem = DSS.ActiveCircuit.ActiveCktElement + for _ in DSS.ActiveCircuit.PDElements: + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + norm_amps = elem.NormalAmps + max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + elif do_values == pqCapacity: - capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + try: + capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + except: + max_currents = {} + elem = DSS.ActiveCircuit.ActiveCktElement + for _ in DSS.ActiveCircuit.PDElements: + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + norm_amps = elem.NormalAmps + max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + elif do_values == pqVoltage: - node_volts = dict(zip(DSS.ActiveCircuit.AllNodeNames, DSS.ActiveCircuit.AllBusVmag * 1e-3)) + node_volts = dict(zip(DSS.ActiveCircuit.AllNodeNames, asarray(DSS.ActiveCircuit.AllBusVmag) * 1e-3)) vbs = np.empty(shape=(line_count, ), dtype=np.float64) vbs.fill(0) extra.append(vbs) @@ -874,8 +906,8 @@ def dss_profile_plot(DSS: IDSS, busnode_to_index = {(bn.rsplit('.', 1)[0], int(bn.rsplit('.', 1)[1])): num for (num, bn) in enumerate(DSS.ActiveCircuit.AllNodeNames)} bus_to_kvbase = {b.Name: b.kVBase for b in DSS.ActiveCircuit.Buses} - puV = DSS.ActiveCircuit.AllBusVmagPu / DenomLN - distances = {name: d for (name, d) in zip(DSS.ActiveCircuit.AllBusNames, DSS.ActiveCircuit.AllBusDistances * LenScale)} + puV = asarray(DSS.ActiveCircuit.AllBusVmagPu) / DenomLN + distances = {name: d for (name, d) in zip(DSS.ActiveCircuit.AllBusNames, asarray(DSS.ActiveCircuit.AllBusDistances) * LenScale)} linewidths = [] segments = [] colors = [] @@ -1084,8 +1116,8 @@ def get_gic_line_data(DSS: IDSS, lines[offset, 1] = to lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style - currents = np.abs(np.asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(current[:elem.NumConductors]) + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) values[offset] = max_current offset += 1 @@ -1417,17 +1449,18 @@ def dss_scatter_plot(DSS: IDSS, x[idx] = b.x y[idx] = b.y - vnodes = b.puVoltages.view(dtype=complex) + vnodes = asarray(b.puVoltages).view(dtype=complex) nnodes = min(3, len(vnodes)) vcomplex[idx, :nnodes] = vnodes[:nnodes] vabs = np.abs(vcomplex) del vcomplex - vmean = np.mean(vabs, axis=1, where=np.isfinite(vabs)) + with suppress_warnings(): + vmean = np.mean(vabs, axis=1, where=np.isfinite(vabs)) if include_3d in ('both', '2d'): fig, ax = plt.subplots(1, 1, constrained_layout=True)#, figsize=(8, 7)) - dss_circuit_plot(DSS, fig=fig, ax=ax) + dss_circuit_plot(DSS, fig=fig, ax=ax, Color1='k') ax.get_xaxis().get_major_formatter().set_scientific(False) ax.get_yaxis().get_major_formatter().set_scientific(False) sc = ax.scatter(x, y, c=vmean) @@ -1442,7 +1475,7 @@ def dss_scatter_plot(DSS: IDSS, fig = plt.figure()#figsize=(7, 7)) ax = fig.add_subplot(projection='3d') - dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True) + dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True, Color1='k') ax.get_xaxis().get_major_formatter().set_scientific(False) ax.get_yaxis().get_major_formatter().set_scientific(False) @@ -1527,13 +1560,13 @@ def dss_visualize_plot(DSS: IDSS, voltage = (quantity == 'Voltages') if quantity == 'Powers': - values = 1e-3 * (element.Voltages.view(dtype=complex) * np.conj(element.Currents.view(dtype=complex))) + values = 1e-3 * (asarray(element.Voltages).view(dtype=complex) * np.conj(asarray(element.Currents).view(dtype=complex))) unit = 'kVA' elif voltage: - values = element.Voltages.view(dtype=complex) + values = asarray(element.Voltages).view(dtype=complex) unit = 'pu' elif quantity == 'Currents': - values = element.Currents.view(dtype=complex) + values = asarray(element.Currents).view(dtype=complex) unit = 'A' ax.set_title(f'{etype}.{ename.upper()} {quantity} ({unit})') @@ -2326,8 +2359,9 @@ def dss_python_cb_plot(ctx, paramsStr): _original_allow_forms = None _do_show = True +_enabled = False -def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True): +def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True, ctx: IDSS = None): """ Enables the plotting subsystem from DSS-Extensions. @@ -2344,8 +2378,14 @@ def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True): global include_3d global _original_allow_forms global _do_show + global _enabled + global DSSPlotCtx + + if ctx is not None: + DSSPlotCtx = ctx _do_show = show + _enabled = True if plot3d and plot2d: include_3d = 'both' @@ -2356,14 +2396,290 @@ def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True): api_util.lib.DSS_RegisterPlotCallback(api_util.lib.dss_python_cb_plot) api_util.lib.DSS_RegisterMessageCallback(api_util.lib.dss_python_cb_write) - _original_allow_forms = DSSPrime.AllowForms - DSSPrime.AllowForms = True + _original_allow_forms = DSSPlotCtx.AllowForms + DSSPlotCtx.AllowForms = True def disable(): + global _enabled + _enabled = False api_util.lib.DSS_RegisterPlotCallback(api_util.ffi.NULL) api_util.lib.DSS_RegisterMessageCallback(api_util.ffi.NULL) if _original_allow_forms is not None: - DSSPrime.AllowForms = _original_allow_forms + DSSPlotCtx.AllowForms = _original_allow_forms + + + +DSV_LINE_STYLES = { + 0: 'solid', + 1: 'dashed', + 2: 'dotted', + 3: 'dashdot', + 4: (0, (3, 5, 1, 5, 1, 5)), +} + +def _int_to_color(v: int): + return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) + +from matplotlib import pyplot as plt +import matplotlib.patches as patches +from numpy import asarray +import numpy as np +from dss.plot import get_marker_dict +import re + +DSV_LINE_STYLES = { + 0: 'solid', + 1: 'dashed', + 2: 'dotted', + 3: 'dashdot', + 4: (0, (3, 5, 1, 5, 1, 5)), +} + +def _int_to_color(v: int): + return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) + +class DSVHandler: + def __init__(self): + self.fig, self.ax = plt.subplots() + self.xy = [0.0, 0.0] + self.line_width = 1 + self.fig_caption = None + self.color = 'k' + self.key_class = None + self.no_scales = False + self.bold = True + self.txt_align = 'left' + + + def BoldLabel(self, param_str: str): + self.bold = int(param_str.strip()) != 0 + + + def Caption(self, param_str: str): + self.fig_caption = param_str.strip().strip('"') + self.fig.canvas.manager.set_window_title(self.fig_caption) + + + def ChartCaption(self, param_str: str): + self.ax.set_title(param_str.strip().strip('"')) + + + def Center(self, param_str: str): + *int_params, text = param_str.split(',') + x, y, s = [int(v.strip()) for v in int_params] + text = text.strip().strip('"') + if '/_' in text: + text = text.replace('/_', '∠') + '°' + + if '->' in text: + text = text.replace('->', '→') + s = s * 1.5 + elif '<-' in text: + text = text.replace('<-', '←') + s = s * 1.5 + elif '^' in text: + text = text.replace('^', '↑') + s = s * 1.5 + + self.ax.text(x, y, text, horizontalalignment='center', fontsize=s * 8 / 13.) + + + def Circle(self, param_str: str): + params = param_str.split(',') + x, y = int(params[0]), int(params[1]) + fc = _int_to_color(int(params[4])) + ec = _int_to_color(int(params[3])) + self.ax.scatter(x, y, marker='o', color=fc, edgecolors=ec, s=50, zorder=10, linewidths=0.5) + + + def ClickOn(self, param_str: str): + #TODO + pass + + + def Curve(self, param_str: str): + *int_params, curve_name, rest = param_str.split(',', 7) + npts, color, width, style, curve_markers, curve_marker = [int(v.strip()) for v in int_params] + if curve_markers: + marker_dict = get_marker_dict(curve_marker) + else: + marker_dict = {} + + data = np.fromstring(rest, dtype=float, sep=',') + self.ax.plot(data[:npts], data[npts:], lw=width/2.0, label=curve_name.strip().strip('"'), color=_int_to_color(color), ls=DSV_LINE_STYLES[style], **marker_dict) + # self.ax.minorticks_on() + + + def DataColor(self, param_str: str): + self.color = _int_to_color(int(param_str)) + + + def Draw(self, param_str: str): + if not self.no_scales: + # Currently not used since Move/Draw is emulated with axhline + return + + x0, y0 = self.xy + x1, y1 = [float(v.strip().strip('"')) for v in param_str.split(',')] + self.ax.plot([x0, x1], [y0, y1], color=self.color, lw=self.line_width/2.0) + + + def FStyle(self, param_str: str): + fstyle = int(param_str.strip().strip('"')) + # if fstyle != 0: + # print('Unhandled font style:', fstyle) + + + def KeepAspect(self, param_str: str): + try: + v = int(param_str.strip().strip('"')) + except: + v = 1 + + if v: + self.ax.set_aspect('equal') + else: + self.ax.set_aspect('auto') + + + def KeyClass(self, param_str: str): + self.key_class = int(param_str.strip()) + + + def Label(self, param_str: str): + *int_params, text, _ = param_str.split(',') + x, y, color_int = [int(v.strip()) for v in int_params] + color = _int_to_color(color_int) + text = text.strip().strip('"') + self.ax.text(x, y, text, + horizontalalignment='center', + fontsize=10 * 8 / 13., + color=color, + backgroundcolor='white', + weight='bold' if self.bold else 'normal' + ) + + + def Line(self, param_str: str): + + #TODO: use LineCollection + + *str_params, rest = param_str.split(',', 3) + line_name, bus1, bus2 = [v.strip().strip('"') for v in str_params] + *int_params, rest = rest.split(',', 4) + offset, data_count, num_cust, total_cust = [int(v) for v in int_params] + *dbl_params, rest = rest.split(',', 6) + kv, dist, x1, y1, x2, y2 = [float(v) for v in dbl_params] + int_params = rest.split(',') + #TODO: markers + color, width, style, dots, mark_center, center_marker_code, node_marker_code, node_marker_size = [int(v) for v in int_params] + + if dots: + node_marker_dict = get_marker_dict(node_marker_code) + node_marker_dict['markersize'] *= max(1, np.sqrt(node_marker_size) - 1) * node_marker_dict['markersize'] / 7.0 + else: + node_marker_dict = {} + + self.ax.plot([x1, x2], [y1, y2], color=_int_to_color(color), lw=width / 2.0, ls=DSV_LINE_STYLES[style], solid_capstyle='round', **node_marker_dict) + + if mark_center: + center_marker_dict = get_marker_dict(center_marker_code) + self.ax.scatter((x1 + x2) / 2, (y1 + y2) / 2, color=_int_to_color(color), **center_marker_dict) + + def Marker(self, param_str: str): + params = param_str.split(',') + x, y = float(params[0]), float(params[1]) + c, symbol, marker_size = [int(v) for v in params[2:]] + marker_dict = get_marker_dict(symbol) + marker_dict['markersize'] *= max(1, np.sqrt(marker_size) - 1) * marker_dict['markersize'] / 7.0 + self.ax.plot(x, y, ls=None, color=_int_to_color(c), **marker_dict) + + + def Move(self, param_str: str): + x, y = [float(v.strip().strip('"')) for v in param_str.split(',')] + if self.no_scales: + self.xy = [x, y] + else: + self.ax.axhline(y, color=self.color, lw=self.line_width / 2.0) + + + def NoScales(self, param_str: str): + self.no_scales = True + self.ax.get_xaxis().set_visible(False) + self.ax.get_yaxis().set_visible(False) + + + def PctRim(self, param_str: str): + self.ax.margins(float(param_str) / 100.0) + + + def Range(self, param_str: str): + pass + + + def Rect(self, param_str: str): + left, bottom, right, top = [int(v) for v in param_str.split(',')] + r = patches.Rectangle((left, bottom), right - left, top - bottom, fill=True, ec='k', fc='#c0c0c0') + self.ax.add_patch(r) -__all__ = ['enable', 'disable'] + def SetProp(self, param_str: str): + if int(param_str.rsplit(',', 1)[-1]) != 0: + self.ax.grid(which='both', ls='--') + else: + self.ax.grid(False) + + + def Text(self, param_str: str): + *int_params, text = param_str.split(',') + x, y, c, s = [int(v.strip()) for v in int_params] + text = text.strip().strip('"') + self.ax.text(x, y, text, ha=self.txt_align, va='center', fontsize=s * 10 / 13.) + + + def TxtAlign(self, param_str: str): + v = int(param_str) + if v == 1: + self.txt_align = 'left' + return + + if v == 2: + self.txt_align = 'center' + return + + if v == 3: + self.txt_align = 'right' + return + + + def Width(self, param_str: str): + self.line_width = int(param_str.strip().strip('"')) + + + def Xlabel(self, param_str: str): + self.ax.set_xlabel(param_str.strip().strip('"')) + + + def Ylabel(self, param_str: str): + self.ax.set_ylabel(param_str.strip().strip('"')) + + +def plot_dsv(fn: str): + handler = DSVHandler() + with open(fn, 'r') as f: + for l in f: + l = l.strip() + if not l: + continue + + item_name, *rest = l.split(',', 1) + item_name = item_name.strip() + # print(item, repr(rest)[:100]) + getattr(handler, item_name)(rest[0] if rest else '') # let the exception propagate on error + + if _do_show: + plt.show() + else: + return handler.fig, handler.ax + +__all__ = ['enable', 'disable', 'plot_dsv', ] From b1d5c81f20e8c1dc67ffbf007fc30cbf0e2aad18 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:58:21 -0300 Subject: [PATCH 20/82] Solution: with Oddie, append a 0 to IncMatrix and Laplacian (for compatiblity with COM) --- dss/ISolution.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/dss/ISolution.py b/dss/ISolution.py index 369704a1..af4cd408 100644 --- a/dss/ISolution.py +++ b/dss/ISolution.py @@ -5,6 +5,7 @@ from ._types import Int32Array from typing import Union, AnyStr, List from .enums import SolveModes, ControlModes, SolutionAlgorithms +import numpy as np class ISolution(Base): __slots__ = [] @@ -618,7 +619,16 @@ def IncMatrix(self) -> Int32Array: Original COM help: https://opendss.epri.com/IncMatrix.html ''' #TODO: expose as sparse matrix - return self._lib.Solution_Get_IncMatrix_GR() + result = self._lib.Solution_Get_IncMatrix_GR() + n = len(result) + if n >= 3 and (n % 3) == 0: # Compatibility with COM + if isinstance(result, np.ndarray): + result = np.resize(result, n + 1) + result[-1] = 0 + else: + result.append(0) + + return result @property def IncMatrixCols(self) -> List[str]: @@ -653,7 +663,16 @@ def Laplacian(self) -> Int32Array: Original COM help: https://opendss.epri.com/Laplacian.html ''' #TODO: expose as sparse matrix - return self._lib.Solution_Get_Laplacian_GR() + result = self._lib.Solution_Get_Laplacian_GR() + n = len(result) + if n >= 3 and (n % 3) == 0: # Compatibility with COM + if isinstance(result, np.ndarray): + result = np.resize(result, n + 1) + result[-1] = 0 + else: + result.append(0) + + return result def SolveAll(self): ''' From f126880617776ca62699da1ae9973c4a9134aabf Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:59:51 -0300 Subject: [PATCH 21/82] Plot: allow plots from Oddie; other minor changes. --- dss/plot.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index 27f7c422..95079913 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -251,7 +251,7 @@ def show(text): @register_cell_magic def dss(line, cell): - if isinstance(DSSPlotCtx, IDSS): + if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_odd: DSSPlotCtx.Text.Commands(cell) else: for line in cell.split('\n'): @@ -2441,6 +2441,8 @@ def _int_to_color(v: int): class DSVHandler: def __init__(self): self.fig, self.ax = plt.subplots() + self.ax.get_xaxis().get_major_formatter().set_scientific(False) + self.ax.get_yaxis().get_major_formatter().set_scientific(False) self.xy = [0.0, 0.0] self.line_width = 1 self.fig_caption = None @@ -2486,7 +2488,7 @@ def Center(self, param_str: str): def Circle(self, param_str: str): params = param_str.split(',') - x, y = int(params[0]), int(params[1]) + x, y = float(params[0]), float(params[1]) fc = _int_to_color(int(params[4])) ec = _int_to_color(int(params[3])) self.ax.scatter(x, y, marker='o', color=fc, edgecolors=ec, s=50, zorder=10, linewidths=0.5) @@ -2537,7 +2539,7 @@ def KeepAspect(self, param_str: str): v = 1 if v: - self.ax.set_aspect('equal') + self.ax.set_aspect('equal', 'datalim') else: self.ax.set_aspect('auto') From 4f677691f02302f6afd68027b23f554b7dbfede4 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:49:24 -0300 Subject: [PATCH 22/82] Update GR pointers for DSS C-API 0.15 --- dss/_cffi_api_util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 8bfc5f80..0cd41aba 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -768,7 +768,6 @@ def track_obj(self, obj): def init_buffers(self): lib = self.lib_unpatched - tmp_string_pointers = (self.ffi.new('char****'), self.ffi.new('int32_t**')) tmp_float64_pointers = (self.ffi.new('double***'), self.ffi.new('int32_t**')) tmp_int32_pointers = (self.ffi.new('int32_t***'), self.ffi.new('int32_t**')) tmp_int8_pointers = (self.ffi.new('int8_t***'), self.ffi.new('int32_t**')) @@ -776,13 +775,12 @@ def init_buffers(self): # reorder pointers so data pointers are first, count pointers last ptr_args = [ ptr - for ptrs in zip(tmp_string_pointers, tmp_float64_pointers, tmp_int32_pointers, tmp_int8_pointers) + for ptrs in zip(tmp_float64_pointers, tmp_int32_pointers, tmp_int8_pointers) for ptr in ptrs ] lib.ctx_DSS_GetGRPointers(self.ctx, *ptr_args) # we don't need to keep the extra indirections - self.gr_string_pointers = (tmp_string_pointers[0][0], tmp_string_pointers[1][0]) self.gr_float64_pointers = (tmp_float64_pointers[0][0], tmp_float64_pointers[1][0]) self.gr_int32_pointers = (tmp_int32_pointers[0][0], tmp_int32_pointers[1][0]) self.gr_int8_pointers = (tmp_int8_pointers[0][0], tmp_int8_pointers[1][0]) From 6fd8db0525f46c11fa9ccce86c7ace20cb62f87b Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:01:25 -0300 Subject: [PATCH 23/82] tests: also save data related to incidence and Laplacian matrices. --- tests/save_outputs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 330c8aaf..5441169b 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -114,6 +114,9 @@ def run(dss: dss.IDSS, fn: str, line_by_line: bool): ): dss.Text.Command = 'export profile phases=all' + dss.Text.Command = 'CalcIncMatrix' + dss.Text.Command = 'CalcLaplacian' + reliabity_ran = True try: dss.ActiveCircuit.Meters.DoReliabilityCalc(False) @@ -159,12 +162,16 @@ def adjust_to_json(cls, field): def export_dss_api_cls(dss: dss.IDSS, dss_cls): printv(dss_cls) + lname = type(dss_cls).__name__.lower() has_iter = hasattr(type(dss_cls), '__iter__') is_ckt_element = getattr(type(dss_cls), '_is_circuit_element', False) ckt_elem = dss.ActiveCircuit.ActiveCktElement ckt_elem_columns = set(type(ckt_elem)._columns) - ckt_elem_columns_meta - pc_elem_columns - {'Handle', 'IsIsolated', 'HasOCPDevice'} fields = list(type(dss_cls)._columns) + if lname.endswith('solution'): + fields.extend(['IncMatrix', 'Laplacian', 'IncMatrixCols', 'IncMatrixRows', ]) + if 'UserClasses' in fields: fields.remove('UserClasses') @@ -188,7 +195,7 @@ def export_dss_api_cls(dss: dss.IDSS, dss_cls): if 'AllPDEatBus' in fields: fields.remove('AllPDEatBus') # if 'Sensor' in fields: # Both Loads and PVSystems - if 'ipvsystems' in type(dss_cls).__name__.lower(): + if 'ipvsystems' in lname: fields.remove('Sensor') # if 'IrradianceNow' in fields: From 7d433c77d719591b0c41e5d197b2394c4b7bd775 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:30:55 -0300 Subject: [PATCH 24/82] Add missing wrapper for Error.Number --- dss/_cffi_api_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 0cd41aba..f6f33d7d 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -281,6 +281,7 @@ def __init__(self, api_util, settings_ptr): self._prepare_api_functions(done, settings_ptr) self.Error_Get_Description = lambda: self._get_string(lib.ctx_Error_Get_Description(ctx)) + self.Error_Get_Number = lambda: lib.ctx_Error_Get_Number(ctx) skip_funcs = {'ctx_New', 'ctx_Dispose', 'ctx_Get_Prime', 'ctx_Set_Prime', 'ctx_Error_Set_Description', 'ctx_Error_Get_NumberPtr', 'ctx_ZIP_Extract_GR'} # First, process all `ctx_*`` functions From 7085a7c59cffb8249759dce5c8357bd98fa635de Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:19:02 -0300 Subject: [PATCH 25/82] patch_dss_com: Fix IsSwitch --- dss/patch_dss_com.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dss/patch_dss_com.py b/dss/patch_dss_com.py index 7f9d09c2..fb96fcda 100644 --- a/dss/patch_dss_com.py +++ b/dss/patch_dss_com.py @@ -100,7 +100,7 @@ def Lines_Get_IsSwitch(self): return False obj.ActiveCircuit.SetActiveElement(f'Line.{name}') - return elem.Properties['Switch'].Val.lower() in ('y', 't') + return elem.Properties['Switch'].Val.lower()[:1] in ('y', 't') def Lines_Set_IsSwitch(self, Value): lines = obj.ActiveCircuit.Lines From 925987732c997ceb9c3936642d082a512b39f600 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:03:11 -0300 Subject: [PATCH 26/82] save_outputs: Run CIM files first --- tests/save_outputs.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 5441169b..21c7bbd7 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -405,11 +405,6 @@ def get_archive_fn(live_fn, fn_prefix=None): debug_suffix = '-debug' if 'debug' in DSS.Version.lower() else '' suffix = f'-dssx_oddd-{sys.platform}-{platform.machine()}-{oddd_ver}{debug_suffix}' - #test_idx = test_filenames.index('L!Version8/Distrib/IEEETestCases/123Bus/RevRegTest.dss') + 50 - # test_filenames = [fn for fn in test_filenames if 'DOCTechNote' not in fn] # DOC not implemented - # test_filenames = ['L!Version8/Distrib/IEEETestCases/123Bus/Run_YearlySim.dss'] - # test_filenames = ['L!Version8/Distrib/IEEETestCases/123Bus/SolarRamp.DSS'] - cimxml_test_filenames = [] # Cannot run these now DSS.AllowForms = False elif SAVE_DSSX_OUTPUT: @@ -455,7 +450,7 @@ def get_archive_fn(live_fn, fn_prefix=None): total_runtime = 0.0 zip_fn = f'results{suffix}.zip' with ZipFile(os.path.join(original_working_dir, zip_fn), mode='a', compression=ZIP_DEFLATED) as zip_out: - for fn in test_filenames + cimxml_test_filenames: + for fn in cimxml_test_filenames + test_filenames: if not fn.strip(): break From 6a64a6e7d1c4d29300224fd4f072bfd718fbac99 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:55:48 -0300 Subject: [PATCH 27/82] save_outputs: Always try to use WindGens and Storages (but allow failure) --- tests/save_outputs.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 21c7bbd7..b4807630 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -334,13 +334,26 @@ def save_state(dss: dss.IDSS, runtime: float = 0.0) -> str: 'XYCurves': dss.ActiveCircuit.XYCurves, } + try: + dss_classes.update({ + 'Storages': dss.ActiveCircuit.Storages, + }) + except AttributeError: + pass + + try: + dss_classes.update({ + 'WindGens': dss.ActiveCircuit.WindGens, + }) + except AttributeError: + pass + try: dss_classes.update({ 'CNData': dss.ActiveCircuit.CNData, 'LineGeometries': dss.ActiveCircuit.LineGeometries, 'LineSpacings': dss.ActiveCircuit.LineSpacings, 'Reactors': dss.ActiveCircuit.Reactors, - 'Storages': dss.ActiveCircuit.Storages, 'TSData': dss.ActiveCircuit.TSData, 'WireData': dss.ActiveCircuit.WireData, }) From 684d0dc0215f0c71a7151875b5b73a5af72bf573 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:26:37 -0300 Subject: [PATCH 28/82] patch_dss_com: Include WindGens and Storage --- dss/patch_dss_com.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/dss/patch_dss_com.py b/dss/patch_dss_com.py index fb96fcda..308d5c5a 100644 --- a/dss/patch_dss_com.py +++ b/dss/patch_dss_com.py @@ -31,7 +31,8 @@ from .ITransformers import ITransformers from .IXYCurves import IXYCurves from .IGICSources import IGICSources -# from .IStorages import IStorages +from .IStorages import IStorages +from .IWindGens import IWindGens def custom_iter(self): @@ -161,7 +162,8 @@ def add_dunders(cls): 'Transformers': ITransformers, 'XYCurves': IXYCurves, 'GICSources': IGICSources, - # 'Storages': IStorages, + 'Storages': IStorages, + 'WindGens': IWindGens, } def filter_cols(py_cls): @@ -194,7 +196,11 @@ def filter_cols(py_cls): type(obj.ActiveCircuit.Topology)._columns = filter_cols(ITopology) for name, py_cls in com_classes_to_dsspy.items(): - cls = type(getattr(obj.ActiveCircuit, name)) + instance = getattr(obj.ActiveCircuit, name, None) + if instance is None: + continue + + cls = type(instance) add_dunders(cls) cls._py_cls = py_cls # Filter columns, removing @@ -203,8 +209,6 @@ def filter_cols(py_cls): if getattr(py_cls, '_is_circuit_element', False): cls._is_circuit_element = True - add_dunders(cls) - return obj __all__ = ['patch_dss_com'] From b72779a2a7c5c1a40e665c40d242e57b456566a8 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 30 Aug 2024 20:30:10 -0300 Subject: [PATCH 29/82] Tests: add backwards compat --- tests/_settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/_settings.py b/tests/_settings.py index 89c658d3..77983221 100644 --- a/tests/_settings.py +++ b/tests/_settings.py @@ -3,7 +3,13 @@ import faulthandler faulthandler.disable() -from dss import DSS, IOddieDSS +from dss import DSS +DSS.COMErrorResults = False +try: + from dss import IOddieDSS +except: + pass + faulthandler.enable() org_dir = os.getcwd() From 683d67670966066af82835861de4124a30c13d29 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 31 Aug 2024 02:08:16 -0300 Subject: [PATCH 30/82] save_outputs: alternative iteration path for Storages and WindGens (to avoid infinite loops) --- tests/save_outputs.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/save_outputs.py b/tests/save_outputs.py index b4807630..1c1f07c5 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -223,6 +223,15 @@ def export_dss_api_cls(dss: dss.IDSS, dss_cls): else: items = [dss_cls] + if ((not SAVE_DSSX_OUTPUT) or SAVE_DSSX_OUTPUT_ODD) and lname in ('istorages', 'iwindgens'): + def iter_cls(): + for i in range(cls.Count): + cls.idx = i + yield cls + + items = iter_cls() + + for _ in items: record = {} for field in fields: From da7d52cbaa978a5b7e423b0dcc61b352eaff9799 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 31 Aug 2024 02:09:08 -0300 Subject: [PATCH 31/82] IDSS: initial ShareGeneral implementation; needs better docstrings, tests, examples, maybe better management. --- dss/IDSS.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dss/IDSS.py b/dss/IDSS.py index bf176064..f592447d 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -542,3 +542,20 @@ def CompatFlags(self) -> int: @CompatFlags.setter def CompatFlags(self, Value: int): self._lib.DSS_Set_CompatFlags(Value) + + + def ShareGeneral(self, otherContext: IDSS): + ''' + Share general DSS objects from this AltDSS context to another. + + **WARNING:** currently, the pointers are not tracked! The user must ensure this context + and its objects are kept alive while other contexts require it. + + ***EXPERIMENTAL*** + + **(API Extension)** + ''' + if self._api_util._is_odd or otherContext._api_util._is_odd: + raise ValueError("Only AltDSS engine contexts can share data.") + + self._lib.ShareGeneral(otherContext._api_util.ctx) \ No newline at end of file From 7d6fce887cdf22ef8c459c582e92162453ef8aaf Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 2 Sep 2024 01:37:40 -0300 Subject: [PATCH 32/82] Settings: expose the new `SkipCommands` and `SkipFileRegExp` settings, and add tests. --- dss/ISettings.py | 58 ++++++++++++++++++++++++++++++++++++++++++- tests/test_general.py | 32 ++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/dss/ISettings.py b/dss/ISettings.py index d0250a4b..d6176b0a 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -3,7 +3,7 @@ # Copyright (c) 2018-2024 DSS-Extensions contributors from ._cffi_api_util import Base from ._types import Float64Array, Int32Array -from typing import AnyStr, Union +from typing import AnyStr, Union, List from .enums import DSSPropertyNameStyle, CktModels class ISettings(Base): @@ -310,3 +310,59 @@ def SetPropertyNameStyle(self, value: DSSPropertyNameStyle): **(API Extension)** ''' self._lib.Settings_SetPropertyNameStyle(value) + + @property + def SkipFileRegExp(self) -> str: + ''' + Regular expression pattern to skip files + + If a file name as provided in the input for the `Redirect` and `Compile` commands + matches the regular expression pattern, it is skipped (the file is not read nor + commands from it are executed). + + Set to an empty string to reset/disable the filter. + + Case-insensitive. + See https://regex.sorokin.engineer/en/latest/regular_expressions.html for information on + the expression syntax and options. + + + **(API Extension)** + ''' + return self._lib.Settings_Get_SkipFileRegExp() + + @SkipFileRegExp.setter + def SkipFileRegExp(self, Value: Union[AnyStr, None]): + self._lib.Settings_Set_SkipFileRegExp(Value or '') + + @property + def SkipCommands(self) -> List[str]: + ''' + List of commands to skip + + List of strings representing the command names to skip when processing DSS text commands or files. + + **(API Extension)** + ''' + + return [ + self._lib.DSS_Executive_Get_Command(i) + for i in self._lib.Settings_Get_SkipCommands_GR() + ] + + @SkipCommands.setter + def SkipCommands(self, Value: List[str]): + if len(Value) != 0 and isinstance(Value[0], str): + # map command names to integer codes + num_commands = self._lib.DSS_Executive_Get_NumCommands() + command_dict = { + self._lib.DSS_Executive_Get_Command(i).lower(): i + for i in range(1, num_commands + 1) + } + Value = [command_dict[cmd_name] for cmd_name in Value] + + Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) + + self._lib.Settings_Set_SkipCommands(ValuePtr, ValueCount) + + diff --git a/tests/test_general.py b/tests/test_general.py index ce42fb6d..8070f3cf 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -931,6 +931,38 @@ def test_line_parent_compat(): assert res_compat[3:2:] == res_no_compat[3:2:] +def test_skip_commands(): + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + DSS.ActiveCircuit.Settings.SkipCommands = ['clear'] + # Since we are skipping the clear command, an exception should be raised + with pytest.raises(DSSException): + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + + DSS.ActiveCircuit.Settings.SkipCommands = [] + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + + +def test_skip_files(): + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + DSS.ActiveCircuit.Settings.SkipFileRegExp = r'.*LineCodes\.DSS' + print(repr(DSS.ActiveCircuit.Settings.SkipFileRegExp)) + + # This should fail since we won't have the LineCodes + with pytest.raises(DSSException): + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + + DSS.ActiveCircuit.Settings.SkipFileRegExp = '' + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + + DSS.ActiveCircuit.Settings.SkipFileRegExp = None + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + + DSS.ActiveCircuit.Settings.SkipFileRegExp = 'some random string just to test' + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + + DSS.ActiveCircuit.Settings.SkipFileRegExp = None + + def test_path_sideeffects(): test_loadshape_save() test_basic_input_errors() From 6d63041263797797e67bf534ca27d34fd7dec348 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:50:16 -0300 Subject: [PATCH 33/82] ShareGeneral and Settings: add shortcuts, update docstrings and tests. --- dss/IDSS.py | 16 ++++++++++++++-- dss/ISettings.py | 28 +++++++++++++++++++--------- tests/test_general.py | 8 ++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/dss/IDSS.py b/dss/IDSS.py index f592447d..4a06bb7d 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -544,13 +544,19 @@ def CompatFlags(self, Value: int): self._lib.DSS_Set_CompatFlags(Value) - def ShareGeneral(self, otherContext: IDSS): + def ShareGeneral(self, otherContext: IDSS, skip_cmds: Optional[List[str]] = None, skip_file_regexp: str = None): ''' Share general DSS objects from this AltDSS context to another. **WARNING:** currently, the pointers are not tracked! The user must ensure this context and its objects are kept alive while other contexts require it. + Optionally, as a shortcut, the user can provide `skip_cmds` to be passed to the `Settings.SkipCommands` + and `skip_file_regexp` to be passed to `Settings.SkipFileRegExp`, in the second DSS context. + + *Note*: If the `clear` command is included in `Settings.SkipCommands`, the `DSS.ClearAll()` method can still be called + and it will reset both skip settings. + ***EXPERIMENTAL*** **(API Extension)** @@ -558,4 +564,10 @@ def ShareGeneral(self, otherContext: IDSS): if self._api_util._is_odd or otherContext._api_util._is_odd: raise ValueError("Only AltDSS engine contexts can share data.") - self._lib.ShareGeneral(otherContext._api_util.ctx) \ No newline at end of file + self._lib.ShareGeneral(otherContext._api_util.ctx) + if skip_cmds is not None: + otherContext.ActiveCircuit.Settings.SkipCommands = skip_cmds + + if skip_file_regexp is not None: + otherContext.ActiveCircuit.Settings.SkipFileRegExp = skip_file_regexp + diff --git a/dss/ISettings.py b/dss/ISettings.py index d6176b0a..cb3ed2ba 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -7,7 +7,9 @@ from .enums import DSSPropertyNameStyle, CktModels class ISettings(Base): - __slots__ = [] + __slots__ = [ + '_command_dict' + ] _columns = [ 'Trapezoidal', @@ -31,6 +33,14 @@ class ISettings(Base): 'IterateDisabled', ] + def __init__(self, api_util): + Base.__init__(self, api_util) + num_commands = self._lib.DSS_Executive_Get_NumCommands() + self._command_dict = { + self._lib.DSS_Executive_Get_Command(i).lower(): i + for i in range(1, num_commands + 1) + } + @property def AllowDuplicates(self) -> bool: ''' @@ -314,11 +324,11 @@ def SetPropertyNameStyle(self, value: DSSPropertyNameStyle): @property def SkipFileRegExp(self) -> str: ''' - Regular expression pattern to skip files + Regular expression pattern to skip files. If a file name as provided in the input for the `Redirect` and `Compile` commands matches the regular expression pattern, it is skipped (the file is not read nor - commands from it are executed). + commands contained in the file are executed). Set to an empty string to reset/disable the filter. @@ -326,6 +336,8 @@ def SkipFileRegExp(self) -> str: See https://regex.sorokin.engineer/en/latest/regular_expressions.html for information on the expression syntax and options. + Even if the `clear` command is included in `Settings.SkipCommands`, the `DSS.ClearAll()` method can + still be called. It resets both skip settings, `SkipCommands` and `SkipFileRegExp`. **(API Extension)** ''' @@ -342,6 +354,9 @@ def SkipCommands(self) -> List[str]: List of strings representing the command names to skip when processing DSS text commands or files. + If the `clear` command is included in `Settings.SkipCommands`, the `DSS.ClearAll()` method can + still be called and it will reset both skip settings, `SkipCommands` and `SkipFileRegExp`. + **(API Extension)** ''' @@ -354,12 +369,7 @@ def SkipCommands(self) -> List[str]: def SkipCommands(self, Value: List[str]): if len(Value) != 0 and isinstance(Value[0], str): # map command names to integer codes - num_commands = self._lib.DSS_Executive_Get_NumCommands() - command_dict = { - self._lib.DSS_Executive_Get_Command(i).lower(): i - for i in range(1, num_commands + 1) - } - Value = [command_dict[cmd_name] for cmd_name in Value] + Value = [self._command_dict[cmd_name] for cmd_name in Value] Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) diff --git a/tests/test_general.py b/tests/test_general.py index 8070f3cf..b1d1b6b2 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -941,6 +941,14 @@ def test_skip_commands(): DSS.ActiveCircuit.Settings.SkipCommands = [] DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + DSS.ActiveCircuit.Settings.SkipCommands = ['clear'] + with pytest.raises(DSSException): + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + + DSS.ClearAll() + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + def test_skip_files(): DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' From 6bc0e21ce9156251d12821bd9f24a5df62b7183a Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:02:38 -0300 Subject: [PATCH 34/82] tests: run PM threads a bit longer to dilute overhead. Mostly for Oddie/official OpenDSS tests. --- tests/test_general.py | 80 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/tests/test_general.py b/tests/test_general.py index b1d1b6b2..21ae59a8 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -288,19 +288,19 @@ def test_pm_threads(): # Let's run 4 days in 4 actors Parallel.ActiveParallel = 1 DSS.Text.Command = 'set activeActor=*' - DSS.Text.Command = 'set mode=yearly number=144 hour=0 controlmode=off stepsize=600' + DSS.Text.Command = 'set mode=yearly number=432 hour=0 controlmode=off stepsize=600' DSS.Text.Command = 'set activeActor=1' DSS.Text.Command = 'set hour=0' DSS.Text.Command = 'set activeActor=2' - DSS.Text.Command = 'set hour=24' + DSS.Text.Command = 'set hour=72' DSS.Text.Command = 'set activeActor=3' - DSS.Text.Command = 'set hour=48' + DSS.Text.Command = 'set hour=144' DSS.Text.Command = 'set activeActor=4' - DSS.Text.Command = 'set hour=72' + DSS.Text.Command = 'set hour=216' DSS.ActiveCircuit.Solution.SolveAll() DSS.Text.Command = 'wait' @@ -328,7 +328,7 @@ def test_pm_threads(): t0 = perf_counter() DSS.Text.Command = f'compile "{fn}"' DSS.ActiveCircuit.Solution.Solve() - DSS.Text.Command = 'set mode=yearly number=144 hour=0 controlmode=off stepsize=600' + DSS.Text.Command = 'set mode=yearly number=432 hour=0 controlmode=off stepsize=600' DSS.ActiveCircuit.Solution.Solve() v_seq.append(DSS.ActiveCircuit.AllBusVolts) @@ -357,7 +357,7 @@ def test_pm_threads(): def _run(ctx, i): ctx.Text.Command = f'compile "{fn}"' ctx.ActiveCircuit.Solution.Solve() - ctx.Text.Command = f'set mode=yearly number=144 hour={i * 24} controlmode=off stepsize=600' + ctx.Text.Command = f'set mode=yearly number=432 hour={i * 24 * 3} controlmode=off stepsize=600' ctx.ActiveCircuit.Solution.Solve() v_ctx[i] = ctx.ActiveCircuit.AllBusVolts @@ -376,14 +376,13 @@ def _run(ctx, i): t1 = perf_counter() dt_ctx = t1 - t0 + + np.testing.assert_allclose(v_ctx[0], v_seq[0]) + np.testing.assert_allclose(v_ctx[1], v_seq[1]) + np.testing.assert_allclose(v_ctx[2], v_seq[2]) + np.testing.assert_allclose(v_ctx[3], v_seq[3]) else: dt_ctx = np.NaN - - np.testing.assert_allclose(v_ctx[0], v_seq[0]) - np.testing.assert_allclose(v_ctx[1], v_seq[1]) - np.testing.assert_allclose(v_ctx[2], v_seq[2]) - np.testing.assert_allclose(v_ctx[3], v_seq[3]) - print(f"PM: {dt_pm:.3g} s; Python threads: {dt_ctx:.3g} s; Sequential: {dt_seq:.3g} s") @@ -914,6 +913,57 @@ def test_loadshape_save(): npt.assert_allclose(pmult, pmult_sng) +def test_loadshape_extended(): + + # Added for OpenDSSC + + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + LS = DSS.ActiveCircuit.LoadShapes + LS.Name = "default" + assert LS.Npts == 24 + ref_p = np.asarray([.677, .6256, .6087, .5833, .58028, .6025, .657, .7477, .832, .88, .94, .989, .985, .98, .9898, .999, 1, .958, .936, .913, .876, .876, .828, .756]) + npt.assert_allclose(LS.Pmult, ref_p) + ref_t = range(len(ref_p)) + LS.TimeArray = ref_t + npt.assert_allclose(LS.TimeArray, ref_t) + ref_q = LS.TimeArray + ref_p + LS.Qmult = ref_q + npt.assert_allclose(LS.Qmult, ref_q) + + DSS.Text.Command = 'new loadshape.test npts=3 pmult=[1.1, 2.2, 3.3] qmult=[4.5, 4.6, 4.7] hour=[1, 2, 7]' + LS.Name = 'test' + assert LS.Npts == 3 + npt.assert_allclose(LS.Pmult, [1.1, 2.2, 3.3]) + npt.assert_allclose(LS.Qmult, [4.5, 4.6, 4.7]) + npt.assert_allclose(LS.TimeArray, [1, 2, 7]) + LS.Pmult *= 2 + npt.assert_allclose(LS.Pmult, [2.2, 4.4, 6.6]) + LS.Qmult /= 2.5 + npt.assert_allclose(LS.Qmult * 2.5, [4.5, 4.6, 4.7]) + LS.TimeArray *= 12 + npt.assert_allclose(LS.TimeArray / 12, [1, 2, 7]) + + +def test_xycurve_extended(): + + # Added for OpenDSSC + + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + LS = DSS.ActiveCircuit.XYCurves + + DSS.Text.Command = 'New XYCurve.test npts=4 xarray=[.1 .2 .4 1.0] yarray=[.86 .9 .93 .97]' + LS.Name = "test" + assert LS.Npts == 4 + ref_x = np.asarray([.1, .2, .4, 1.0]) + ref_y = np.asarray([.86, .9, .93, .97]) + npt.assert_allclose(LS.Xarray, ref_x) + npt.assert_allclose(LS.Yarray, ref_y) + LS.Xarray *= 2 + npt.assert_allclose(LS.Xarray, ref_x * 2) + LS.Yarray /= 2.5 + npt.assert_allclose(LS.Yarray, ref_y / 2.5) + + def test_line_parent_compat(): from dss import DSSCompatFlags DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' @@ -983,6 +1033,8 @@ def test_path_sideeffects(): # for _ in range(250): # test_pm_threads() - test_path_sideeffects() - test_capacitor_reactor() + # test_path_sideeffects() + # test_capacitor_reactor() + test_loadshape_extended() + test_xycurve_extended() print('DONE!') \ No newline at end of file From d321287f1b7947b2a8f483f6783d0efae36b432a Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:10:20 -0300 Subject: [PATCH 35/82] CktElement: update `BusNames` to allow getting the names without the connection/node spec --- dss/ICktElement.py | 21 ++++++++++++++++----- dss/patch_dss_com.py | 7 ++++++- tests/test_general.py | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/dss/ICktElement.py b/dss/ICktElement.py index 98040081..295fc317 100644 --- a/dss/ICktElement.py +++ b/dss/ICktElement.py @@ -160,19 +160,30 @@ def AllVariableValues(self) -> Float64Array: ''' return self._lib.CktElement_Get_AllVariableValues_GR() - @property - def BusNames(self) -> List[str]: + def _get_BusNames(self, removeNodes: bool = False) -> List[str]: ''' Bus definitions to which each terminal is connected. + The `removeNodes` argument is an **API Extension**. Use it to get only the bus names, + without the connection/node specification, if present. + Original COM help: https://opendss.epri.com/BusNames.html ''' - return self._lib.CktElement_Get_BusNames() + return self._lib.CktElement_Get_BusNames(removeNodes) - @BusNames.setter - def BusNames(self, Value: List[AnyStr]): + def _set_BusNames(self, Value: List[AnyStr]): self._set_string_array(self._lib.CktElement_Set_BusNames, Value) + BusNames = property(_get_BusNames, _set_BusNames) # type: List[str] + ''' + Bus definitions to which each terminal is connected. + + In the getter function (`_get_BusNames`), the `removeNodes` argument is an **API Extension**. + Use it to get only the bus names, without the connection/node specification, if present. + + Original COM help: https://opendss.epri.com/BusNames.html + ''' + @property def CplxSeqCurrents(self) -> Float64ArrayOrComplexArray: ''' diff --git a/dss/patch_dss_com.py b/dss/patch_dss_com.py index 308d5c5a..18e5997f 100644 --- a/dss/patch_dss_com.py +++ b/dss/patch_dss_com.py @@ -113,13 +113,18 @@ def Lines_Set_IsSwitch(self, Value): obj.ActiveCircuit.SetActiveElement(f'Line.{name}') elem.Properties['Switch'].Val = 'y' if Value else 'n' + def _get_BusNames(self, removeNodes=False): + return [x.split('.', 1)[0] for x in self.BusNames] # Callable DSS type(obj).__call__ = custom_dss_call # Monitors AsMatrix type(obj.ActiveCircuit.Monitors).AsMatrix = Monitors_AsMatrix - + + # Extended getter for CktElement.BusNames + type(obj.ActiveCircuit.ActiveCktElement)._get_BusNames = _get_BusNames + # Load Phases type(obj.ActiveCircuit.Loads).Phases = property(Load_Phases, Load_Set_Phases) diff --git a/tests/test_general.py b/tests/test_general.py index 21ae59a8..8095bd77 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -1027,6 +1027,20 @@ def test_path_sideeffects(): test_loadshape_save() +def test_busnames_ext(): + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + CE = DSS.ActiveCircuit.ActiveCktElement + + DSS.ActiveCircuit.SetActiveElement('Line.632633') + assert tuple(CE.BusNames) == ('632.1.2.3', '633.1.2.3') + assert tuple(CE._get_BusNames(True)) == ('632', '633') + + DSS.ActiveCircuit.SetActiveElement('Transformer.Sub') + assert tuple(CE.BusNames) == ('sourcebus', '650') + assert tuple(CE._get_BusNames(True)) == ('sourcebus', '650') + + + if __name__ == '__main__': DSS.AllowForms = False print(DSS.Version) From 4d78ec7822a389851fad841b2163bb0d18f22ba3 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:11:28 -0300 Subject: [PATCH 36/82] _cffi_api_util: rename main class to `AltDSSAPIUtil`, add `_map_objs` (private flag) --- dss/_cffi_api_util.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index f6f33d7d..2a127987 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -520,7 +520,7 @@ def _decode_and_free_string(self, s) -> str: def altdss_python_util_callback(ctx, event_code, step, ptr): # print(ctx_util.ctx, AltDSSEvent(event_code), step, ptr) - ctx_util = CffiApiUtil._ctx_to_util[ctx] + ctx_util = AltDSSAPIUtil._ctx_to_util[ctx] if event_code == AltDSSEvent.ReprocessBuses: ctx_util.reprocess_buses_callback(step) @@ -531,7 +531,7 @@ def altdss_python_util_callback(ctx, event_code, step, ptr): return -class CffiApiUtil: +class AltDSSAPIUtil: ''' An internal class with various API and DSSContext management functions and structures. ''' @@ -554,6 +554,7 @@ def __init__(self, ffi, lib, ctx=None, is_odd=False): self._obj_refs = [] self._bus_ref_to_name = None self._is_clearing = False + self._map_objs = True if ctx is None: self.lib = lib ctx = lib.ctx_Get_Prime() @@ -563,8 +564,8 @@ def __init__(self, ffi, lib, ctx=None, is_odd=False): self.settings_ptr = ffi.new('int32_t*') self.settings_ptr[0] = 0 self.lib = CtxLib(self, self.settings_ptr) - if ctx not in CffiApiUtil._ctx_to_util: - CffiApiUtil._ctx_to_util[ctx] = self + if ctx not in AltDSSAPIUtil._ctx_to_util: + AltDSSAPIUtil._ctx_to_util[ctx] = self self.track_objects = True self.register_callbacks() @@ -1348,3 +1349,7 @@ def to_altdss(self) -> DSSObject: ''' ptr = self._Get_Pointer() return self._api_util.get_dss_obj(ptr) + + +# For backwards compat +CffiApiUtil = AltDSSAPIUtil \ No newline at end of file From 11d1ce9fef2dd7453d1fc764cda6cc7e84a71bad Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 12 Sep 2024 01:17:01 -0300 Subject: [PATCH 37/82] YMatrix: remove unused argument in GetCompressedYMatrix --- dss/IYMatrix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dss/IYMatrix.py b/dss/IYMatrix.py index 8913a3f8..7f5e85dd 100644 --- a/dss/IYMatrix.py +++ b/dss/IYMatrix.py @@ -18,7 +18,7 @@ class IYMatrix(Base): __slots__ = [] - def GetCompressedYMatrix(self, factor: bool = True) -> Tuple[ComplexArray, Int32Array, Int32Array]: + def GetCompressedYMatrix(self) -> Tuple[ComplexArray, Int32Array, Int32Array]: '''Return as (data, indices, indptr) that can fed into `scipy.sparse.csc_matrix`''' ffi = self._api_util.ffi @@ -33,7 +33,7 @@ def GetCompressedYMatrix(self, factor: bool = True) -> Tuple[ComplexArray, Int32 cValsPtr = ffi.new('double**') lib = self._api_util.lib_unpatched # use the raw CFFI version - lib.YMatrix_GetCompressedYMatrix(factor, nBus, nNz, ColPtr, RowIdxPtr, cValsPtr) + lib.YMatrix_GetCompressedYMatrix(True, nBus, nNz, ColPtr, RowIdxPtr, cValsPtr) if not nBus[0] or not nNz[0]: res = None From 0b28c3009ed63c032ed05faa3296d18dc97004b2 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 12 Sep 2024 01:17:41 -0300 Subject: [PATCH 38/82] Circuit: Minor docstring update ion AllBusVmagPu --- dss/ICircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 384596ef..991800e4 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -437,7 +437,7 @@ def AllBusVmag(self) -> Float64Array: @property def AllBusVmagPu(self) -> Float64Array: ''' - Double Array of all bus voltages (each node) magnitudes in Per unit + Array of all bus voltages (each node) magnitudes in Per unit Original COM help: https://opendss.epri.com/AllBusVmagPu.html ''' From 1565fd3093bfde34e92d2c5463de4ec44ca15b9d Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:12:44 -0300 Subject: [PATCH 39/82] Reactors: enable on Oddie since it's implemented since 2024-10-04 on the EPRI's distro. --- dss/ICircuit.py | 2 +- dss/IReactors.py | 22 +++++++++++++++++++--- dss/_cffi_api_util.py | 5 +++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 991800e4..0e70cb6b 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -214,7 +214,7 @@ def __init__(self, api_util): self.WireData = IWireData(api_util) if not api_util._is_odd else None self.CNData = ICNData(api_util) if not api_util._is_odd else None self.TSData = ITSData(api_util) if not api_util._is_odd else None - self.Reactors = IReactors(api_util) if not api_util._is_odd else None + self.Reactors = IReactors(api_util) self.ReduceCkt = IReduceCkt(api_util) #: Circuit Reduction Interface self.Storages = IStorages(api_util) self.GICSources = IGICSources(api_util) diff --git a/dss/IReactors.py b/dss/IReactors.py index f28364fa..01ace2ec 100644 --- a/dss/IReactors.py +++ b/dss/IReactors.py @@ -9,7 +9,7 @@ class IReactors(Iterable): ''' Reactor objects - (API Extension) + API Status: **(API Extension)** before 2024-10-04. Since then, functions partially marked as extensions (see each property/function documentation). ''' __slots__ = [] @@ -44,12 +44,18 @@ def SpecType(self) -> int: ''' How the reactor data was provided: 1=kvar, 2=R+jX, 3=R and X matrices, 4=sym components. Depending on this value, only some properties are filled or make sense in the context. + + **(API Extension)** ''' return self._lib.Reactors_Get_SpecType() #TODO: use enum @property def IsDelta(self) -> bool: - '''Delta connection or wye?''' + ''' + Delta connection or wye? + + **(API Extension)** + ''' return self._lib.Reactors_Get_IsDelta() @IsDelta.setter @@ -74,6 +80,8 @@ def LmH(self) -> float: def LmH(self, Value: float): self._lib.Reactors_Set_LmH(Value) + lmH = LmH # Compatibility for the new + @property def kV(self) -> float: '''For 2, 3-phase, kV phase-phase. Otherwise specify actual coil rating.''' @@ -94,7 +102,11 @@ def kvar(self, Value: float): @property def Phases(self) -> int: - '''Number of phases.''' + ''' + Number of phases. + + **(API Extension)** + ''' return self._lib.Reactors_Get_Phases() @Phases.setter @@ -107,6 +119,8 @@ def Bus1(self) -> str: Name of first bus. Bus2 property will default to this bus, node 0, unless previously specified. Only Bus1 need be specified for a Yg shunt reactor. + + **(API Extension)** ''' return self._lib.Reactors_Get_Bus1() @@ -119,6 +133,8 @@ def Bus2(self) -> str: ''' Name of 2nd bus. Defaults to all phases connected to first bus, node 0, (Shunt Wye Connection) except when Bus2 is specifically defined. Not necessary to specify for delta (LL) connection + + **(API Extension)** ''' return self._lib.Reactors_Get_Bus2() diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 2a127987..1cf6a69e 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -15,6 +15,7 @@ pass try: + xxxx # Try to import the fast backend from dss_python_backend._fastdss import AltDSS_PyContext except: @@ -1337,6 +1338,10 @@ def idx(self) -> int: ''' return self._Get_idx() + @idx.setter + def idx(self, Value: int): + self._Set_idx(Value) + def to_altdss(self) -> DSSObject: ''' Returns a Python object for the current active DSS object in this interface. From 32debbcf442d102f5796371b7b40a023ce6dbe00 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:07:59 -0300 Subject: [PATCH 40/82] Tests/save_outputs: fix class reference --- tests/save_outputs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 1c1f07c5..d1826602 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -225,9 +225,9 @@ def export_dss_api_cls(dss: dss.IDSS, dss_cls): if ((not SAVE_DSSX_OUTPUT) or SAVE_DSSX_OUTPUT_ODD) and lname in ('istorages', 'iwindgens'): def iter_cls(): - for i in range(cls.Count): - cls.idx = i - yield cls + for i in range(dss_cls.Count): + dss_cls.idx = i + yield dss_cls items = iter_cls() From e9e4df4c668efb3d7cae1b89777368110f37f88a Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:08:34 -0300 Subject: [PATCH 41/82] Oddie: try to handle errors better when loading the library --- dss/Oddie.py | 71 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/dss/Oddie.py b/dss/Oddie.py index af6d2c93..132b5000 100644 --- a/dss/Oddie.py +++ b/dss/Oddie.py @@ -1,4 +1,5 @@ from __future__ import annotations +import sys, platform, ctypes, os from typing import Optional from ._cffi_api_util import CffiApiUtil from .IDSS import IDSS @@ -19,7 +20,7 @@ class IOddieDSS(IDSS): the `dss_python_backend` package. If it is not available, an import error should occur when trying to use this. - AltDSS Oddie wraps OpenDSSDirect.DLL, providing a minimal compatiliby layer + AltDSS Oddie wraps OpenDSSDirect.DLL, providing a minimal compatibility layer to expose it with the same API as AltDSS/DSS C-API. With it, we can just reuse most of the tools from the other projects on DSS-Extensions without too much extra work. @@ -31,7 +32,7 @@ class IOddieDSS(IDSS): more information. :param library_path: The name or full path of the target dynamic library to - load. Defaults to trying to load "OpenDSSDirect" from `c:\Program Files\OpenDSS\x64`, + load. Defaults to trying to load "OpenDSSDirect" from `C:\Program Files\OpenDSS\x64`, followed by trying to load it from the current path. :param load_flags: Optional, flags to feed the [`LoadLibrary`](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa) @@ -41,7 +42,42 @@ class IOddieDSS(IDSS): the default settings (recommended) will be used. For advanced users. ''' + def _handle_load_lib_error(self): + ''' + This function is used to try to provide a more helpful message when the + library cannot be loaded. + ''' + try: + if sys.platform == 'win32': + error = ctypes.GetLastError() + if error: + raise ctypes.WinError(error) + + return + + if sys.platform == 'linux': + # ld = ctypes.cdll.LoadLibrary("ld-linux-x86-64.so.2") + # ld.dlerror.argtypes = [] + # ld.dlerror.restype = ctypes.c_char_p + # error = ld.dlerror() + # if error: + # raise RuntimeError(error.decode()) + return + except: + # We can ignore if something fails since the generic + # check will still work outside. + pass + def __init__(self, library_path: str = '', load_flags: Optional[int] = None, oddie_options: Optional[OddieOptions] = None): + if sys.platform == 'cygwin': + raise NotImplementedError("Cygwin support is not implemented") + elif sys.platform == 'wasi': + raise NotImplementedError("WASI support is not implemented") + elif sys.platform == 'win32': + not64bits = (platform.architecture()[0] != '64bit') + if not64bits: + raise NotImplementedError("On Windows, only 64-bit (x64) environments are supported. If you need support, please open an issue at https://github.com/dss-extensions/") + from dss_python_backend import _altdss_oddie_capi lib = _altdss_oddie_capi.lib ffi = _altdss_oddie_capi.ffi @@ -52,19 +88,34 @@ def __init__(self, library_path: str = '', load_flags: Optional[int] = None, odd c_load_flags = ffi.new('uint32_t*', load_flags) if library_path: - library_path = library_path.encode() - lib.Oddie_SetLibOptions(library_path, c_load_flags) - ctx = lib.ctx_New() - else: - # Try the default install folder - library_path = rb'C:\Program Files\OpenDSS\x64\OpenDSSDirect.dll' + if not isinstance(library_path, bytes): + library_path = str(library_path).encode() + lib.Oddie_SetLibOptions(library_path, c_load_flags) ctx = lib.ctx_New() if ctx == NULL: - # Try from the general path, let the system resolve it - library_path = rb'OpenDSSDirect.dll' + self._handle_load_lib_error() + + elif sys.platform == 'win32': + _win32_lib_paths = [ + rb'C:\Program Files\OpenDSS\x64\OpenDSSDirect.dll', # Try the default install folder + rb'OpenDSSDirect.dll', # Try from the general path, let the system resolve it + ] + + for library_path in _lib_paths: lib.Oddie_SetLibOptions(library_path, c_load_flags) ctx = lib.ctx_New() + if ctx != NULL: + break + else: + self._handle_load_lib_error() + + elif sys.platform == 'linux': + library_path = b'libOpenDSSC.so' #TODO: add proper version extensions (e.g. libOpenDSSC.so.10) + lib.Oddie_SetLibOptions(library_path, c_load_flags) + ctx = lib.ctx_New() + if ctx == NULL: + self._handle_load_lib_error() if ctx == NULL: raise RuntimeError("Could not load the target library.") From 501ed3f9cc184f86f8cfeacaf01ff2b45e690247 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:09:03 -0300 Subject: [PATCH 42/82] Tests/compare_outputs: compare integer arrays --- tests/compare_outputs.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/compare_outputs.py b/tests/compare_outputs.py index 7600644e..0bab27f6 100644 --- a/tests/compare_outputs.py +++ b/tests/compare_outputs.py @@ -293,6 +293,21 @@ def compare(self, a, b, org_path=None): continue + if isinstance(va[0], int): + va = np.asarray(va) + vb = np.asarray(vb) + + if len(vb) != len(va): + self.printe('ERROR (int, vector, shapes):', path, f'a: {len(va)}, b: {len(vb)}') + continue + + if not all(va == vb): + self.printe('ERROR (int. vector):', path, f'a: {va}, b: {vb}') + + continue + + + if isinstance(va[0], float) or va[0] is None: if None in va: va = [x if x is not None else np.NaN for x in va] From 31a2543c0e8deeaeb106de4927c2e6039737d77e Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 1 Nov 2024 08:31:05 -0300 Subject: [PATCH 43/82] WIP plotting updates --- dss/plot.py | 92 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index 95079913..6201760f 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -25,7 +25,7 @@ from matplotlib.path import Path from matplotlib.collections import LineCollection from mpl_toolkits.mplot3d.art3d import Line3DCollection - from matplotlib.patches import Rectangle + import matplotlib.patches as patches import matplotlib.colors import scipy.sparse.coo as coo except: @@ -34,7 +34,6 @@ if TYPE_CHECKING: from altdss.AltDSS import IAltDSS - class DSSPlotType(Enum): AutoAddLog = 'AutoAddLog' Circuit = 'Circuit' @@ -1546,7 +1545,7 @@ def dss_visualize_plot(DSS: IDSS, box_xy0 = np.array([100, 10]) box_xy1 = np.array([XMAX - 100, y]) box_wh = box_xy1 - box_xy0 - middle_box = Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') + middle_box = patches.Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') ax.text(XMAX / 2, 10 + (y - 10) / 2, f'{etype}.{ename.upper()}', ha='center', va='center', fontweight='bold', rotation='vertical') ax.add_patch(middle_box) ax.plot([0, 300], [0, 0], color='gray', lw=7) @@ -2420,26 +2419,43 @@ def disable(): def _int_to_color(v: int): return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) -from matplotlib import pyplot as plt -import matplotlib.patches as patches -from numpy import asarray -import numpy as np -from dss.plot import get_marker_dict -import re - -DSV_LINE_STYLES = { - 0: 'solid', - 1: 'dashed', - 2: 'dotted', - 3: 'dashdot', - 4: (0, (3, 5, 1, 5, 1, 5)), +DSS_ITEMS = { + 'BoldLabel', + 'Caption', + 'Center', + 'ChartCaption', + 'Circle', + 'ClickOn', + 'Curve', + 'DataColor', + 'Draw', + 'FStyle', + 'KeepAspect', + 'KeyClass', + 'Label', + 'Line', + 'Marker', + 'Move', + 'NoScales', + 'PctRim', + 'Range', + 'Rect', + 'SetProp', + 'Text', + 'TxtAlign', + 'Width', + 'Xlabel', + 'Ylabel', } -def _int_to_color(v: int): - return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) +class IPlotting: + def __init__(self, dss: IDSS): + self.dss = dss + class DSVHandler: - def __init__(self): + def __init__(self, fn: str): + self.fn = fn self.fig, self.ax = plt.subplots() self.ax.get_xaxis().get_major_formatter().set_scientific(False) self.ax.get_yaxis().get_major_formatter().set_scientific(False) @@ -2563,7 +2579,6 @@ def Label(self, param_str: str): def Line(self, param_str: str): - #TODO: use LineCollection *str_params, rest = param_str.split(',', 3) @@ -2661,27 +2676,32 @@ def Width(self, param_str: str): def Xlabel(self, param_str: str): self.ax.set_xlabel(param_str.strip().strip('"')) - + def Ylabel(self, param_str: str): self.ax.set_ylabel(param_str.strip().strip('"')) + def parse(self): + with open(self.fn, 'r') as f: + for l in f: + l = l.strip() + if not l: + continue -def plot_dsv(fn: str): - handler = DSVHandler() - with open(fn, 'r') as f: - for l in f: - l = l.strip() - if not l: - continue + item_name, *rest = l.split(',', 1) + item_name = item_name.strip() + if item_name not in DSS_ITEMS: + raise NotImplemented(f'"{item_name}" DSV item is not implemented') - item_name, *rest = l.split(',', 1) - item_name = item_name.strip() - # print(item, repr(rest)[:100]) - getattr(handler, item_name)(rest[0] if rest else '') # let the exception propagate on error + # print(item, repr(rest)[:100]) + getattr(self, item_name)(rest[0] if rest else '') # let the exception propagate on error - if _do_show: - plt.show() - else: - return handler.fig, handler.ax + if _do_show: + plt.show() + else: + return self.fig, self.ax + + +def plot_dsv(fn: str): + return DSVHandler(fn).parse() __all__ = ['enable', 'disable', 'plot_dsv', ] From c06c7e738b01bdafd362f1e0cb5f54c46a46427a Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:21:16 -0300 Subject: [PATCH 44/82] General changes: - Update the code to reflect the new backends with FastDSS and the new loader-style lib. - Rename _is_odd to _is_oddie to make it clear that it's not related to ODD.py; add a method `is_oddie()` in the main class. - Remove DSSSimComs from the high-level API - Adjust some function names to match the new backend, as well as the prepared lib initialization. - Move `DSSException` to a new `dss.error.Exception`. - `test_skip_files()` updated/fixed to match the current impl. - _cffi_api_util being refactored TODO: decide how the settings pointers will be handled across DSS-Python and ODD.py --- docs/index.md | 2 +- dss/IBus.py | 2 +- dss/ICircuit.py | 14 +- dss/IDSS.py | 34 +- dss/IDSSimComs.py | 22 - dss/IMonitors.py | 4 +- dss/IYMatrix.py | 2 +- dss/Oddie.py | 22 +- dss/_cffi_api_util.py | 151 +- dss/error.py | 3 + dss/notebook.py | 123 ++ dss/plot.py | 4175 ++++++++++++++++++------------------- dss/plot2.py | 2698 ++++++++++++++++++++++++ tests/test_general.py | 20 +- tests/test_past_issues.py | 17 +- 15 files changed, 5015 insertions(+), 2274 deletions(-) delete mode 100644 dss/IDSSimComs.py create mode 100644 dss/error.py create mode 100644 dss/notebook.py create mode 100644 dss/plot2.py diff --git a/docs/index.md b/docs/index.md index 9e8a90b3..63df480e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -141,7 +141,6 @@ For a quick overview of DSS-Python, the main DSS class is organized as follows. - {class}`DSS.ActiveCircuit.CapControls ` - {class}`DSS.ActiveCircuit.CNData ` **(API Extension)** - {class}`DSS.ActiveCircuit.CtrlQueue ` - - {class}`DSS.ActiveCircuit.DSSim_Coms ` - {class}`DSS.ActiveCircuit.Fuses ` - {class}`DSS.ActiveCircuit.Generators ` - {class}`DSS.ActiveCircuit.GICSources ` @@ -172,6 +171,7 @@ For a quick overview of DSS-Python, the main DSS class is organized as follows. - {class}`DSS.ActiveCircuit.TSData ` **(API Extension)** - {class}`DSS.ActiveCircuit.Vsources ` - {class}`DSS.ActiveCircuit.WireData ` **(API Extension)** + - {class}`DSS.ActiveCircuit.WindGens ` - {class}`DSS.ActiveCircuit.XYCurves ` diff --git a/dss/IBus.py b/dss/IBus.py index 20989c83..cb35372a 100644 --- a/dss/IBus.py +++ b/dss/IBus.py @@ -451,7 +451,7 @@ def __call__(self, index: Union[int, str]) -> IBus: return self.__getitem__(index) def __iter__(self) -> Iterator[IBus]: - if self._api_util._is_odd: + if self._api_util._is_oddie: for i in range(self._lib.Circuit_Get_NumBuses()): self._lib.Circuit_SetActiveBusi(i) yield self diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 0e70cb6b..2fac44c6 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -31,7 +31,6 @@ from .ILoadShapes import ILoadShapes from .IFuses import IFuses from .IISources import IISources -from .IDSSimComs import IDSSimComs from .IPVSystems import IPVSystems from .IVsources import IVsources from .ILineCodes import ILineCodes @@ -82,7 +81,6 @@ class ICircuit(Base): 'Fuses', 'Isources', 'ISources', - 'DSSim_Coms', 'PVSystems', 'Vsources', 'LineCodes', @@ -155,7 +153,6 @@ class ICircuit(Base): Fuses: IFuses Isources: IISources ISources: IISources - DSSim_Coms: IDSSimComs PVSystems: IPVSystems Vsources: IVsources LineCodes: ILineCodes @@ -205,15 +202,14 @@ def __init__(self, api_util): object.__setattr__(self, 'Isources', Isources) object.__setattr__(self, 'ISources', Isources) - self.DSSim_Coms = IDSSimComs(api_util) self.PVSystems = IPVSystems(api_util) self.Vsources = IVsources(api_util) self.LineCodes = ILineCodes(api_util) - self.LineGeometries = ILineGeometries(api_util) if not api_util._is_odd else None - self.LineSpacings = ILineSpacings(api_util) if not api_util._is_odd else None - self.WireData = IWireData(api_util) if not api_util._is_odd else None - self.CNData = ICNData(api_util) if not api_util._is_odd else None - self.TSData = ITSData(api_util) if not api_util._is_odd else None + self.LineGeometries = ILineGeometries(api_util) if not api_util._is_oddie else None + self.LineSpacings = ILineSpacings(api_util) if not api_util._is_oddie else None + self.WireData = IWireData(api_util) if not api_util._is_oddie else None + self.CNData = ICNData(api_util) if not api_util._is_oddie else None + self.TSData = ITSData(api_util) if not api_util._is_oddie else None self.Reactors = IReactors(api_util) self.ReduceCkt = IReduceCkt(api_util) #: Circuit Reduction Interface self.Storages = IStorages(api_util) diff --git a/dss/IDSS.py b/dss/IDSS.py index 4a06bb7d..28ff85b1 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -14,7 +14,6 @@ from .IDSS_Executive import IDSS_Executive from .IDSSEvents import IDSSEvents from .IParser import IParser -from .IDSSimComs import IDSSimComs from .IYMatrix import IYMatrix from .IZIP import IZIP @@ -46,7 +45,6 @@ class IDSS(Base): 'Executive', 'Events', 'Parser', - 'DSSim_Coms', 'YMatrix', 'ZIP', '_version', @@ -75,7 +73,6 @@ class IDSS(Base): Executive: IDSS_Executive Events: IDSSEvents Parser: IParser - DSSim_Coms: IDSSimComs YMatrix: IYMatrix ZIP: IZIP @@ -137,15 +134,11 @@ def __init__(self, api_util): self.Executive = IDSS_Executive(api_util) #: Kept for compatibility. - self.Events = IDSSEvents(api_util) if not api_util._is_odd else None + self.Events = IDSSEvents(api_util) if not api_util._is_oddie else None #: Kept for compatibility. self.Parser = IParser(api_util) - #: Kept for compatibility. Apparently was used for DSSim-PC (now OpenDSS-G), a - #: closed-source software developed by EPRI using LabView. - self.DSSim_Coms = IDSSimComs(api_util) if not api_util._is_odd else None - #: The YMatrix interface provides advanced access to the internals of #: the DSS engine. The sparse admittance matrix of the system is also #: available here. @@ -160,7 +153,7 @@ def __init__(self, api_util): #: and run scripts inside the ZIP, without creating extra files on disk. #: #: **(API Extension)** - self.ZIP = IZIP(api_util) if not api_util._is_odd else None + self.ZIP = IZIP(api_util) if not api_util._is_oddie else None Base.__init__(self, api_util) @@ -192,6 +185,16 @@ def to_opendssdirect(self) -> OpenDSSDirect: from opendssdirect.OpenDSSDirect import OpenDSSDirect return OpenDSSDirect._get_instance(ctx=self._api_util.ctx, api_util=self._api_util) + def is_oddie(self) -> bool: + """ + Returns True if this instance is based on the Oddie compatibility layer for + the official OpenDSS Direct API (a.k.a. DCSL). + + Note that the default engine in DSS-Python has been based on AltDSS since + 2018, even though it was not called AltDSS then. + """ + return self._api_util._is_oddie + def ClearAll(self): self._lib.DSS_ClearAll() @@ -214,8 +217,9 @@ def Start(self, code: int) -> bool: handled automatically, so the users do not need to call it manually, unless using AltDSS/DSS C-API directly without further tools. - On the official OpenDSS, `Start` also does nothing at all in the current - versions. + On the official OpenDSS, `Start` also does nothing at all in the current + Delphi versions. It is required for OpenDSS-C, but also handled behind + the scenes on DSS-Extensions. Original COM help: https://opendss.epri.com/Start.html ''' @@ -439,7 +443,7 @@ def NewContext(self) -> IDSS: **(API Extension)** ''' - if self._api_util._is_odd: + if self._api_util._is_oddie: raise NotImplementedError("NewContext is not supported for the official OpenDSS engine.") ffi = self._api_util.ffi @@ -552,7 +556,7 @@ def ShareGeneral(self, otherContext: IDSS, skip_cmds: Optional[List[str]] = None and its objects are kept alive while other contexts require it. Optionally, as a shortcut, the user can provide `skip_cmds` to be passed to the `Settings.SkipCommands` - and `skip_file_regexp` to be passed to `Settings.SkipFileRegExp`, in the second DSS context. + and `skip_file_regexp` to be passed to `Settings.SkipFileRegExp`, in the second DSS context. *Note*: If the `clear` command is included in `Settings.SkipCommands`, the `DSS.ClearAll()` method can still be called and it will reset both skip settings. @@ -561,10 +565,10 @@ def ShareGeneral(self, otherContext: IDSS, skip_cmds: Optional[List[str]] = None **(API Extension)** ''' - if self._api_util._is_odd or otherContext._api_util._is_odd: + if self._api_util._is_oddie or otherContext._api_util._is_oddie: raise ValueError("Only AltDSS engine contexts can share data.") - self._lib.ShareGeneral(otherContext._api_util.ctx) + self._lib.ctx_ShareGeneral(otherContext._api_util.ctx) if skip_cmds is not None: otherContext.ActiveCircuit.Settings.SkipCommands = skip_cmds diff --git a/dss/IDSSimComs.py b/dss/IDSSimComs.py deleted file mode 100644 index 623e4874..00000000 --- a/dss/IDSSimComs.py +++ /dev/null @@ -1,22 +0,0 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors -from ._cffi_api_util import Base -from ._types import Float64Array -import warnings - -class IDSSimComs(Base): - ''' - **Deprecated**; use `DSS.ActiveCircuit.ActiveBus` API or the AltDSS alternatives instead - ''' - __slots__ = [] - - def BusVoltage(self, Index: int) -> Float64Array: - warnings.warn('Use ActiveCircuit.ActiveBus or the AltDSS (AltDSS-Python) alternatives.', DeprecationWarning, stacklevel=2) - return self._lib.DSSimComs_BusVoltage_GR(Index) - - def BusVoltagepu(self, Index: int) -> Float64Array: - warnings.warn('Use ActiveCircuit.ActiveBus or the AltDSS (AltDSS-Python) alternatives.', DeprecationWarning, stacklevel=2) - return self._lib.DSSimComs_BusVoltagepu_GR(Index) - - diff --git a/dss/IMonitors.py b/dss/IMonitors.py index 7fe33b8f..840bffcb 100644 --- a/dss/IMonitors.py +++ b/dss/IMonitors.py @@ -44,7 +44,7 @@ def Channel(self, Index: int) -> Float32Array: ffi = self._api_util.ffi api_util = self._api_util - api_util.lib_unpatched.ctx_Monitors_Get_ByteStream_GR(api_util.ctx) + api_util.lib_unpatched.Monitors_Get_ByteStream_GR(api_util.ctx) api_util._check_for_error() ptr, cnt = api_util.gr_int8_pointers cnt = cnt[0] @@ -67,7 +67,7 @@ def AsMatrix(self) -> Float64Array: ffi = self._api_util.ffi api_util = self._api_util - api_util.lib_unpatched.ctx_Monitors_Get_ByteStream_GR(api_util.ctx) + api_util.lib_unpatched.Monitors_Get_ByteStream_GR(api_util.ctx) api_util._check_for_error() ptr, cnt = api_util.gr_int8_pointers cnt = cnt[0] diff --git a/dss/IYMatrix.py b/dss/IYMatrix.py index 7f5e85dd..0c8fbaaa 100644 --- a/dss/IYMatrix.py +++ b/dss/IYMatrix.py @@ -33,7 +33,7 @@ def GetCompressedYMatrix(self) -> Tuple[ComplexArray, Int32Array, Int32Array]: cValsPtr = ffi.new('double**') lib = self._api_util.lib_unpatched # use the raw CFFI version - lib.YMatrix_GetCompressedYMatrix(True, nBus, nNz, ColPtr, RowIdxPtr, cValsPtr) + lib.YMatrix_GetCompressedYMatrix(self._api_util.ctx, True, nBus, nNz, ColPtr, RowIdxPtr, cValsPtr) if not nBus[0] or not nNz[0]: res = None diff --git a/dss/Oddie.py b/dss/Oddie.py index 132b5000..d30808db 100644 --- a/dss/Oddie.py +++ b/dss/Oddie.py @@ -68,6 +68,16 @@ def _handle_load_lib_error(self): # check will still work outside. pass + + def is_oddie(self) -> bool: + """ + Returns True if this instance is based on the Oddie compatibility layer for + the official OpenDSS Direct API (a.k.a. DCSL). + + Note that the default instance in OpenDSSDirect.py is based on AltDSS since 2018. + """ + return True + def __init__(self, library_path: str = '', load_flags: Optional[int] = None, oddie_options: Optional[OddieOptions] = None): if sys.platform == 'cygwin': raise NotImplementedError("Cygwin support is not implemented") @@ -76,11 +86,11 @@ def __init__(self, library_path: str = '', load_flags: Optional[int] = None, odd elif sys.platform == 'win32': not64bits = (platform.architecture()[0] != '64bit') if not64bits: - raise NotImplementedError("On Windows, only 64-bit (x64) environments are supported. If you need support, please open an issue at https://github.com/dss-extensions/") + raise NotImplementedError("On Windows, only 64-bit (x64) environments are supported with Oddie. If you need support for further architectures, please open an issue at https://github.com/dss-extensions/") - from dss_python_backend import _altdss_oddie_capi - lib = _altdss_oddie_capi.lib - ffi = _altdss_oddie_capi.ffi + from dss_python_backend import oddie as oddie_capi + lib = oddie_capi.lib + ffi = oddie_capi.ffi NULL = ffi.NULL c_load_flags = NULL @@ -120,14 +130,14 @@ def __init__(self, library_path: str = '', load_flags: Optional[int] = None, odd if ctx == NULL: raise RuntimeError("Could not load the target library.") - if lib.ctx_DSS_Start(ctx, 0) != 1: + if lib.DSS_Start(ctx, 0) == 0: raise RuntimeError("DSS_Start call was not successful.") if oddie_options is not None: lib.Oddie_SetOptions(oddie_options) ctx = ffi.gc(ctx, lib.ctx_Dispose) - api_util = CffiApiUtil(ffi, lib, ctx, is_odd=True) + api_util = CffiApiUtil(ffi, lib, ctx, is_oddie=True) api_util._library_path = library_path IDSS.__init__(self, api_util) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 1cf6a69e..890dcb81 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -1,5 +1,5 @@ from __future__ import annotations -import warnings +import os, warnings from functools import partial, wraps from weakref import ref, WeakKeyDictionary import numpy as np @@ -7,6 +7,7 @@ from typing import Any, AnyStr, Callable, List, Union, Iterator, Optional, TYPE_CHECKING from .enums import AltDSSEvent from dss_python_backend.events import get_manager_for_ctx +from .error import DSSException if TYPE_CHECKING: try: @@ -14,13 +15,22 @@ except: pass +AltDSS_PyContext = None try: - xxxx - # Try to import the fast backend - from dss_python_backend._fastdss import AltDSS_PyContext + if os.environ.get('DSS_EXTENSIONS_FASTDSS', '') != '0': + # Try to import the fast backend + from dss_python_backend._fastdss import AltDSS_PyContext + else: + warnings.warn("DSS-Extensions: DSS_EXTENSIONS_FASTDSS environment variable is set to 0; using the legacy full CFFI backend.") except: + warnings.warn("DSS-Extensions: Could not import the FastDSS backend; using the legacy full CFFI backend.") + pass + +if AltDSS_PyContext is None: + # Import the prepared function info if the fast implementation from + # AltDSS_PyContext is not available. import dss_python_backend._func_info as _func_info - AltDSS_PyContext = None + # Assumed UTF8; unless the fast C extension (dss_python_backend._fast_strs) is not @@ -79,11 +89,6 @@ def _is_case_insensitive() -> bool: return (getattr(Base, '__getattr__', None) == Base._getattr or getattr(Base, '__getattr__', None) == Base._getattr_case_check) -class DSSException(Exception): - def __str__(self): - return f'(#{self.args[0]}) {self.args[1]}' - - # For backwards compatibility, will be removed for version 1.0 DssException = DSSException use_com_compat = set_case_insensitive_attributes @@ -122,7 +127,7 @@ def _get_strs_ctx(self, errorPtr, ctx, func: Callable, *args: Any) -> List[str]: def _get_bool_ctx(self, errorPtr, ctx, func: Callable, *args): - result = func(ctx, *args) + result = bool(func(ctx, *args)) if errorPtr[0] and Base._use_exceptions: error_num = errorPtr[0] errorPtr[0] = 0 @@ -167,7 +172,7 @@ def _prepare_api_functions_slow(self, done): errorPtr = self._errorPtr t = _func_info.t api_util = self._api_util - is_odd = api_util._is_odd + is_oddie = api_util._is_oddie wrappers = { t.fastdss_types_b16: ('', self._get_bool_ctx,), @@ -189,11 +194,7 @@ def _prepare_api_functions_slow(self, done): arg_wrapper = self._str_arg_wrapper suffix, wrapper, *wrapper_args = wrappers.get(res_type, default_wrapper) - for ctx_name in ctx_names: - if ctx_name in done: - continue - - name = ctx_name[4:] + for name in ctx_names: if name in done: continue @@ -201,12 +202,10 @@ def _prepare_api_functions_slow(self, done): if name in done: continue - ctx_name += suffix - try: - func = getattr(lib, ctx_name) + func = getattr(lib, name) except AttributeError: - if is_odd: + if is_oddie: continue raise @@ -225,15 +224,10 @@ def _prepare_api_functions(self, done, settings_ptr): ctx = self._ctx ffi = self._ffi ctx_int = int(ffi.cast('uintptr_t', ctx)) + lib_int = int(ffi.cast('uintptr_t', self._api_util.lib_unpatched)) self._settings_ptr = settings_ptr settings_ptr_int = int(ffi.cast('uintptr_t', self._settings_ptr)) - - if not self._api_util._is_odd: - self._fast = AltDSS_PyContext(ctx_int, settings_ptr_int, DSSException, done, self) - else: - from dss_python_backend._fastdss_oddie import AltDSS_PyContext as AltDSS_PyContext_Oddie - self._fast = AltDSS_PyContext_Oddie(ctx_int, settings_ptr_int, DSSException, done, self) - + self._fast = AltDSS_PyContext(ctx_int, lib_int, settings_ptr_int, DSSException, done, self) def _get_string(self, b) -> str: if b: @@ -272,29 +266,48 @@ def __init__(self, api_util, settings_ptr): lib = self._lib = api_util.lib_unpatched ctx = self._ctx = api_util.ctx ffi = self._ffi = api_util.ffi + self.settings_ptr = settings_ptr - self._errorPtr = _errorPtr = lib.ctx_Error_Get_NumberPtr(ctx) + self._errorPtr = _errorPtr = lib.Error_Get_NumberPtr(ctx) #TODO: test if a pointer is better than keeping this self._prepared_funcs = [] # Wrap most of the API to provide simpler Python access - done = set(('ctx_Error_Get_Description', 'ctx_Error_Get_Number', 'Error_Get_Description', 'Error_Get_Number')) + done = set(('Error_Get_Description', 'Error_Get_Number',)) self._prepare_api_functions(done, settings_ptr) - self.Error_Get_Description = lambda: self._get_string(lib.ctx_Error_Get_Description(ctx)) - self.Error_Get_Number = lambda: lib.ctx_Error_Get_Number(ctx) + self.Error_Get_Description = lambda: self._get_string(lib.Error_Get_Description(ctx)) + self.Error_Get_Number = lambda: lib.Error_Get_Number(ctx) - skip_funcs = {'ctx_New', 'ctx_Dispose', 'ctx_Get_Prime', 'ctx_Set_Prime', 'ctx_Error_Set_Description', 'ctx_Error_Get_NumberPtr', 'ctx_ZIP_Extract_GR'} + skip_funcs = { + 'ctx_New', 'ctx_Dispose', 'ctx_Get_Prime', 'ctx_Set_Prime', 'Error_Set_Description', 'Error_Get_NumberPtr', 'ctx_ZIP_Extract_GR', + 'DSS_BeginPascalThread', 'DSS_WaitPascalThread', 'DSS_SetPropertiesMO', 'DSS_SetMessagesMO', + 'engineName', 'isAltDSS', 'libHandle', 'versionSignature', + } + + skip_prefixes = ( + 'Oddie_', 'DSS_Dispose_', 'CmathLib_', 'DSSimComs_', 'Alt_', 'Obj_', 'Batch_', + ) + + force_include_prefixes = ( + 'Batch_Create', 'Batch_Filter', + ) + # First, process all `ctx_*`` functions - for name, value in vars(lib).items(): - is_ctx = name.startswith('ctx_') - if (not is_ctx and not name.startswith(('Batch_Create', 'Batch_Filter', ))) or (name in done): + + for name in dir(lib): + if name in done: + continue + + if name.startswith(skip_prefixes) and not name.startswith(force_include_prefixes): continue + value = getattr(lib, name) + # print('>>>', name) + # Keep the basic management functions alone if name in skip_funcs: - if name.startswith('ctx_DSSEvents_') or name == 'ctx_Error_Set_Description': - name = name[4:] + if name.startswith('DSSEvents_') or name == 'Error_Set_Description': setattr(self, name, partial(value, ctx)) else: setattr(self, name, value) @@ -302,27 +315,24 @@ def __init__(self, api_util, settings_ptr): done.add(name) continue - if is_ctx: - name = name[4:] - if name in done: - continue - - if name.endswith('_GR'): - # A few GR functions that don't have dedicated low-level mapping - wrapper_func, res_func = self._error_checked_ctx_gr, api_util.get_float64_gr_array - setattr(self, name, partial(wrapper_func, _errorPtr, ctx, value, res_func)) - done.add(name) - continue + if name.endswith('_GR'): + # A few GR functions that don't have dedicated low-level mapping + wrapper_func, res_func = self._error_checked_ctx_gr, api_util.get_float64_gr_array + setattr(self, name, partial(wrapper_func, _errorPtr, ctx, value, res_func)) + done.add(name) + continue # General functions and array setters are only error checked, no special handling yet setattr(self, name, partial(self._error_checked, _errorPtr, partial(value, ctx))) done.add(name) # Then the new Alt_* family - for name, value in vars(lib).items(): - if (not name.startswith('Alt_')) or name in done: + for name in dir(lib): + if (not name.startswith('Alt_')) or name in done: #TODO: What about Obj_ and Batch_? continue + value = getattr(lib, name) + if name.startswith('Alt_Bus'): setattr(self, name, partial(self._error_checked, _errorPtr, partial(value, ctx))) else: @@ -331,11 +341,11 @@ def __init__(self, api_util, settings_ptr): done.add(name) # Finally the remaining fields - for name, value in vars(lib).items(): + for name in dir(lib): if name.startswith('ctx_') or name in done: continue - setattr(self, name, value) + setattr(self, name, getattr(lib, name)) # if isinstance(value, int): # setattr(self, name, value) # else: @@ -540,11 +550,11 @@ class AltDSSAPIUtil: _altdss: AltDSS - def __init__(self, ffi, lib, ctx=None, is_odd=False): + def __init__(self, ffi, lib, ctx=None, is_oddie=False): self._opendssdirect = None self._dss_python = None self._altdss = None - self._is_odd = is_odd + self._is_oddie = is_oddie self.owns_ctx = True self.codec = codec self.ctx = ctx @@ -562,9 +572,9 @@ def __init__(self, ffi, lib, ctx=None, is_odd=False): self.ctx = ctx self.init_buffers() - self.settings_ptr = ffi.new('int32_t*') - self.settings_ptr[0] = 0 - self.lib = CtxLib(self, self.settings_ptr) + self.settings_ptr = settings_ptr_dsspy = ffi.new('int32_t*') + settings_ptr_dsspy[0] = 0 + self.lib = CtxLib(self, settings_ptr_dsspy) if ctx not in AltDSSAPIUtil._ctx_to_util: AltDSSAPIUtil._ctx_to_util[ctx] = self @@ -603,19 +613,20 @@ def _get_lib(self, prefer_lists: bool, oddpy: bool): # We already have a prepared object, just ensure the settings are OK if prefer_lists: - self.settings_oddpy_ptr[0] = self.settings_oddpy_ptr[0] | _ODDPyStrings | _UseLists + settings_oddpy_ptr = self.lib_odd.settings_ptr + settings_oddpy_ptr[0] = settings_oddpy_ptr[0] | _ODDPyStrings | _UseLists else: - self.settings_oddpy_ptr[0] = (self.settings_oddpy_ptr[0] | _ODDPyStrings) & (~_UseLists) + settings_oddpy_ptr[0] = (settings_oddpy_ptr[0] | _ODDPyStrings) & (~_UseLists) return self.lib_odd - self.settings_oddpy_ptr = self.ffi.new('int32_t*') + settings_oddpy_ptr = self.ffi.new('int32_t*') if prefer_lists: - self.settings_oddpy_ptr[0] = self.settings_ptr[0] | _ODDPyStrings | _UseLists + settings_oddpy_ptr[0] = self.settings_ptr[0] | _ODDPyStrings | _UseLists else: - self.settings_oddpy_ptr[0] = (self.settings_ptr[0] | _ODDPyStrings) & (~_UseLists) + settings_oddpy_ptr[0] = (self.settings_ptr[0] | _ODDPyStrings) & (~_UseLists) - self.lib_odd = CtxLib(self, self.settings_oddpy_ptr) + self.lib_odd = CtxLib(self, settings_oddpy_ptr) return self.lib_odd @@ -725,7 +736,7 @@ def clear_callback(self, step: int): def register_callbacks(self): - if self._is_odd: + if self._is_oddie: return mgr = get_manager_for_ctx(self.ctx) @@ -734,7 +745,7 @@ def register_callbacks(self): mgr.register_func(AltDSSEvent.ReprocessBuses, altdss_python_util_callback) def unregister_callbacks(self): - if self._is_odd: + if self._is_oddie: return mgr = get_manager_for_ctx(self.ctx) mgr.unregister_func(AltDSSEvent.Clear, altdss_python_util_callback) @@ -742,7 +753,7 @@ def unregister_callbacks(self): # The context will die, no need to do anything else currently. def __del__(self): - if self._is_odd: + if self._is_oddie: return self.clear_callback(0) @@ -781,7 +792,7 @@ def init_buffers(self): for ptrs in zip(tmp_float64_pointers, tmp_int32_pointers, tmp_int8_pointers) for ptr in ptrs ] - lib.ctx_DSS_GetGRPointers(self.ctx, *ptr_args) + lib.DSS_GetGRPointers(self.ctx, *ptr_args) # we don't need to keep the extra indirections self.gr_float64_pointers = (tmp_float64_pointers[0][0], tmp_float64_pointers[1][0]) @@ -791,7 +802,7 @@ def init_buffers(self): # also keep a casted version for complex floats self.gr_cfloat64_pointers = (self.ffi.cast('double _Complex**', tmp_float64_pointers[0][0]), tmp_float64_pointers[1][0]) - self._errorPtr = lib.ctx_Error_Get_NumberPtr(self.ctx) + self._errorPtr = lib.Error_Get_NumberPtr(self.ctx) def clear_buffers(self): @@ -1232,7 +1243,7 @@ def get_bus_obj(self, ptr) -> Optional[AltBus]: def _oddie_not_impl(): - raise NotImplementedError("This API requires is not implemented in the official OpenDSS engine or it is available in Oddie.") + raise NotImplementedError("This API requires a function that is not implemented in the official OpenDSS engine.") class Iterable(Base): __slots__ = [ diff --git a/dss/error.py b/dss/error.py new file mode 100644 index 00000000..a213a1d0 --- /dev/null +++ b/dss/error.py @@ -0,0 +1,3 @@ +class DSSException(Exception): + def __str__(self): + return f'(#{self.args[0]}) {self.args[1]}' diff --git a/dss/notebook.py b/dss/notebook.py new file mode 100644 index 00000000..a9214872 --- /dev/null +++ b/dss/notebook.py @@ -0,0 +1,123 @@ +from .IDSS import IDSS + +try: + from IPython import get_ipython + from IPython.display import FileLink, display, display_html, HTML + from IPython.core.magic import register_cell_magic + ipython = get_ipython() + if ipython is None: + raise ImportError + + import html + + def link_file(fn): + relfn = os.path.relpath(fn, os.getcwd()) + if relfn.startswith('..'): + # cannot show in the notebook :( + display(HTML(f'

File output ("{html.escape(relfn)}") outside current workspace.

')) + else: + display(FileLink(relfn, result_html_prefix=f'File output ("{html.escape(fn)}"): ')) + + def show(text): + display(text) + + + @register_cell_magic + def dss(line, cell): + if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_oddie: + DSSPlotCtx.Text.Commands(cell) + else: + for line in cell.split('\n'): + DSSPlotCtx(line) + res = DSSPlotCtx.Text.Result + if res.endswith('.DSV'): + if _enabled and FilePath(res).exists(): + plot_dsv(res) + + DSSPlotCtx.AllowChangeDir = False +except: + def link_file(fn): + print(f'Output file: "{fn}"') + + def show(text): + print(text) + + + #FileLink('path_to_file/filename.extension') + +# import os +# import html +# import tqdm +# from tqdm.notebook import tqdm +# import IPython.display + + +# dss_progress_bar = None +# dss_progress_desc = '' + + +@api_util.ffi.def_extern() +def dss_python_cb_write(ctx, message_str, message_type: int, message_size: int, message_subtype: int): + global dss_progress_bar + global dss_progress_desc + + # DSS = _ctx2dss(ctx) + + message_str = api_util.ffi.string(message_str).decode(api_util.codec) + if message_type == api_util.lib.DSSMessageType_Error: + #print('DSS Error:', message_str, file=sys.stderr) + pass + elif message_type in (api_util.lib.DSSMessageType_ProgressCaption, api_util.lib.DSSMessageType_ProgressFormCaption): + #dss_progress_desc = message_str + # print('Progress Caption:', message_str, file=sys.stderr) + pass + elif message_type == api_util.lib.DSSMessageType_Progress: + #print('DSS Progress:', message_str, file=sys.stderr) + pass + elif message_type == api_util.lib.DSSMessageType_FireOffEditor: + link_file(message_str) + # try: + # # print('DSSMessageType_FireOffEditor') + # with open(message_str, 'r') as f: + # text = f.read() + + # IPython.display.display({'text/plain': text}, raw=True) + # except: + # print(f'Could not display file "{message_str}"') + # return 1 + + elif message_type == api_util.lib.DSSMessageType_ProgressPercent: + try: + pass + # n = int(message_str) + # desc = '' + # if n == 0 and dss_progress_bar is not None: + # dss_progress_bar = None + + # if dss_progress_bar is None: + # dss_progress_bar = tqdm(total=100, desc=dss_progress_desc) + + # if n < 0: + # del dss_progress_bar + # dss_progress_bar = None + # return 0 + + + # dss_progress_bar.n = n + # dss_progress_bar.refresh() +# if n == 100: +# dss_progress_bar.close() + except: + import traceback + traceback.print_exc() + print('DSS Progress:', message_str) + + # else: + # # print(message_type) + # # print(message_str) + # IPython.display.display({'text/plain': message_str}, raw=True) + else: + # do nothing for now... + pass + + return 0 diff --git a/dss/plot.py b/dss/plot.py index 6201760f..2377263d 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -7,7 +7,7 @@ """ from __future__ import annotations import os, re, json, sys, warnings -from typing import List, TYPE_CHECKING, Optional, Tuple, Dict +from typing import List, TYPE_CHECKING, Optional, Tuple, Dict, Union, Iterable from typing_extensions import TypedDict, Unpack from . import api_util from . import DSS as DSSPlotCtx @@ -31,6 +31,8 @@ except: raise ImportError("SciPy and matplotlib are required to use this module.") +from .notebook import * + if TYPE_CHECKING: from altdss.AltDSS import IAltDSS @@ -162,41 +164,42 @@ class PlotParams(TypedDict): CaseName: str CaseYear: int + DEFAULT_MARKER_PARAMS = ObjMarkers( - MarkTransformers=False - # TransMarkerCode: Optional[int] - # TransMarkerSize: Optional[float] + MarkTransformers=False, + TransMarkerCode=None, + TransMarkerSize=None, - # MarkCapacitors: Optional[bool] - # CapMarkerCode: Optional[int] - # CapMarkerSize: Optional[float] + MarkCapacitors=False, + CapMarkerCode=None, + CapMarkerSize=None, - # MarkPVSystems: Optional[bool] - # PVMarkerCode: Optional[int] - # PVMarkerSize: Optional[float] + MarkPVSystems=False, + PVMarkerCode=None, + PVMarkerSize=None, - # MarkStorage: Optional[bool] - # StoreMarkerCode: Optional[int] - # StoreMarkerSize: Optional[float] + MarkStorage=False, + StoreMarkerCode=None, + StoreMarkerSize=None, - # MarkSwitches: Optional[bool] - # SwitchMarkerCode: Optional[int] + MarkSwitches=False, + SwitchMarkerCode=None, - # MarkFuses: Optional[bool] - # FuseMarkerCode: Optional[int] - # FuseMarkerSize: Optional[float] + MarkFuses=False, + FuseMarkerCode=None, + FuseMarkerSize=None, - # MarkRegulators: Optional[bool] - # RegMarkerCode: Optional[int] - # RegMarkerSize: Optional[float] + MarkRegulators=False, + RegMarkerCode=None, + RegMarkerSize=None, - # MarkRelays: Optional[bool] - # RelayMarkerCode: Optional[int] - # RelayMarkerSize: Optional[float] + MarkRelays=False, + RelayMarkerCode=None, + RelayMarkerSize=None, - # MarkReclosers: Optional[bool] - # RecloserMarkerCode: Optional[int] - # RecloserMarkerSize: Optional[float] + MarkReclosers=False, + RecloserMarkerCode=None, + RecloserMarkerSize=None, ) DEFAULT_PLOT_PARAMS = PlotParams( @@ -226,57 +229,6 @@ class PlotParams(TypedDict): MaxScale=None, ) -try: - from IPython import get_ipython - from IPython.display import FileLink, display, display_html, HTML - from IPython.core.magic import register_cell_magic - ipython = get_ipython() - if ipython is None: - raise ImportError - - import html - - def link_file(fn): - relfn = os.path.relpath(fn, os.getcwd()) - if relfn.startswith('..'): - # cannot show in the notebook :( - display(HTML(f'

File output ("{html.escape(relfn)}") outside current workspace.

')) - else: - display(FileLink(relfn, result_html_prefix=f'File output ("{html.escape(fn)}"): ')) - - def show(text): - display(text) - - - @register_cell_magic - def dss(line, cell): - if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_odd: - DSSPlotCtx.Text.Commands(cell) - else: - for line in cell.split('\n'): - DSSPlotCtx(line) - res = DSSPlotCtx.Text.Result - if res.endswith('.DSV'): - if _enabled and FilePath(res).exists(): - plot_dsv(res) - - DSSPlotCtx.AllowChangeDir = False -except: - def link_file(fn): - print(f'Output file: "{fn}"') - - def show(text): - print(text) - - - #FileLink('path_to_file/filename.extension') - -# import os -# import html -# import tqdm -# from tqdm.notebook import tqdm -# import IPython.display - include_3d = '2d' # '2d' (default), '3d' (prefer 3d), 'both' str_to_pq = { @@ -384,6 +336,44 @@ def show(text): MARKER_SEQ = (5, 15, 2, 8, 26, 36, 39, 19, 18) + +DSV_LINE_STYLES = { + 0: 'solid', + 1: 'dashed', + 2: 'dotted', + 3: 'dashdot', + 4: (0, (3, 5, 1, 5, 1, 5)), +} + +DSS_ITEMS = { + 'BoldLabel', + 'Caption', + 'Center', + 'ChartCaption', + 'Circle', + 'ClickOn', + 'Curve', + 'DataColor', + 'Draw', + 'FStyle', + 'KeepAspect', + 'KeyClass', + 'Label', + 'Line', + 'Marker', + 'Move', + 'NoScales', + 'PctRim', + 'Range', + 'Rect', + 'SetProp', + 'Text', + 'TxtAlign', + 'Width', + 'Xlabel', + 'Ylabel', +} + def get_marker_dict(dss_code): marker, size, fill = MARKER_MAP[dss_code] res = dict( @@ -404,6 +394,22 @@ def get_marker_dict(dss_code): def nodot(b): return b.split('.', 1)[0] +def unquote(field: str): + field = field.strip() + if field[0] == '"' and field[-1] == '"': + return field[1:-1] + + return field + +node_re = re.compile(r'(.*?)(\.[0-9])*$') + +def remove_nodes(bus): + match = node_re.match(bus) + return match.group(1) + +def _int_to_color(v: int): + return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) + class ToggleAdvancedTypes: def __init__(self, dss: IDSS, value: bool): self._value = value @@ -421,2287 +427,2182 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._dss.AdvancedTypes = self._previous -def dss_monitor_plot(DSS: IDSS, - *, - ObjectName: str = None, - Channels: List[int] = None, # TODO: allow channel names too - Bases: List[float] = None, - **kwargs: Unpack[PlotParams] -): - monitor = DSS.ActiveCircuit.Monitors - monitor.Name = ObjectName - data = monitor.AsMatrix() - if data is None or len(data) == 0: - raise ValueError("There is not data to plot in the monitor. Hint: check the solution mode, solve the circuit and retry.") - - channels = Channels - num_ch = monitor.NumChannels - channels = [ch for ch in channels if ch >= 1 and ch <= num_ch] - if len(channels) == 0: - raise IndexError("No valid channel numbers were specified.") - - bases = Bases - header = list(monitor.Header) - if len(monitor.dblHour) < len(monitor.dblFreq): - header.insert(0, 'Frequency') - header.insert(1, 'Harmonic') - xlabel = 'Frequency (Hz)' - h = data[:, 0] - else: - header.insert(0, 'Hour') - header.insert(1, 'Seconds') - h = data[:, 0] * 3600 + data[:, 1] - total_seconds = max(h) - min(h) - if total_seconds < 7200: - xlabel = 'Time (s)' - else: - xlabel = 'Time (h)' - h /= 3600 +class DSVHandler: + def __init__(self, fn: Union[str, FilePath]): + self.fn = fn + self.fig, self.ax = plt.subplots() + self.ax.get_xaxis().get_major_formatter().set_scientific(False) + self.ax.get_yaxis().get_major_formatter().set_scientific(False) + self.xy = [0.0, 0.0] + self.line_width = 1 + self.fig_caption = None + self.color = 'k' + self.key_class = None + self.no_scales = False + self.bold = True + self.txt_align = 'left' - separate = False - if separate: - fig, axs = plt.subplots(len(channels), sharex=True)#, figsize=(8, 9)) - icolor = -1 - for ax, base, ch in zip(axs, bases, channels): - ch += 1 - icolor += 1 - ax.plot(h, data[:, ch] / base, color=Colors[icolor % len(Colors)]) - ax.grid() - ax.set_ylabel(header[ch]) - else: - fig, ax = plt.subplots(1) - icolor = -1 - for base, ch in zip(bases, channels): - ch += 1 - icolor += 1 - ax.plot(h, data[:, ch] / base, label=header[ch], color=Colors[icolor % len(Colors)]) + def BoldLabel(self, param_str: str): + self.bold = int(param_str.strip()) != 0 - ax.grid() - ax.legend() - ax.set_ylabel('Mag') # Where "Mag" comes from? - - ax.set_title(ObjectName) - ax.set_xlabel(xlabel) - - -def dss_tshape_plot(DSS: IDSS, - *, - ObjectName: str = None, - Color1: str = None, - **kwargs: Unpack[PlotParams] -): - # There is no dedicated API yet but we can move to the Obj API - name = ObjectName - DSS.Text.Command = f'? tshape.{name}.temp' - p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - try: - DSS.Text.Command = f'? tshape.{name}.hour' - h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - except: - h = np.array([]) - try: - interval = f'? tshape.{name}.interval' # hours - interval = float(DSS.Text.Result) - except: - interval = 1 + def Caption(self, param_str: str): + self.fig_caption = param_str.strip().strip('"') + self.fig.canvas.manager.set_window_title(self.fig_caption) - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"TShape.{ObjectName}") - if not h.size: - h = interval * np.array(range(len(p))) + def ChartCaption(self, param_str: str): + self.ax.set_title(param_str.strip().strip('"')) - x_unit = 'h' - if h[-1] < 1: - h *= 3600 - x_unit = 's' - color1 = Color1 - ax.plot(h, p, color=color1, label="Price") - ax.set_title(f"TShape = {ObjectName}") - ax.set_xlabel(f'Time ({x_unit})') - ax.set_ylabel('Temperature') + def Center(self, param_str: str): + *int_params, text = param_str.split(',') + x, y, s = [int(v.strip()) for v in int_params] + text = text.strip().strip('"') + if '/_' in text: + text = text.replace('/_', '∠') + '°' + + if '->' in text: + text = text.replace('->', '→') + s = s * 1.5 + elif '<-' in text: + text = text.replace('<-', '←') + s = s * 1.5 + elif '^' in text: + text = text.replace('^', '↑') + s = s * 1.5 - ax.grid(ls='--') - plt.tight_layout() + self.ax.text(x, y, text, horizontalalignment='center', fontsize=s * 8 / 13.) + def Circle(self, param_str: str): + params = param_str.split(',') + x, y = float(params[0]), float(params[1]) + fc = _int_to_color(int(params[4])) + ec = _int_to_color(int(params[3])) + self.ax.scatter(x, y, marker='o', color=fc, edgecolors=ec, s=50, zorder=10, linewidths=0.5) -def dss_priceshape_plot(DSS: IDSS, - *, - ObjectName: str = None, - Color1: str = None, - **kwargs: Unpack[PlotParams] -): - # There is no dedicated API yet but we can move to the Obj API - name = ObjectName - DSS.Text.Command = f'? priceshape.{name}.price' - p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - try: - DSS.Text.Command = f'? priceshape.{name}.hour' - h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - except: - h = np.array([]) - try: - interval = f'? priceshape.{name}.interval' # hours - interval = float(DSS.Text.Result) - except: - interval = 1 + def ClickOn(self, param_str: str): + #TODO + pass - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"PriceShape.{ObjectName}") - if not h.size: - h = interval * np.array(range(len(p))) + def Curve(self, param_str: str): + *int_params, curve_name, rest = param_str.split(',', 7) + npts, color, width, style, curve_markers, curve_marker = [int(v.strip()) for v in int_params] + if curve_markers: + marker_dict = get_marker_dict(curve_marker) + else: + marker_dict = {} + + data = np.fromstring(rest, dtype=float, sep=',') + self.ax.plot(data[:npts], data[npts:], lw=width/2.0, label=curve_name.strip().strip('"'), color=_int_to_color(color), ls=DSV_LINE_STYLES[style], **marker_dict) + # self.ax.minorticks_on() - x_unit = 'h' - if h[-1] < 1: - h *= 3600 - x_unit = 's' - color1 = Color1 + def DataColor(self, param_str: str): + self.color = _int_to_color(int(param_str)) - ax.plot(h, p, color=color1, label="Price") - ax.set_title(f"PriceShape = {ObjectName}") - ax.set_xlabel(f'Time ({x_unit})') - ax.set_ylabel('Price') - ax.grid(ls='--') - plt.tight_layout() + def Draw(self, param_str: str): + if not self.no_scales: + # Currently not used since Move/Draw is emulated with axhline + return + + x0, y0 = self.xy + x1, y1 = [float(v.strip().strip('"')) for v in param_str.split(',')] + self.ax.plot([x0, x1], [y0, y1], color=self.color, lw=self.line_width/2.0) -def dss_loadshape_plot(DSS: IDSS, - *, - ObjectName: str = None, - Color1: str = None, - Color2: str = None, - **kwargs: Unpack[PlotParams] -): -# pprint(kwargs) - - ls = DSS.ActiveCircuit.LoadShapes - ls.Name = ObjectName - h = asarray(ls.TimeArray) - p = asarray(ls.Pmult) - q = asarray(ls.Qmult) - - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"LoadShape.{ObjectName}") + def FStyle(self, param_str: str): + fstyle = int(param_str.strip().strip('"')) + # if fstyle != 0: + # print('Unhandled font style:', fstyle) - if not h.size or h is None or len(h) != len(p): - h = ls.HrInterval * np.array(range(len(p))) - x_unit = 'h' - if h[-1] < 1: - h *= 3600 - x_unit = 's' + def KeepAspect(self, param_str: str): + try: + v = int(param_str.strip().strip('"')) + except: + v = 1 + + if v: + self.ax.set_aspect('equal', 'datalim') + else: + self.ax.set_aspect('auto') - color1 = Color1 - color2 = Color2 - ax.plot(h, p, color=color1, label="Pmult") - if q.size == p.size: - ax.plot(h, q, color=color2, label="Qmult") + def KeyClass(self, param_str: str): + self.key_class = int(param_str.strip()) - ax.set_title(f"LoadShape = {ObjectName}") - ax.set_xlabel(f'Time ({x_unit})') - if ls.UseActual: - if q.size == p.size: - ax.set_ylabel('kW, kvar') - else: - ax.set_ylabel('kW') - else: - ax.set_ylabel('p.u.') - ax.grid(ls='--') - if q.size == p.size: - ax.legend() - plt.tight_layout() + def Label(self, param_str: str): + *int_params, text, _ = param_str.split(',') + x, y, color_int = [int(v.strip()) for v in int_params] + color = _int_to_color(color_int) + text = text.strip().strip('"') + self.ax.text(x, y, text, + horizontalalignment='center', + fontsize=10 * 8 / 13., + color=color, + backgroundcolor='white', + weight='bold' if self.bold else 'normal' + ) -node_re = re.compile(r'(.*?)(\.[0-9])*$') + def Line(self, param_str: str): + #TODO: use LineCollection -def remove_nodes(bus): - match = node_re.match(bus) - return match.group(1) + *str_params, rest = param_str.split(',', 3) + line_name, bus1, bus2 = [v.strip().strip('"') for v in str_params] + *int_params, rest = rest.split(',', 4) + offset, data_count, num_cust, total_cust = [int(v) for v in int_params] + *dbl_params, rest = rest.split(',', 6) + kv, dist, x1, y1, x2, y2 = [float(v) for v in dbl_params] + int_params = rest.split(',') + #TODO: markers + color, width, style, dots, mark_center, center_marker_code, node_marker_code, node_marker_size = [int(v) for v in int_params] -# def remove_nodes2(bus): - # dot_pos = bus.find('.') - # if dot_pos == -1: - # return bus + if dots: + node_marker_dict = get_marker_dict(node_marker_code) + node_marker_dict['markersize'] *= max(1, np.sqrt(node_marker_size) - 1) * node_marker_dict['markersize'] / 7.0 + else: + node_marker_dict = {} - # return bus[:dot_pos] - -def get_branch_data(DSS: IDSS, - branch_objects: DSSIterable, - bus_coords: Dict[str, Tuple[float, float, float]], - do_values=pqNone, - do_switches=False, - idxs=None, - single_ph_line_style: int = 1, - three_ph_line_style: int = 1 -): - line_count = branch_objects.Count if not idxs else len(idxs) - lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) - lines.fill(np.nan) - values = np.empty(shape=(line_count, ), dtype=np.float64) - values.fill(np.nan) - lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) - - element = DSS.ActiveCircuit.ActiveCktElement - - if do_switches: - switch_idxs = [] - isolated_idxs = [] - try: - element.IsIsolated - has_is_isolated = True - except: - has_is_isolated = False - isolated_names = set(name.lower() for name in DSS.ActiveCircuit.Topology.AllIsolatedBranches if name) - - extra = [switch_idxs, isolated_idxs] - else: - extra = [] - # def get_buses_line(l): - # b1 = remove_nodes(l.Bus1) - # b2 = remove_nodes(l.Bus2) - - offset = 0 - skip = set() - - # norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu - # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu - # emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu - # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu - - vbs = None - if do_values == pqCurrent: - # Currently the same as pqCapacity to match the OpenDSS impl.; the correct would be: - #max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllMaxCurrents(True))) - try: - max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) - except: - max_currents = {} - elem = DSS.ActiveCircuit.ActiveCktElement - for _ in DSS.ActiveCircuit.PDElements: - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - norm_amps = elem.NormalAmps - max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 - - elif do_values == pqCapacity: - try: - capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) - except: - max_currents = {} - elem = DSS.ActiveCircuit.ActiveCktElement - for _ in DSS.ActiveCircuit.PDElements: - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - norm_amps = elem.NormalAmps - max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 - - elif do_values == pqVoltage: - node_volts = dict(zip(DSS.ActiveCircuit.AllNodeNames, asarray(DSS.ActiveCircuit.AllBusVmag) * 1e-3)) - vbs = np.empty(shape=(line_count, ), dtype=np.float64) - vbs.fill(0) - extra.append(vbs) - - if idxs: - l = branch_objects - for idx in idxs: - l.idx = idx - buses = element.BusNames - b1 = remove_nodes(buses[0]) - b2 = remove_nodes(buses[1]) - - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None: - skip.add(idx) - continue - - lines[offset, 0] = fr - lines[offset, 1] = to - offset += 1 - - if do_values == pqNone: - return lines[:offset] - - offset = 0 - for idx in idxs: - if idx in skip: - continue - - l.idx = idx - - if do_values == pqPower: - values[offset] = np.abs(element.TotalPowers[0]) - elif do_values == pqLosses: - values[offset] = abs(element.Losses[0]) / l.Length - elif do_values == pqVoltage: - b2name = nodot(l.Bus2) - b = DSS.ActiveCircuit.Buses[b2name] - vb = b.kVBase - vbs[offset] = vb - value = 1e30 - if vb > 0: - for n in b.Nodes: - if n > 0 and n <= 3: - value = min(value, node_volts[f'{b2name}.{n}'] / vb) - - values[offset] = value - elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.NaN) - elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.NaN) - - offset += 1 + self.ax.plot([x1, x2], [y1, y2], color=_int_to_color(color), lw=width / 2.0, ls=DSV_LINE_STYLES[style], solid_capstyle='round', **node_marker_dict) - return lines[:offset], values[:offset] - - else: - for i, l in enumerate(branch_objects): - buses = element.BusNames - b1 = remove_nodes(buses[0]) - b2 = remove_nodes(buses[1]) - - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None or not element.Enabled: - skip.add(i) - continue + if mark_center: + center_marker_dict = get_marker_dict(center_marker_code) + self.ax.scatter((x1 + x2) / 2, (y1 + y2) / 2, color=_int_to_color(color), **center_marker_dict) - if do_switches: - if ((has_is_isolated and element.IsIsolated) or - ((not has_is_isolated) and (element.Name.lower() in isolated_names))): - isolated_idxs.append(offset) - if l.IsSwitch: - #skip.add(i) - switch_idxs.append(offset) - #continue + def Marker(self, param_str: str): + params = param_str.split(',') + x, y = float(params[0]), float(params[1]) + c, symbol, marker_size = [int(v) for v in params[2:]] + marker_dict = get_marker_dict(symbol) + marker_dict['markersize'] *= max(1, np.sqrt(marker_size) - 1) * marker_dict['markersize'] / 7.0 + self.ax.plot(x, y, ls=None, color=_int_to_color(c), **marker_dict) - lines[offset, 0] = fr - lines[offset, 1] = to - - offset += 1 - - if do_values == pqNone: - return [lines[:offset], None, None] + extra - - offset = 0 - - for i, l in enumerate(branch_objects): - if i in skip: - continue - - if do_values == pqPower: - values[offset] = np.abs(element.TotalPowers[0]) - elif do_values == pqLosses: - values[offset] = abs(element.Losses[0]) / l.Length - elif do_values == pqVoltage: - b2name = nodot(l.Bus2) - b = DSS.ActiveCircuit.Buses[b2name] - vb = b.kVBase - vbs[offset] = vb - value = 1e30 - - if l.Phases < 3: - lines_styles[offset] = 1 - - if vb > 0: - for n in b.Nodes: - if n > 0 and n <= 3: - value = min(value, node_volts[f'{b2name}.{n}'] / vb) - - values[offset] = value - elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.NaN) - elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.NaN) - - lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style - offset += 1 - - return [lines[:offset], values[:offset], lines_styles[:offset]] + extra - - -def get_point_data(DSS: IDSS, - point_objects: Union[str, Iterable], - bus_coords: Dict[str, Tuple[float, float, float]], - do_values: bool = False -): - if isinstance(point_objects, str): - cls = point_objects - DSS.SetActiveClass(cls) - point_objects = DSS.ActiveClass - - point_count = point_objects.Count - - points = np.empty(shape=(point_count, 2), dtype=np.float64) - values = np.empty(shape=(point_count, ), dtype=np.float64) - - offset = 0 - skip = set() - element = DSS.ActiveCircuit.ActiveCktElement - for i, _ in enumerate(point_objects): - buses = element.BusNames - all_coords = [] - buses = [remove_nodes(b) for b in buses] - all_coords = [c for c in (bus_coords.get(b) for b in buses) if c] - - if not all_coords: - skip.add(i) - continue - coords = tuple(sum(c) / len(all_coords) for c in zip(*all_coords)) - - points[offset] = coords - offset += 1 - - if not do_values: - return points[:offset] - - offset = 0 - for i, _ in enumerate(point_objects): - if i in skip: - continue - - values[offset] = np.abs(element.TotalPowers[0]) - offset += 1 - - return points[:offset], values[:offset] + def Move(self, param_str: str): + x, y = [float(v.strip().strip('"')) for v in param_str.split(',')] + if self.no_scales: + self.xy = [x, y] + else: + self.ax.axhline(y, color=self.color, lw=self.line_width / 2.0) -def dss_profile_plot(DSS: IDSS, - *, - PhasesToPlot: int = None, - ProfileScale: float = None, - **kwargs: Unpack[PlotParams] -): - if len(DSS.ActiveCircuit.Meters) == 0: - raise RuntimeError(f"An EnergyMeter is required to use 'plot profile'") - - vmin = DSS.ActiveCircuit.Settings.NormVminpu - vmax = DSS.ActiveCircuit.Settings.NormVmaxpu - if ProfileScale == '120kft': - xlabel = 'Distance (kft)' - ylabel = '120 Base Voltage' - DenomLN = 1.0 / 120.0 - # DenomLL = 1.732 / 120.0 - LenScale = 3.2809 - # RangeScale = 120.0 - else: - xlabel = 'Distance (km)' - ylabel = 'p.u. Voltage' - DenomLN = 1.0 - # DenomLL = 1.732 - LenScale = 1.0 - # RangeScale = 1.0 - - busnode_to_index = {(bn.rsplit('.', 1)[0], int(bn.rsplit('.', 1)[1])): num for (num, bn) in enumerate(DSS.ActiveCircuit.AllNodeNames)} - bus_to_kvbase = {b.Name: b.kVBase for b in DSS.ActiveCircuit.Buses} - puV = asarray(DSS.ActiveCircuit.AllBusVmagPu) / DenomLN - distances = {name: d for (name, d) in zip(DSS.ActiveCircuit.AllBusNames, asarray(DSS.ActiveCircuit.AllBusDistances) * LenScale)} - linewidths = [] - segments = [] - colors = [] - linestyles = [] - seg_phases = [] - pri_only = (PhasesToPlot == DSSPlotPhases.PROFILEALLPRI) - if PhasesToPlot in [DSSPlotPhases.PROFILEALL, DSSPlotPhases.PROFILEALLPRI, DSSPlotPhases.PROFILE3PH]: - phases = (1, 2, 3) - else: - phases = PhasesToPlot - try: - _ = iter(phases) - except: - phases = [phases] - - for em in DSS.ActiveCircuit.Meters: - branch_names = em.AllBranchesInZone - br: str - for br in branch_names: - if not br.startswith('Line.'): - continue + def NoScales(self, param_str: str): + self.no_scales = True + self.ax.get_xaxis().set_visible(False) + self.ax.get_yaxis().set_visible(False) - ls = '-' - lw = 2 - DSS.ActiveCircuit.Lines.Name = br[len('Line.'):] + def PctRim(self, param_str: str): + self.ax.margins(float(param_str) / 100.0) - if DSSPlotPhases.PROFILE3PH == PhasesToPlot and DSS.ActiveCircuit.Lines.Phases < 3: - continue - bus1 = nodot(DSS.ActiveCircuit.Lines.Bus1) - bus2 = nodot(DSS.ActiveCircuit.Lines.Bus2) + def Range(self, param_str: str): + pass - # Plot all phases present (between 1 and 3) - for iphs in phases: - try: - b1n_idx = busnode_to_index[(bus1, iphs)] - b2n_idx = busnode_to_index[(bus2, iphs)] - except: - continue - if bus_to_kvbase[bus1] < 1.0: - if pri_only: - continue - ls = ':' - lw = 1 - - segments.append(((distances[bus1], puV[b1n_idx]), (distances[bus2], puV[b2n_idx]))) - colors.append(Colors[iphs - 1]) - seg_phases.append(iphs) - linestyles.append(ls) - linewidths.append(lw) - #TODO: NodeMarkerCode, NodeMarkerWidth - - if include_3d in ('both', '2d'): - fig = plt.figure()#figsize=(9, 5)) - ax = fig.add_subplot(1, 1, 1) - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): - ax.set_title('L-L Voltage Profile') - else: - ax.set_title('L-N Voltage Profile') - + def Rect(self, param_str: str): + left, bottom, right, top = [int(v) for v in param_str.split(',')] + r = patches.Rectangle((left, bottom), right - left, top - bottom, fill=True, ec='k', fc='#c0c0c0') + self.ax.add_patch(r) - lc = LineCollection(segments, linewidth=linewidths, colors=colors, linestyles=linestyles) - # ax.set_title('{}:{}, max: {:3g}'.format(DSS.ActiveCircuit.Name, quantity, quantity_max_value)) - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - ax.add_collection(lc) - ax.autoscale_view() - ax.axhline(vmin, color='darkred', ls='-', lw=3) - ax.axhline(vmax, color='darkred', ls='-', lw=3) - ax.grid(ls='--') - plt.tight_layout() - - if include_3d in ('both', '3d'): - fig2 = plt.figure()#figsize=(7, 7)) - ax2 = fig2.add_subplot(1, 1, 1, projection='3d') - ax2.set_xlabel(xlabel) - ax2.set_ylabel(ylabel) - if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): - ax2.set_title('L-L Voltage Profile') + def SetProp(self, param_str: str): + if int(param_str.rsplit(',', 1)[-1]) != 0: + self.ax.grid(which='both', ls='--') else: - ax2.set_title('L-N Voltage Profile') - - segments_3d = [ - [(*p, ph) for p in seg] for seg, ph in zip(segments, seg_phases) - ] - rseg = np.ravel(segments) - max_x = np.max(rseg[::2]) - max_y = np.max(rseg[1::2]) - min_y = np.min(rseg[1::2]) - lc3d = Line3DCollection(segments_3d, colors=colors, linestyles=linestyles) - ax2.add_collection(lc3d) - ax2.set_xlabel(xlabel) - ax2.set_ylabel(ylabel) - ax2.set_zlabel('Phase') - xl = [0, max_x] - yl = [min(min_y, vmin) - 0.05, min(max_y, vmax) + 0.05] - maxph = np.max(seg_phases) + 1 - ax2.set_xlim(xl) - ax2.set_ylim(yl) - ax2.set_zlim(0, maxph) - ax2.plot_surface( - np.array([xl, xl]), - np.array([[vmax, vmax]] * 2), - np.array([[0, 0], [maxph, maxph]]), - color='k', - alpha=0.5 - ) - ax2.plot_surface( - np.array([xl, xl]), - np.array([[vmin, vmin]] * 2), - np.array([[0, 0], [maxph, maxph]]), - color='k', - alpha=0.5 - ) - ax2.autoscale_view() - - - -def _get_gic_line_data_altdss( - altdss: IAltDSS, - bus_coords: Dict[str, Tuple[float, float, float]], - single_ph_line_style: int = 1, - three_ph_line_style: int = 1 -): - branch_objects = altdss.GICLine - line_count = len(branch_objects)# if not idxs else len(idxs) - lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) - lines.fill(np.nan) - values = np.empty(shape=(line_count, ), dtype=np.float64) - values.fill(np.nan) - lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) - offset = 0 - # skip = set() - - # GIC lines are not exposed nicely in the classic API, so we'll use the new Obj API - for gic_line in altdss.GICLine: - if not gic_line.enabled: - continue - - b1 = remove_nodes(gic_line.bus1) - b2 = remove_nodes(gic_line.bus2) - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None: - # skip.add(idx) - continue - - lines[offset, 0] = fr - lines[offset, 1] = to + self.ax.grid(False) - lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style - values[offset] = gic_line.MaxCurrent(1) - offset += 1 - return lines[:offset], values[:offset], lines_styles[:offset] + def Text(self, param_str: str): + *int_params, text = param_str.split(',') + x, y, c, s = [int(v.strip()) for v in int_params] + text = text.strip().strip('"') + self.ax.text(x, y, text, ha=self.txt_align, va='center', fontsize=s * 10 / 13.) -def get_gic_line_data(DSS: IDSS, - bus_coords: Dict[str, Tuple[float, float]], - single_ph_line_style: int = 1, - three_ph_line_style: int = 1 -): - try: - return _get_gic_line_data_altdss( - DSS.to_altdss(), - bus_coords, - single_ph_line_style=single_ph_line_style, - three_ph_line_style=three_ph_line_style - ) - except: - pass + def TxtAlign(self, param_str: str): + v = int(param_str) + if v == 1: + self.txt_align = 'left' + return - # Fallback for Oddie and COM - DSS.ActiveCircuit.SetActiveClass('GICLine') - aclass = DSS.ActiveCircuit.ActiveClass - line_count = aclass.Count# if not idxs else len(idxs) - lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) - lines.fill(np.nan) - values = np.empty(shape=(line_count, ), dtype=np.float64) - values.fill(np.nan) - lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) - offset = 0 - # skip = set() - - # GIC lines are not exposed nicely in the classic API - elem = DSS.ActiveCircuit.ActiveCktElement - idx = aclass.First - while idx != 0: - buses = elem.BusNames - b1 = remove_nodes(buses[0]) - b2 = remove_nodes(buses[1]) - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None: - # skip.add(idx) - continue + if v == 2: + self.txt_align = 'center' + return - lines[offset, 0] = fr - lines[offset, 1] = to - - lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - values[offset] = max_current - offset += 1 - - return lines[:offset], values[:offset], lines_styles[:offset] - - -def dss_circuit_plot(DSS: IDSS, - *, - fig=None, - ax=None, - is3d=False, - Quantity: str = None, - Dots: bool = False, - Color1: str = None, - Color2: str = None, - Color3: str = None, - SinglePhLineStyle: int = None, - ThreePhLineStyle: int = None, - MaxLineThickness: float = None, - BusMarkers: List[BusMarker] = None, - Labels: bool = None, - Markers: ObjMarkers = None, - MaxScale: float = None, - MaxScaleIsSpecified: bool = None, - **kwargs: Unpack[PlotParams] -): - if not MaxScaleIsSpecified: - MaxScale = None - - quantity = str_to_pq.get(Quantity, pqNone) - dots = Dots - color1 = Color1 - color2 = Color2 - color3 = Color3 - single_ph_line_style = SinglePhLineStyle - three_ph_line_style = ThreePhLineStyle - max_lw = MaxLineThickness - bus_markers = BusMarkers or [] - do_labels = Labels - - norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu - # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu - emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu - # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu - - # bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if (b.x, b.y) != (0.0, 0.0)) - bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if b.Coorddefined) - - if fig is None: - fig = plt.figure()#figsize=(8, 7)) - - given_ax = ax is not None - if not given_ax: - ax = plt.gca() - else: - plt.sca(ax) - - if not is3d: - ax.set_aspect('equal', 'datalim') + if v == 3: + self.txt_align = 'right' + return - lines_lines, lines_values, lines_styles, switch_idxs, isolated_idxs, *extra = get_branch_data( - DSS, - DSS.ActiveCircuit.Lines, - bus_coords, - do_values=quantity, - do_switches=True, - single_ph_line_style=single_ph_line_style, - three_ph_line_style=three_ph_line_style - ) - if isolated_idxs: - line_idx = isolated_idxs - if not is3d: - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#ff00ff', capstyle='round')) + def Width(self, param_str: str): + self.line_width = int(param_str.strip().strip('"')) - if switch_idxs: - line_idx = switch_idxs - if not is3d: - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#000000', capstyle='round')) - - switch_idxs = set(switch_idxs) - isolated_idxs = set(isolated_idxs) - #lc_lines = LineCollection(lines_lines, linewidths=0.5, color=color1)# + 3 * lines_values / np.max(lines_values), linestyle='solid', color=color1) - quantity_max_value = MaxScale if MaxScale is not None else 0.0 - - quantity_suffix = '' - - if lines_lines is not None and len(lines_lines) > 0: - if quantity in (pqVoltage,): - colors = [] - for v in lines_values: - if v > norm_min_volts or np.isnan(v): - colors.append(color1) - elif v > emerg_min_volts: - colors.append(color2) - else: - colors.append(color3) - - - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] - if not is3d: - edgecolors = [colors[i] for i in line_idx] - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=edgecolors, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) + + def Xlabel(self, param_str: str): + self.ax.set_xlabel(param_str.strip().strip('"')) - # if is3d: - # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) - # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) - # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) + + def Ylabel(self, param_str: str): + self.ax.set_ylabel(param_str.strip().strip('"')) + + def parse(self): + with open(self.fn, 'r') as f: + for l in f: + l = l.strip() + if not l: + continue - quantity_max_value = 0 - elif quantity in (pqLosses,): - - if quantity_max_value == 0: - # quantity_max_value = max(lines_values) * 1e-3 - # For compatibility with the official version, loop through all lines instead - # of the actual plotted lines - element = DSS.ActiveCircuit.ActiveCktElement - quantity_max_value = max( - abs(element.Losses[0] / line.Length) - for line in DSS.ActiveCircuit.Lines - if element.Enabled - ) * 0.001 - - lines_values = np.clip(3 * 1e-3 * lines_values / quantity_max_value, 0.5, max_lw) - if not is3d: - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] - # edgecolors = [colors[i] for i in line_idx] - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + item_name, *rest = l.split(',', 1) + item_name = item_name.strip() + if item_name not in DSS_ITEMS: + raise NotImplemented(f'"{item_name}" DSV item is not implemented') - elif quantity in (pqCurrent, pqCapacity): - line_idx = [i for i in range(lines_lines.shape[0]) if i not in isolated_idxs and i not in switch_idxs] - colors = [color3 if v > 100 and not np.isnan(v) else color1 for v in lines_values[line_idx]] + # print(item, repr(rest)[:100]) + getattr(self, item_name)(rest[0] if rest else '') # let the exception propagate on error - if quantity_max_value == 0: - quantity_max_value = max(lines_values) + if _do_show: + self.fig.show() + else: + return self.fig, self.ax - lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) - if not is3d: - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle='-', color=colors, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) - elif quantity != pqNone: - if quantity == pqPower: - quantity_suffix = ' kW' - if quantity_max_value == 0: - #lines_values *= 1e-3 +class DSSMPLPlotter: + def __init__(self, dss: IDSS): + self.dss = dss - # For compatibility with the official version, loop through all lines instead - # of the actual plotted lines - element = DSS.ActiveCircuit.ActiveCktElement - - quantity_max_value = max( - element.TotalPowers[0] - for _ in DSS.ActiveCircuit.Lines - if element.Enabled - ) #* 0.001 + def dss_monitor_plot(DSS: IDSS, + *, + ObjectName: str = None, + Channels: List[int] = None, # TODO: allow channel names too + Bases: List[float] = None, + **kwargs: Unpack[PlotParams] + ): + monitor = DSS.ActiveCircuit.Monitors + monitor.Name = ObjectName + data = monitor.AsMatrix() + if data is None or len(data) == 0: + raise ValueError("There is not data to plot in the monitor. Hint: check the solution mode, solve the circuit and retry.") + + channels = Channels + num_ch = monitor.NumChannels + channels = [ch for ch in channels if ch >= 1 and ch <= num_ch] + if len(channels) == 0: + raise IndexError("No valid channel numbers were specified.") + + bases = Bases + header = list(monitor.Header) + if len(monitor.dblHour) < len(monitor.dblFreq): + header.insert(0, 'Frequency') + header.insert(1, 'Harmonic') + xlabel = 'Frequency (Hz)' + h = data[:, 0] + else: + header.insert(0, 'Hour') + header.insert(1, 'Seconds') + h = data[:, 0] * 3600 + data[:, 1] + total_seconds = max(h) - min(h) + if total_seconds < 7200: + xlabel = 'Time (s)' else: - #TODO:may need workaround about GeneralPlotQuantity - quantity_max_value = max(lines_values) + xlabel = 'Time (h)' + h /= 3600 + + separate = False + if separate: + fig, axs = plt.subplots(len(channels), sharex=True)#, figsize=(8, 9)) + icolor = -1 + for ax, base, ch in zip(axs, bases, channels): + ch += 1 + icolor += 1 + ax.plot(h, data[:, ch] / base, color=Colors[icolor % len(Colors)]) + ax.grid() + ax.set_ylabel(header[ch]) - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] - if not is3d: - ax.add_collection(LineCollection( - lines_lines[line_idx, :], - linewidths=np.clip(0.5 + 3 * lines_values[line_idx] / quantity_max_value, 0.5, max_lw), - linestyle=LINES_STYLE_CODE.get(ls, 'solid'), - color=color1, - capstyle='round' - )) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) else: - #TODO: handle 1 and 3 phase, etc.? - if not is3d: - ax.add_collection(LineCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) - # else: - # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) - # ax.set_xlim(np.min(lines_lines[:, :, 0]), np.max(lines_lines[:, :, 0])) - # ax.set_ylim(np.min(lines_lines[:, :, 1]), np.max(lines_lines[:, :, 1])) + fig, ax = plt.subplots(1) + icolor = -1 + for base, ch in zip(bases, channels): + ch += 1 + icolor += 1 + ax.plot(h, data[:, ch] / base, label=header[ch], color=Colors[icolor % len(Colors)]) - transformers_lines, *_ = get_branch_data(DSS, DSS.ActiveCircuit.Transformers, bus_coords) + ax.grid() + ax.legend() + ax.set_ylabel('Mag') # Where "Mag" comes from? - if not is3d: - lc_transformers = LineCollection(transformers_lines, linewidth=3, linestyle='solid', color='gray') - ax.add_collection(lc_transformers) + ax.set_title(ObjectName) + ax.set_xlabel(xlabel) - lines_lines, lines_values, lines_styles, *_ = get_gic_line_data(DSS, bus_coords, single_ph_line_style=single_ph_line_style, three_ph_line_style=three_ph_line_style) - if len(lines_lines) != 0: - if quantity_max_value == 0: - quantity_max_value = max(lines_values) - lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls] - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + def dss_tshape_plot(self, + *, + ObjectName: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] + ): + # There is no dedicated API yet but we can move to the Obj API + name = ObjectName + DSS = self.DSS + DSS.Text.Command = f'? tshape.{name}.temp' + p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + try: + DSS.Text.Command = f'? tshape.{name}.hour' + h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + except: + h = np.array([]) + try: + interval = f'? tshape.{name}.interval' # hours + interval = float(DSS.Text.Result) + except: + interval = 1 + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"TShape.{ObjectName}") - # 'Daisysize' - # 'Markercode', 'Nodewidth' # NodeMarkerCode - - branch_marker_options = [ - ('MarkSwitches', 'SwitchMarkerCode', None, DSS.ActiveCircuit.Lines, switch_idxs), - ('MarkFuses', 'FuseMarkerCode', 'FuseMarkerSize', DSS.ActiveCircuit.Fuses, None), - ('MarkRegulators', 'RegMarkerCode', 'RegMarkerSize', DSS.ActiveCircuit.RegControls, None), - ('MarkRelays', 'RelayMarkerCode', 'RelayMarkerSize', DSS.ActiveCircuit.Relays, None), - ('MarkReclosers', 'RecloserMarkerCode', 'RecloserMarkerSize', DSS.ActiveCircuit.Reclosers, None) - ] - - point_marker_options = [ - ('MarkTransformers', 'TransMarkerCode', 'TransMarkerSize', DSS.ActiveCircuit.Transformers, None), - ('MarkCapacitors', 'CapMarkerCode', 'CapMarkerSize', DSS.ActiveCircuit.Capacitors, None), - ('MarkPVSystems', 'PVMarkerCode', 'PVMarkerSize', DSS.ActiveCircuit.PVSystems, None), - ('MarkStorage', 'StoreMarkerCode', 'StoreMarkerSize', 'Storage', None), - ] - - pmarkers = Markers - if pmarkers is not None: - for (mark_opt, code_opt, size_opt, objs, idxs) in branch_marker_options: - # print(mark_opt, pmarkers[mark_opt]) - if not pmarkers[mark_opt]: - continue - - marker_code = pmarkers[code_opt] - marker_size = pmarkers[size_opt] - #TODO: use marker_size? - marker_dict = get_marker_dict(marker_code) - if mark_opt == 'MarkRegulators': - for obj in objs: - DSS.ActiveCircuit.Transformers.Name = obj.Transformer - bus = remove_nodes(DSS.ActiveCircuit.ActiveCktElement.BusNames[obj.Winding - 1]) - coords = bus_coords.get(bus) - if coords is None: - continue - ax.plot(*coords, color='red', **marker_dict) - - else: - #TODO? branch_lines = get_branch_data(DSS, objs, bus_coords, idxs=idxs) - pass - - - for (mark_opt, code_opt, size_opt, objs, idxs) in point_marker_options: - if not pmarkers[mark_opt]: - continue - - marker_code = pmarkers[code_opt] - marker_size = pmarkers[size_opt] - - points = get_point_data(DSS, objs, bus_coords) - - # if marker_code not in MARKER_MAP: - #marker_code = 25 - - marker_dict = get_marker_dict(marker_code) - #marker_dict['markersize'] *= (marker_size / 2.0)**2 - marker_dict['markersize'] *= (marker_size / 1.2)**2 - - #marker_dict['marker'] = marker_dict['marker'].vertices - #marker_dict.pop('markersize') - #marker_dict.pop('markerfacecolor') - # print(mark_opt, marker_dict['marker']) - # pprint(marker_dict) - ax.plot(points[:, 0], points[:, 1], ls='', color='red', **marker_dict) - #ax.plot(points[:, 0], points[:, 1], color='red', ls='', marker=6, alpha=1) - - for bus_marker in bus_markers: - name = bus_marker['Name'] - bus = DSS.ActiveCircuit.Buses[name] - if not bus.Coorddefined: - raise RuntimeError('Bus markers: coordinates are not defined for bus "{name}"') - - marker_dict = get_marker_dict(bus_marker['Code']) - marker_size = bus_marker['Size'] - marker_dict['markersize'] *= (marker_size / 6) - ax.plot(bus.x, bus.y, ls='', color=bus_marker['Color'], **marker_dict) - - - ax.set_xlabel('X') - ax.set_ylabel('Y') - if not given_ax: - if quantity != pqNone: - ax.set_title('{}:{}, max={:g}{}'.format(DSS.ActiveCircuit.Name.upper(), quantity_str[quantity], quantity_max_value, quantity_suffix)) - ax.autoscale_view() - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - plt.tight_layout() - - if do_labels: - coords_to_names = {} - for name, coords in bus_coords.items(): - prev = coords_to_names.get(coords) - if prev: - coords_to_names[coords] = prev + ',' + name - else: - coords_to_names[coords] = name + if not h.size: + h = interval * np.array(range(len(p))) - for coords, name in coords_to_names.items(): - ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) + x_unit = 'h' + if h[-1] < 1: + h *= 3600 + x_unit = 's' - + color1 = Color1 + ax.plot(h, p, color=color1, label="Price") + ax.set_title(f"TShape = {ObjectName}") + ax.set_xlabel(f'Time ({x_unit})') + ax.set_ylabel('Temperature') -def dss_scatter_plot(DSS: IDSS, - **kwargs: Unpack[PlotParams] -): - x = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) - y = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) - vcomplex = np.empty(shape=(DSS.ActiveCircuit.NumBuses, 3), dtype=complex) - x.fill(np.nan) - y.fill(np.nan) - vcomplex.fill(np.nan) - for idx, b in enumerate(DSS.ActiveCircuit.Buses): - if not b.Coorddefined: - continue - - x[idx] = b.x - y[idx] = b.y - vnodes = asarray(b.puVoltages).view(dtype=complex) - nnodes = min(3, len(vnodes)) - vcomplex[idx, :nnodes] = vnodes[:nnodes] - - vabs = np.abs(vcomplex) - del vcomplex - with suppress_warnings(): - vmean = np.mean(vabs, axis=1, where=np.isfinite(vabs)) - - if include_3d in ('both', '2d'): - fig, ax = plt.subplots(1, 1, constrained_layout=True)#, figsize=(8, 7)) - dss_circuit_plot(DSS, fig=fig, ax=ax, Color1='k') - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - sc = ax.scatter(x, y, c=vmean) - fig.colorbar(sc, label='V1 (pu)') - ax.set_title('{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude')) - - if include_3d in ('both', '3d'): - bus_coords = {} - for idx, b in enumerate(DSS.ActiveCircuit.Buses): - if b.Coorddefined: - bus_coords[b.Name] = (b.x, b.y, vmean[idx]) - - fig = plt.figure()#figsize=(7, 7)) - ax = fig.add_subplot(projection='3d') - dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True, Color1='k') - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - - # if is3d: - # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) - # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) - # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) - - sc = ax.scatter(x, y, vmean, c='k', s=2) - - segs = [] - el = DSS.ActiveCircuit.ActiveCktElement - for pd in DSS.ActiveCircuit.PDElements: - buses = el.BusNames - if len(buses) != 2: - continue + ax.grid(ls='--') + fig.set_layout_engine(layout='tight') - seg = [] - for b in buses: - c = bus_coords.get(nodot(b), None) - if c is not None: - seg.append(c) - - if len(seg) == 2: - segs.append(seg) - - segs = np.array(segs, dtype=float) - seg_v = (segs[:, 0, 2] + segs[:, 1, 2]) / 2 - lc3d = Line3DCollection(segs) - ax.add_collection(lc3d) - lc3d.set_array(seg_v) - #fig.colorbar(sc, label='V1 (pu)') - ax.set_title('{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude')) - - -def dss_visualize_plot(DSS: IDSS, - *, - Quantity: str = None, - ElementType: str = None, - ElementName: str = None, - **kwargs: Unpack[PlotParams] -): - XMAX = 300 - #pprint(kwargs) - quantity = Quantity - - # Fix for backend v0.13.1 - quantity = { - 'Power': 'Powers', - 'Current': 'Currents', - 'Voltage': 'Voltages', - }.get(quantity, quantity) - - element = DSS.ActiveCircuit.ActiveCktElement - etype, ename = ElementType, ElementName - nconds = element.NumConductors - # nphases = element.NumPhases - buses = element.BusNames[:2] # max 2 terminals - vbases = [max(1, 1000 * DSS.ActiveCircuit.Buses[nodot(b)].kVBase) for b in buses] - - # assert DSS.ActiveCircuit.ActiveCktElement.Name == ElementType + '.' + ElementName - fig, ax = plt.subplots(1, gridspec_kw=dict(left=0.05, right=0.95, bottom=0.05, top=0.92))#, figsize=(8.6, 7)) - ax.get_xaxis().set_visible(False) - ax.get_yaxis().set_visible(False) - ax.grid(False) - - y = 20 + 10 * nconds - box_xy0 = np.array([100, 10]) - box_xy1 = np.array([XMAX - 100, y]) - box_wh = box_xy1 - box_xy0 - middle_box = patches.Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') - ax.text(XMAX / 2, 10 + (y - 10) / 2, f'{etype}.{ename.upper()}', ha='center', va='center', fontweight='bold', rotation='vertical') - ax.add_patch(middle_box) - ax.plot([0, 300], [0, 0], color='gray', lw=7) - - ax.plot([-5] * 2, [5, y - 5], color='k', lw=7) - ax.text(25, y, buses[0].upper(), ha='left') - if len(buses) > 1: - ax.plot([XMAX + 5] * 2, [5, y - 5], color='k', lw=7) - ax.text(XMAX - 25, y, buses[1].upper(), ha='right') - - voltage = (quantity == 'Voltages') - - if quantity == 'Powers': - values = 1e-3 * (asarray(element.Voltages).view(dtype=complex) * np.conj(asarray(element.Currents).view(dtype=complex))) - unit = 'kVA' - elif voltage: - values = asarray(element.Voltages).view(dtype=complex) - unit = 'pu' - elif quantity == 'Currents': - values = asarray(element.Currents).view(dtype=complex) - unit = 'A' - - ax.set_title(f'{etype}.{ename.upper()} {quantity} ({unit})') - size = 'x-small' - - def get_text(): - v = values[bus_idx * nconds + cond] - if quantity == 'Powers': - arrow_text = f"{v.real:-.6g} {'-' if v.imag < 0 else '+'} j{abs(v.imag):g}" - else: - if quantity == 'Voltages': - v /= vbase - arrow_text = f"{np.abs(v):-.6g} {unit} ∠ {np.angle(v, deg=True):.2f}°" - return arrow_text - for bus_idx, vbase in enumerate(vbases): - for cond in range(nconds): - if cond < (nconds - 1): - weight = 'bold' - lw = 2 - else: - weight = 'normal' - lw = 0.6667 - - if bus_idx: - arrow_x = XMAX + 5 - arrow_y = y - (cond + 1) * 10.0 - dx = box_xy1[0] - arrow_x - ax.text(arrow_x - 20, arrow_y + 2, get_text(), ha='right', fontweight=weight, size=size) - if voltage: - plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) - x = XMAX - 4 * (cond) - 1 - ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) - else: - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) - - else: - arrow_x = -5 - arrow_y = y - (cond + 1) * 10.0 - dx = box_xy0[0] + 5 - ax.text(arrow_x + 20, arrow_y + 2, get_text(), ha='left', fontweight=weight, size=size) - if voltage: - plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) - x = 4 * (cond) + 1 - ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) - else: - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) - - if quantity == 'Currents': - # Residual - v = -np.sum(values[(nconds * bus_idx):(nconds * (bus_idx + 1))]) - txt = f"{np.abs(v):-.6g} A ∠ {np.angle(v, deg=True):.2f}°" - - if bus_idx: - arrow_x = XMAX + 5 - arrow_y = -10 - dx = box_xy1[0] - arrow_x - ax.text(arrow_x - 5, arrow_y + 2, txt, ha='right', fontweight='normal', size=size) - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) - else: - arrow_x = -5 - arrow_y = -10 - dx = box_xy0[0] + 5 - ax.text(arrow_x + 5, arrow_y + 2, txt, ha='left', fontweight='normal', size=size) - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) - - ax.set_xlim(-20, XMAX + 20) - ax.set_ylim(-15, y + 5) - - -def dss_general_data_plot(DSS: IDSS, - *, - PlotType: str = None, - ObjectName: str = None, - ValueIndex: int = None, - Color1: str = None, - Color2: str = None, - Labels: bool = None, - MinScaleIsSpecified: bool = None, - MaxScaleIsSpecified: bool = None, - MinScale: float = None, - MaxScale: float = None, - - **kwargs: Unpack[PlotParams] -): - if not MaxScaleIsSpecified: - MaxScale = None - - if not MinScaleIsSpecified: - MinScale = None - - is_general = PlotType == 'GeneralData' - ValueIndex = max(1, ValueIndex - 1) - fn = ObjectName - do_labels = Labels - color1 = Color1 - color2 = Color2 - - # Whenever we add Pandas as a dependency, this could be - # rewritten to avoid all the extra/slow work - exp = re.compile('[,=\t]') - with open(fn, 'r') as f: - line = f.readline().rstrip() - field = exp.split(line)[ValueIndex].strip() #TODO: Is this right?! - f.seek(0) - # Find min and max - names, vals = [], [] - for line in f: - if not line: - continue + def dss_priceshape_plot(self, + *, + ObjectName: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] + ): + # There is no dedicated API yet but we can move to the Obj API + name = ObjectName + DSS = self.DSS - data = exp.split(line) - name, val = data[0], data[ValueIndex] - if len(val): - names.append(name) - vals.append(float(val)) - - vals = np.asarray(vals) - min_val = np.min(vals) - max_val = np.max(vals) - - # Do some sanity checking on the numbers. Don't want to include negative numbers in autoadd plot - if not is_general: - if min_val < 0.0: - min_val = 0.0 - if max_val < 0.0: - max_val = 0.0 - - if MaxScaleIsSpecified: - max_val = MaxScale # Override with user specified value - if MinScaleIsSpecified: - min_val = MinScale # Override with user specified value - - diff = max_val - min_val - if diff == 0.0: - diff = max_val - if diff == 0.0: - diff = 1.0 # Everything is zero - - sidxs = np.argsort(vals) - bus: IBus = DSS.ActiveCircuit.ActiveBus - data = [] - labels = [] - colors = [] - c1 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color1)) - c2 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color2)) - for i in sidxs: - name, val = names[i], vals[i] - if DSS.ActiveCircuit.SetActiveBus(name) <= 0 or not bus.Coorddefined: - continue - - if is_general: - data.append((bus.x, bus.y, val)) - s = ((val - min_val) / diff) - colors.append(c2*s + c1*(1-s)) - # InterpolateGradientColor(Color1, Color2, (GenPlotItem.Value - MinValue) / Diff), - else: # ptAutoAddLogPlot - data.append((bus.x, bus.y, val)) - # GetAutoColor((GenPlotItem.Value - MinValue) / Diff), - - if do_labels: - labels.append(bus.Name) + DSS.Text.Command = f'? priceshape.{name}.price' + p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + try: + DSS.Text.Command = f'? priceshape.{name}.hour' + h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + except: + h = np.array([]) - data = np.asarray(data) + try: + interval = f'? priceshape.{name}.interval' # hours + interval = float(DSS.Text.Result) + except: + interval = 1 + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"PriceShape.{ObjectName}") - dss_circuit_plot(DSS, **kwargs) + if not h.size: + h = interval * np.array(range(len(p))) - #fig = plt.figure(figsize=(8, 7)) - plt.title(f'{field}, Max={max_val:.3g}') - ax = plt.gca() - #if not is3d: - #ax.set_aspect('equal', 'datalim') + x_unit = 'h' + if h[-1] < 1: + h *= 3600 + x_unit = 's' - ax.scatter(data[:, 0], data[:, 1], c=colors, zorder=10) - # ax.colorbar() + color1 = Color1 - #ax.autoscale_view() - #ax.get_xaxis().get_major_formatter().set_scientific(False) - #ax.get_yaxis().get_major_formatter().set_scientific(False) - #plt.tight_layout() + ax.plot(h, p, color=color1, label="Price") + ax.set_title(f"PriceShape = {ObjectName}") + ax.set_xlabel(f'Time ({x_unit})') + ax.set_ylabel('Price') - # marker_code = MarkerIdx + ax.grid(ls='--') + fig.set_layout_engine(layout='tight') + + + def dss_loadshape_plot(self, + *, + ObjectName: str = None, + Color1: str = None, + Color2: str = None, + **kwargs: Unpack[PlotParams] + ): + # pprint(kwargs) + DSS = self.DSS + + ls = DSS.ActiveCircuit.LoadShapes + ls.Name = ObjectName + h = asarray(ls.TimeArray) + p = asarray(ls.Pmult) + q = asarray(ls.Qmult) + + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"LoadShape.{ObjectName}") - # NodeMarkerWidth: int - # MarkerIdx = NodeMarkerCode + if not h.size or h is None or len(h) != len(p): + h = ls.HrInterval * np.array(range(len(p))) - # marker_code = pmarkers[code_opt] - # marker_size = pmarkers[size_opt] - #marker_dict = get_marker_dict(marker_code) - # ax.plot(*coords, color='red', **marker_dict) - #MarkSpecialClasses + x_unit = 'h' + if h[-1] < 1: + h *= 3600 + x_unit = 's' + color1 = Color1 + color2 = Color2 -def dss_matrix_plot(DSS: IDSS, - *, - MatrixType: str = None, - Color1: str = None, - **kwargs: Unpack[PlotParams] -): - # plot_id = kwargs.get('PlotId', None) - if MatrixType == 'IncMatrix': - title = 'Incidence matrix' - data = DSS.ActiveCircuit.Solution.IncMatrix[:-1] - else: - title = 'Laplacian matrix' - data = DSS.ActiveCircuit.Solution.Laplacian[:-1] + ax.plot(h, p, color=color1, label="Pmult") + if q.size == p.size: + ax.plot(h, q, color=color2, label="Qmult") - x, y, v = data[0::3], data[1::3], data[2::3] - m = coo.coo_matrix((v, (x, y))) - #fig, [ax, ax2] = plt.subplots(1, 2, figsize=(8.6 * 2, 8.6), constrained_layout=True, num=title) - - if include_3d in ('both', '2d'): - fig = plt.figure(constrained_layout=True)#, num=plot_id) #, figsize=(8.6, 8.6)) - ax = fig.add_subplot(1, 1, 1) - ax.grid(True) - ax.spy(m, marker='s', markersize=1, color=Color1) - ax.set_xlabel('Column') - ax.set_ylabel('Row') - ax.set_title(title) - - if include_3d in ('both', '3d'): - fig = plt.figure()#figsize=(8.6, 8.6), num=plot_id + '_3D') - ax2 = fig.add_subplot(1, 1, 1, projection='3d') - ax2.scatter(x, y, v, c=v, marker='s') - ax2.set_xlabel('Column') - ax2.set_ylabel('Row') - ax2.set_zlabel('Value') - - -def dss_daisy_plot(DSS: IDSS, - *, - DaisyBusList: List[str] = None, - Quantity: str = None, - Labels: bool = None, - DaisySize: float = None, - **kwargs: Unpack[PlotParams] -): - dss_circuit_plot(DSS, **kwargs) - - # print(params['DaisySize']) - - ax = plt.gca() - XMIN, XMAX = ax.get_xlim() - quantity = str_to_pq.get(Quantity, pqNone) - daisy_bus_list = DaisyBusList - do_labels = Labels - daisy_size = DaisySize - - ax.set_title(f'Device Locations / {quantity_str[quantity]}') - element = DSS.ActiveCircuit.ActiveCktElement - - if len(daisy_bus_list) == 0: - for g in DSS.ActiveCircuit.Generators: - if element.Enabled: - daisy_bus_list.append(element.BusNames[0]) - - counts = np.zeros(shape=(DSS.ActiveCircuit.NumBuses + 1,), dtype=np.int32) - for b in daisy_bus_list: - idx = DSS.ActiveCircuit.SetActiveBus(b) - if idx > 0: - counts[idx] += 1 - - radius = 0.005 * daisy_size * (XMAX - XMIN) - lines = [] - pointx, pointy = [], [] - for bidx in np.nonzero(counts)[0]: - bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] - if not bus.Coorddefined: - continue - - cnt = counts[bidx] - angle0 = 0 - angle = np.pi * 2.0 / cnt - for j in range(cnt): - Xc = bus.x + 2 * radius * np.cos(angle * j + angle0) - Yc = bus.y + 2 * radius * np.sin(angle * j + angle0) - lines.append([(bus.x, bus.y), (Xc, Yc)]) - pointx.append(Xc) - pointy.append(Yc) - + ax.set_title(f"LoadShape = {ObjectName}") + ax.set_xlabel(f'Time ({x_unit})') + if ls.UseActual: + if q.size == p.size: + ax.set_ylabel('kW, kvar') + else: + ax.set_ylabel('kW') + else: + ax.set_ylabel('p.u.') - lc = LineCollection(lines, linewidth=1, colors='r') - ax.add_collection(lc) - ax.scatter(pointx, pointy, marker='o', color='yellow', edgecolors='red', s=100, zorder=10) + ax.grid(ls='--') + if q.size == p.size: + ax.legend() + fig.set_layout_engine(layout='tight') + + + def _get_branch_data(self, + branch_objects: DSSIterable, + bus_coords: Dict[str, Tuple[float, float, float]], + do_values=pqNone, + do_switches=False, + idxs=None, + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 + ): + DSS = self.DSS + + line_count = branch_objects.Count if not idxs else len(idxs) + lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) + lines.fill(np.nan) + values = np.empty(shape=(line_count, ), dtype=np.float64) + values.fill(np.nan) + lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) + + element = DSS.ActiveCircuit.ActiveCktElement + + if do_switches: + switch_idxs = [] + isolated_idxs = [] + try: + element.IsIsolated + has_is_isolated = True + except: + has_is_isolated = False + isolated_names = set(name.lower() for name in DSS.ActiveCircuit.Topology.AllIsolatedBranches if name) - if not do_labels: - return + extra = [switch_idxs, isolated_idxs] + else: + extra = [] + # def get_buses_line(l): + # b1 = remove_nodes(l.Bus1) + # b2 = remove_nodes(l.Bus2) - for bidx in np.nonzero(counts)[0]: - bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] - if not bus.Coorddefined: - continue + offset = 0 + skip = set() - ax.text(bus.x, bus.y, bus.Name, zorder=11, fontsize='xx-small', va='center', clip_on=True) + # norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu + # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu + # emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu + # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu + + vbs = None + if do_values == pqCurrent: + # Currently the same as pqCapacity to match the OpenDSS impl.; the correct would be: + #max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllMaxCurrents(True))) + try: + max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + except: + max_currents = {} + elem = DSS.ActiveCircuit.ActiveCktElement + for _ in DSS.ActiveCircuit.PDElements: + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + norm_amps = elem.NormalAmps + max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + + elif do_values == pqCapacity: + try: + capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + except: + max_currents = {} + elem = DSS.ActiveCircuit.ActiveCktElement + for _ in DSS.ActiveCircuit.PDElements: + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + norm_amps = elem.NormalAmps + max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + + elif do_values == pqVoltage: + node_volts = dict(zip(DSS.ActiveCircuit.AllNodeNames, asarray(DSS.ActiveCircuit.AllBusVmag) * 1e-3)) + vbs = np.empty(shape=(line_count, ), dtype=np.float64) + vbs.fill(0) + extra.append(vbs) + + if idxs: + l = branch_objects + for idx in idxs: + l.idx = idx + buses = element.BusNames + b1 = remove_nodes(buses[0]) + b2 = remove_nodes(buses[1]) + + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None: + skip.add(idx) + continue + + lines[offset, 0] = fr + lines[offset, 1] = to + offset += 1 + + if do_values == pqNone: + return lines[:offset] + + offset = 0 + for idx in idxs: + if idx in skip: + continue + + l.idx = idx + + if do_values == pqPower: + values[offset] = np.abs(element.TotalPowers[0]) + elif do_values == pqLosses: + values[offset] = abs(element.Losses[0]) / l.Length + elif do_values == pqVoltage: + b2name = nodot(l.Bus2) + b = DSS.ActiveCircuit.Buses[b2name] + vb = b.kVBase + vbs[offset] = vb + value = 1e30 + if vb > 0: + for n in b.Nodes: + if n > 0 and n <= 3: + value = min(value, node_volts[f'{b2name}.{n}'] / vb) + + values[offset] = value + elif do_values == pqCurrent: + values[offset] = max_currents.get(element.Name, np.NaN) + elif do_values == pqCapacity: + values[offset] = capacities.get(element.Name, np.NaN) + + offset += 1 + + return lines[:offset], values[:offset] + + else: + for i, l in enumerate(branch_objects): + buses = element.BusNames + b1 = remove_nodes(buses[0]) + b2 = remove_nodes(buses[1]) + + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None or not element.Enabled: + skip.add(i) + continue + if do_switches: + if ((has_is_isolated and element.IsIsolated) or + ((not has_is_isolated) and (element.Name.lower() in isolated_names))): + isolated_idxs.append(offset) -def unquote(field: str): - field = field.strip() - if field[0] == '"' and field[-1] == '"': - return field[1:-1] - - return field + if l.IsSwitch: + #skip.add(i) + switch_idxs.append(offset) + #continue + lines[offset, 0] = fr + lines[offset, 1] = to -def dss_di_plot(DSS: IDSS, - *, - CaseName: str = None, - MeterName: str = None, - Registers: List[int] = None, - CaseYear: str = None, - PeakDay: bool = None, - **kwargs: Unpack[PlotParams] -): - caseYear, caseName, meterName = CaseYear, CaseName, MeterName - plotRegisters, peakDay = Registers, PeakDay + offset += 1 + + if do_values == pqNone: + return [lines[:offset], None, None] + extra + + offset = 0 - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', meterName + '.csv') + for i, l in enumerate(branch_objects): + if i in skip: + continue + + if do_values == pqPower: + values[offset] = np.abs(element.TotalPowers[0]) + elif do_values == pqLosses: + values[offset] = abs(element.Losses[0]) / l.Length + elif do_values == pqVoltage: + b2name = nodot(l.Bus2) + b = DSS.ActiveCircuit.Buses[b2name] + vb = b.kVBase + vbs[offset] = vb + value = 1e30 + + if l.Phases < 3: + lines_styles[offset] = 1 + + if vb > 0: + for n in b.Nodes: + if n > 0 and n <= 3: + value = min(value, node_volts[f'{b2name}.{n}'] / vb) + + values[offset] = value + elif do_values == pqCurrent: + values[offset] = max_currents.get(element.Name, np.NaN) + elif do_values == pqCapacity: + values[offset] = capacities.get(element.Name, np.NaN) + + lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style + offset += 1 + + return [lines[:offset], values[:offset], lines_styles[:offset]] + extra + - if len(plotRegisters) == 0: - raise RuntimeError("No register indices were provided for DI_Plot") + def _get_point_data(self, + point_objects: Union[str, Iterable], + bus_coords: Dict[str, Tuple[float, float, float]], + do_values: bool = False + ): + DSS = self.DSS + if isinstance(point_objects, str): + cls = point_objects + DSS.SetActiveClass(cls) + point_objects = DSS.ActiveClass + + point_count = point_objects.Count - if not os.path.exists(fn): - fn = fn[:-4] + '_1.csv' + points = np.empty(shape=(point_count, 2), dtype=np.float64) + values = np.empty(shape=(point_count, ), dtype=np.float64) - # Whenever we add Pandas as a dependency, this could be - # rewritten to avoid all the extra/slow work - selected_data = [] - day_data = [] - mult = 1 if peakDay else 0.001 - - # If the file doesn't exist, let the exception raise - with open(fn, 'r') as f: - header = f.readline().rstrip() - allRegisterNames = [unquote(field) for field in header.strip().strip(' \t,').split(',')] - registerNames = [allRegisterNames[i] for i in plotRegisters] - - if not len(registerNames): - raise RuntimeError("Could not find any register name in the file") + offset = 0 + skip = set() + element = DSS.ActiveCircuit.ActiveCktElement + for i, _ in enumerate(point_objects): + buses = element.BusNames + all_coords = [] + buses = [remove_nodes(b) for b in buses] + all_coords = [c for c in (bus_coords.get(b) for b in buses) if c] + + if not all_coords: + skip.add(i) + continue - for line in f: - if not line: + coords = tuple(sum(c) / len(all_coords) for c in zip(*all_coords)) + + points[offset] = coords + offset += 1 + + if not do_values: + return points[:offset] + + offset = 0 + for i, _ in enumerate(point_objects): + if i in skip: continue + + values[offset] = np.abs(element.TotalPowers[0]) + offset += 1 + + return points[:offset], values[:offset] - rawValues = line.split(',') - selValues = [float(rawValues[0]), *(float(rawValues[i]) for i in plotRegisters)] - if not peakDay: - selected_data.append(selValues) - else: - day_data.append(selValues) - if len(day_data) == 24: - max_vals = [max(x) for x in zip(*day_data)] - max_vals[0] = day_data[0][0] - day_data = [] - selected_data.append(max_vals) - - if day_data: - max_vals = [max(x) for x in zip(*day_data)] - max_vals[0] = day_data[0][0] - day_data = [] - selected_data.append(max_vals) - - vals = np.asarray(selected_data, dtype=float) - fig, ax = plt.subplots(1) - icolor = -1 - for idx, name in enumerate(registerNames, start=1): - icolor += 1 - ax.plot(vals[:, 0], vals[:, idx] * mult, label=name, color=Colors[icolor % len(Colors)]) - - ax.set_title(f'{caseName}, Yr={caseYear}') - ax.set_xlabel('Hour') - ax.set_ylabel('MW, MWh or MVA') - ax.legend() - ax.grid() - - -def _plot_yearly_case(DSS: IDSS, caseName: str, meterName: str, plotRegisters: List[int], icolor: int, ax, registerNames: List[str]): - anyData = True - xvalues = [] - all_yvalues = [[] for _ in plotRegisters] - for caseYear in range(0, 21): - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'Totals_1.csv') - if not os.path.exists(fn): - continue - with open(fn, 'r') as f: - f.readline() # Skip the header - # Get started - initialize Registers 1 - registerVals = [float(x) * 0.001 for x in f.readline().split(',')] - if len(registerVals): - xvalues.append(registerVals[7]) - - if len(xvalues) == 0: - raise RuntimeError('No data to plot') - - for caseYear in range(0, 21): - if meterName.lower() in ('totals', 'systemmeter', 'totals_1', 'systemmeter_1'): - suffix = '' if meterName.endswith('_1') else '_1' - meterName = meterName.lower().replace('totals', 'Totals').replace('systemmeter', 'SystemMeter') - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', f'{meterName}{suffix}.csv') - searchForMeterLine = False + def dss_profile_plot(self, + *, + PhasesToPlot: int = None, + ProfileScale: float = None, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + + if len(DSS.ActiveCircuit.Meters) == 0: + raise RuntimeError(f"An EnergyMeter is required to use 'plot profile'") + + vmin = DSS.ActiveCircuit.Settings.NormVminpu + vmax = DSS.ActiveCircuit.Settings.NormVmaxpu + if ProfileScale == '120kft': + xlabel = 'Distance (kft)' + ylabel = '120 Base Voltage' + DenomLN = 1.0 / 120.0 + # DenomLL = 1.732 / 120.0 + LenScale = 3.2809 + # RangeScale = 120.0 else: - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'EnergyMeterTotals_1.csv') - searchForMeterLine = True + xlabel = 'Distance (km)' + ylabel = 'p.u. Voltage' + DenomLN = 1.0 + # DenomLL = 1.732 + LenScale = 1.0 + # RangeScale = 1.0 + + busnode_to_index = {(bn.rsplit('.', 1)[0], int(bn.rsplit('.', 1)[1])): num for (num, bn) in enumerate(DSS.ActiveCircuit.AllNodeNames)} + bus_to_kvbase = {b.Name: b.kVBase for b in DSS.ActiveCircuit.Buses} + puV = asarray(DSS.ActiveCircuit.AllBusVmagPu) / DenomLN + distances = {name: d for (name, d) in zip(DSS.ActiveCircuit.AllBusNames, asarray(DSS.ActiveCircuit.AllBusDistances) * LenScale)} + linewidths = [] + segments = [] + colors = [] + linestyles = [] + seg_phases = [] + pri_only = (PhasesToPlot == DSSPlotPhases.PROFILEALLPRI) + if PhasesToPlot in [DSSPlotPhases.PROFILEALL, DSSPlotPhases.PROFILEALLPRI, DSSPlotPhases.PROFILE3PH]: + phases = (1, 2, 3) + else: + phases = PhasesToPlot + try: + _ = iter(phases) + except: + phases = [phases] + + for em in DSS.ActiveCircuit.Meters: + branch_names = em.AllBranchesInZone + br: str + for br in branch_names: + if not br.startswith('Line.'): + continue - if not os.path.exists(fn): - continue + ls = '-' + lw = 2 - with open(fn, 'r') as f: - header = f.readline() - if len(registerNames) == 0: - allRegisterNames = [unquote(field) for field in header.strip(' \t,').split(',')] - registerNames.extend(allRegisterNames[i] for i in plotRegisters) + DSS.ActiveCircuit.Lines.Name = br[len('Line.'):] + + if DSSPlotPhases.PROFILE3PH == PhasesToPlot and DSS.ActiveCircuit.Lines.Phases < 3: + continue - if not searchForMeterLine: - line = f.readline() + bus1 = nodot(DSS.ActiveCircuit.Lines.Bus1) + bus2 = nodot(DSS.ActiveCircuit.Lines.Bus2) + + # Plot all phases present (between 1 and 3) + for iphs in phases: + try: + b1n_idx = busnode_to_index[(bus1, iphs)] + b2n_idx = busnode_to_index[(bus2, iphs)] + except: + continue + + if bus_to_kvbase[bus1] < 1.0: + if pri_only: + continue + ls = ':' + lw = 1 + + segments.append(((distances[bus1], puV[b1n_idx]), (distances[bus2], puV[b2n_idx]))) + colors.append(Colors[iphs - 1]) + seg_phases.append(iphs) + linestyles.append(ls) + linewidths.append(lw) + #TODO: NodeMarkerCode, NodeMarkerWidth + + if include_3d in ('both', '2d'): + fig = plt.figure()#figsize=(9, 5)) + ax = fig.add_subplot(1, 1, 1) + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): + ax.set_title('L-L Voltage Profile') else: - for line in f: - label, rest = line.split(',', 1) - if label.strip().lower() == meterName.lower(): - line = f'{caseYear},{rest}' - else: - raise RuntimeError("Meter not found") + ax.set_title('L-N Voltage Profile') + - registerVals = [float(x) * 0.001 for x in line.strip(' \t,').split(',')] - if len(registerVals): - for yvalues, idx in zip(all_yvalues, plotRegisters): - yvalues.append(registerVals[idx]) - - for yvalues, idx, regName in zip(all_yvalues, plotRegisters, registerNames): - marker_code = MARKER_SEQ[icolor % len(MARKER_SEQ)] - ax.plot(xvalues, yvalues, label=f'{caseName}:{meterName}:{regName}', color=Colors[icolor % len(Colors)], **get_marker_dict(marker_code)) - icolor += 1 - - return icolor - - -def dss_yearly_curve_plot(DSS: IDSS, *, - MeterName: str = None, - CaseNames: List[str] = None, - Registers: List[str] = None, - **kwargs: Unpack[PlotParams] -): - caseNames, meterName, plotRegisters = CaseNames, MeterName, Registers - - fig, ax = plt.subplots(1) - icolor = 0 - registerNames = [] - for caseName in caseNames: - icolor = _plot_yearly_case(DSS, caseName, MeterName, plotRegisters, icolor, ax, registerNames) - - if icolor == 0: - plt.close(fig) - raise RuntimeError('No files found') - - fig.suptitle(f"Yearly Curves for case(s): {', '.join(caseNames)}") - ax.set_title(f"Meter: {meterName}; Registers: {', '.join(registerNames)}", fontsize='small') - ax.set_xlabel('Total Area MW') - ax.set_ylabel('MW, MWh or MVA') - ax.legend() - ax.grid() - - -def dss_comparecases_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): - print('TODO: dss_comparecases_plot', kwargs) - - -def dss_zone_plot(DSS: IDSS, - *, - ObjectName: str, - Quantity: DSSPlotQuantity = DEFAULT_PLOT_PARAMS['Quantity'], - ShowLoops: bool = DEFAULT_PLOT_PARAMS['ShowLoops'], - Dots: bool = DEFAULT_PLOT_PARAMS['Dots'], - Labels: bool = DEFAULT_PLOT_PARAMS['Labels'], - Color1: str = DEFAULT_PLOT_PARAMS['Color1'], - Color3: str = DEFAULT_PLOT_PARAMS['Color3'], - SinglePhLineStyle: int = DEFAULT_PLOT_PARAMS['SinglePhLineStyle'], - ThreePhLineStyle: int = DEFAULT_PLOT_PARAMS['ThreePhLineStyle'], - MaxLineThickness: float = DEFAULT_PLOT_PARAMS['MaxLineThickness'], - MaxScale: float = DEFAULT_PLOT_PARAMS['MaxScale'], - **kwargs: Unpack[PlotParams] -): - obj_name = ObjectName - show_loops = ShowLoops - color1 = Color1 - color3 = Color3 - single_ph_line_style = LINES_STYLE_CODE.get(SinglePhLineStyle) - three_ph_line_style = LINES_STYLE_CODE.get(ThreePhLineStyle) - dots = Dots - do_labels = Labels - quantity = str_to_pq.get(Quantity, pqNone) - max_lw = MaxLineThickness - - if MaxScale is not None: - quantity_max_value = MaxScale - else: - quantity_max_value = 0 - - - ActiveCircuit = DSS.ActiveCircuit - - if obj_name: - ActiveCircuit.Meters.Name = obj_name - meters = [ActiveCircuit.Meters] - else: - meters = ActiveCircuit.Meters - - elem = ActiveCircuit.ActiveCktElement - line = ActiveCircuit.Lines - topo = ActiveCircuit.Topology - - icolor = 0 - - #TODO: check if/where we need to transform to lowercase. - bus_coords = dict((b.Name.lower(), (b.x, b.y)) for b in ActiveCircuit.Buses if b.Coorddefined) - - meter_marker_dict = get_marker_dict(24) - meter_marker_dict['markersize'] *= (3 / 3.5)**2 - - lines1, lines1_colors, labels1 = [], [], [] - lines3, lines3_colors, labels3 = [], [], [] - - # lw1, lw3 will initially hold the values, later transformed to actual widths - lw1, lw3 = [], [] - - if quantity in (pqCurrent, pqCapacity): - capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) - - coords_to_names = {} - - def _name_coords(c, name): - prev = coords_to_names.get(c) - if prev is None: - coords_to_names[c] = name - return - elif prev == name: - return + lc = LineCollection(segments, linewidth=linewidths, colors=colors, linestyles=linestyles) + + # ax.set_title('{}:{}, max: {:3g}'.format(DSS.ActiveCircuit.Name, quantity, quantity_max_value)) + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + ax.add_collection(lc) + ax.autoscale_view() + ax.axhline(vmin, color='darkred', ls='-', lw=3) + ax.axhline(vmax, color='darkred', ls='-', lw=3) + ax.grid(ls='--') + fig.set_layout_engine(layout='tight') - if prev.endswith(',' + name) or prev.startswith(name + ',') or (',' + name + ',') in prev: - return + if include_3d in ('both', '3d'): + fig2 = plt.figure()#figsize=(7, 7)) + ax2 = fig2.add_subplot(1, 1, 1, projection='3d') + ax2.set_xlabel(xlabel) + ax2.set_ylabel(ylabel) + if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): + ax2.set_title('L-L Voltage Profile') + else: + ax2.set_title('L-N Voltage Profile') + + segments_3d = [ + [(*p, ph) for p in seg] for seg, ph in zip(segments, seg_phases) + ] + rseg = np.ravel(segments) + max_x = np.max(rseg[::2]) + max_y = np.max(rseg[1::2]) + min_y = np.min(rseg[1::2]) + lc3d = Line3DCollection(segments_3d, colors=colors, linestyles=linestyles) + ax2.add_collection(lc3d) + ax2.set_xlabel(xlabel) + ax2.set_ylabel(ylabel) + ax2.set_zlabel('Phase') + xl = [0, max_x] + yl = [min(min_y, vmin) - 0.05, min(max_y, vmax) + 0.05] + maxph = np.max(seg_phases) + 1 + ax2.set_xlim(xl) + ax2.set_ylim(yl) + ax2.set_zlim(0, maxph) + ax2.plot_surface( + np.array([xl, xl]), + np.array([[vmax, vmax]] * 2), + np.array([[0, 0], [maxph, maxph]]), + color='k', + alpha=0.5 + ) + ax2.plot_surface( + np.array([xl, xl]), + np.array([[vmin, vmin]] * 2), + np.array([[0, 0], [maxph, maxph]]), + color='k', + alpha=0.5 + ) + ax2.autoscale_view() + + + def _get_gic_line_data_altdss( + self, + altdss: IAltDSS, + bus_coords: Dict[str, Tuple[float, float, float]], + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 + ): + branch_objects = altdss.GICLine + line_count = len(branch_objects)# if not idxs else len(idxs) + lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) + lines.fill(np.nan) + values = np.empty(shape=(line_count, ), dtype=np.float64) + values.fill(np.nan) + lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) + offset = 0 + # skip = set() - coords_to_names[c] = prev + ',' + name + # GIC lines are not exposed nicely in the classic API, so we'll use the new Obj API + for gic_line in altdss.GICLine: + if not gic_line.enabled: + continue + b1 = remove_nodes(gic_line.bus1) + b2 = remove_nodes(gic_line.bus2) + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None: + # skip.add(idx) + continue + + lines[offset, 0] = fr + lines[offset, 1] = to - def _add_line(element, color): - br_name = element.Name - bus1, bus2 = element.BusNames[:2] - bus1, bus2 = nodot(bus1).lower(), nodot(bus2).lower() - c1 = bus_coords.get(bus1) - c2 = bus_coords.get(bus2) - lw = 1 - if not c1 or not c2: - return None, None + lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style + values[offset] = gic_line.MaxCurrent(1) + offset += 1 - if do_labels: - _name_coords(c1, f'{bus1}({feeder_name})') - _name_coords(c2, f'{bus2}({feeder_name})') + return lines[:offset], values[:offset], lines_styles[:offset] - if quantity == pqPower: - lw = element.TotalPowers[0] - elif quantity == pqVoltage: - lw = 1 - elif quantity == pqLosses: - lw = 0 - try: - if element.Name.startswith('Line.'): - lw = 1e-3 * abs(element.Losses[0] / line.Length) - except: - pass - elif quantity in (pqCurrent, pqCapacity): - lw = capacities.get(element.Name, np.NaN) - - if (element.NumPhases == 1): - lines1.append([c1, c2]) - lines1_colors.append(color) - labels1.append(br_name) - lw1.append(lw) - return lines1_colors, len(lines1_colors) - 1 - else: - lines3.append([c1, c2]) - lines3_colors.append(color) - labels3.append(br_name) - lw3.append(lw) - return lines3_colors, len(lines3_colors) - 1 - - - fig, ax = plt.subplots(1) - for meter in meters: - if not elem.Enabled: - continue - - feeder_name = meter.Name - branches = meter.AllBranchesInZone - if not branches: - continue - - # Meter marker - _ = topo.First - coords = bus_coords.get(elem.BusNames[meter.MeteredTerminal - 1]) - if coords: - plt.plot(*coords, color='red', **meter_marker_dict) - feeder_color = color1 if show_loops else Colors[icolor % len(Colors)] - icolor += 1 + def _get_gic_line_data(self, + bus_coords: Dict[str, Tuple[float, float]], + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 + ): + DSS = self.DSS + try: + return self._get_gic_line_data_altdss( + DSS.to_altdss(), + bus_coords, + single_ph_line_style=single_ph_line_style, + three_ph_line_style=three_ph_line_style + ) + except: + pass - br_idx = topo.First - while br_idx != 0: - if not elem.Enabled: + # Fallback for Oddie and COM + DSS.ActiveCircuit.SetActiveClass('GICLine') + aclass = DSS.ActiveCircuit.ActiveClass + line_count = aclass.Count# if not idxs else len(idxs) + lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) + lines.fill(np.nan) + values = np.empty(shape=(line_count, ), dtype=np.float64) + values.fill(np.nan) + lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) + offset = 0 + # skip = set() + + # GIC lines are not exposed nicely in the classic API + elem = DSS.ActiveCircuit.ActiveCktElement + idx = aclass.First + while idx != 0: + buses = elem.BusNames + b1 = remove_nodes(buses[0]) + b2 = remove_nodes(buses[1]) + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None: + # skip.add(idx) continue + + lines[offset, 0] = fr + lines[offset, 1] = to + + lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + values[offset] = max_current + offset += 1 - lcs, lidx = _add_line(elem, feeder_color) - if show_loops: - looped = (topo.LoopedBranch != 0) - if looped: - # The looped PDE is set as active by LoopedBranch - _add_line(elem, color3) - # Adjust the original to color3 - if lidx is not None: - lcs[lidx] = color3 - - br_idx = topo.Next - - - lw1 = np.asarray(lw1) - lw3 = np.asarray(lw3) - - if quantity_max_value == 0: - lw1_max_value = 0 - lw3_max_value = 0 - if len(lw1): - lw1_max_value = np.nanmax(lw1) - if np.isfinite(lw1_max_value): - quantity_max_value = max(quantity_max_value, lw1_max_value) - if len(lw3): - lw3_max_value = np.nanmax(lw3) - if np.isfinite(lw3_max_value): - quantity_max_value = max(quantity_max_value, lw3_max_value) - - if quantity_max_value == 0: - quantity_max_value = 1 - - lw1 = np.clip(3 * lw1 / quantity_max_value, 0.5, max_lw) - lw3 = np.clip(3 * lw3 / quantity_max_value, 0.5, max_lw) - lines1 = np.asarray(lines1) - lines3 = np.asarray(lines3) - lc1 = LineCollection(lines1, linewidth=lw1, colors=lines1_colors, linestyle=single_ph_line_style) - lc3 = LineCollection(lines3, linewidth=lw3, colors=lines3_colors, linestyle=three_ph_line_style) - ax.add_collection(lc1) - ax.add_collection(lc3) - if dots: - for lines, lc in ((lines1, lc1), (lines3, lc3)): - ax.scatter(lines[:, 0, 0].ravel(), lines[:, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) - ax.scatter(lines[:, 1, 0].ravel(), lines[:, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) + return lines[:offset], values[:offset], lines_styles[:offset] + + + def dss_circuit_plot(self, + *, + fig=None, + ax=None, + is3d=False, + Quantity: str = None, + Dots: bool = False, + Color1: str = None, + Color2: str = None, + Color3: str = None, + SinglePhLineStyle: int = None, + ThreePhLineStyle: int = None, + MaxLineThickness: float = None, + BusMarkers: List[BusMarker] = None, + Labels: bool = None, + Markers: ObjMarkers = None, + MaxScale: float = None, + MaxScaleIsSpecified: bool = None, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + + if not MaxScaleIsSpecified: + MaxScale = None + + quantity = str_to_pq.get(Quantity, pqNone) + dots = Dots + color1 = Color1 + color2 = Color2 + color3 = Color3 + single_ph_line_style = SinglePhLineStyle + three_ph_line_style = ThreePhLineStyle + max_lw = MaxLineThickness + bus_markers = BusMarkers or [] + do_labels = Labels + + norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu + # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu + emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu + # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu - ax.set_title(f'Meter Zone: {obj_name}' if obj_name else 'All Meter Zones') - - for coords, name in coords_to_names.items(): - ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - - ax.set_aspect('equal', 'datalim') - ax.autoscale() - - - -dss_plot_funcs = { - 'Scatter': dss_scatter_plot, - 'Daisy': dss_daisy_plot, - 'TShape': dss_tshape_plot, - 'PriceShape': dss_priceshape_plot, - 'LoadShape': dss_loadshape_plot, - 'Monitor': dss_monitor_plot, - 'Circuit': dss_circuit_plot, - 'Profile': dss_profile_plot, - 'Visualize': dss_visualize_plot, - 'YearlyCurve': dss_yearly_curve_plot, - 'Matrix': dss_matrix_plot, - 'GeneralData': dss_general_data_plot, - 'DI': dss_di_plot, -# 'CompareCases': dss_comparecases_plot, - 'MeterZones': dss_zone_plot -} + # bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if (b.x, b.y) != (0.0, 0.0)) + bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if b.Coorddefined) + + if fig is None: + fig = plt.figure()#figsize=(8, 7)) + + given_ax = ax is not None + if not given_ax: + ax = plt.gca() + else: + plt.sca(ax) -def dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): - try: - ptype = kwargs['PlotType'] - if ptype not in dss_plot_funcs: - raise NotImplementedError(f'ERROR: not implemented plot type "{ptype}"') - return -1 + if not is3d: + ax.set_aspect('equal', 'datalim') + + lines_lines, lines_values, lines_styles, switch_idxs, isolated_idxs, *extra = self._get_branch_data( + DSS, + DSS.ActiveCircuit.Lines, + bus_coords, + do_values=quantity, + do_switches=True, + single_ph_line_style=single_ph_line_style, + three_ph_line_style=three_ph_line_style + ) + + if isolated_idxs: + line_idx = isolated_idxs + if not is3d: + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#ff00ff', capstyle='round')) - with ToggleAdvancedTypes(DSS, False), warnings.catch_warnings(): - warnings.simplefilter("ignore") - dss_plot_funcs.get(ptype)(DSS, **kwargs) + if switch_idxs: + line_idx = switch_idxs + if not is3d: + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#000000', capstyle='round')) + + switch_idxs = set(switch_idxs) + isolated_idxs = set(isolated_idxs) + #lc_lines = LineCollection(lines_lines, linewidths=0.5, color=color1)# + 3 * lines_values / np.max(lines_values), linestyle='solid', color=color1) + quantity_max_value = MaxScale if MaxScale is not None else 0.0 + + quantity_suffix = '' + + if lines_lines is not None and len(lines_lines) > 0: + if quantity in (pqVoltage,): + colors = [] + for v in lines_values: + if v > norm_min_volts or np.isnan(v): + colors.append(color1) + elif v > emerg_min_volts: + colors.append(color2) + else: + colors.append(color3) + + + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] + if not is3d: + edgecolors = [colors[i] for i in line_idx] + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=edgecolors, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) + + # if is3d: + # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) + # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) + # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) + + quantity_max_value = 0 + elif quantity in (pqLosses,): + + if quantity_max_value == 0: + # quantity_max_value = max(lines_values) * 1e-3 + # For compatibility with the official version, loop through all lines instead + # of the actual plotted lines + element = DSS.ActiveCircuit.ActiveCktElement + quantity_max_value = max( + abs(element.Losses[0] / line.Length) + for line in DSS.ActiveCircuit.Lines + if element.Enabled + ) * 0.001 - except Exception as ex: - from traceback import format_exc - # print('DSS: Error while plotting. Parameters:', kwargs, file=sys.stderr) - DSS._errorPtr[0] = 777 - DSS._lib.Error_Set_Description(f"Error in the plot backend: {ex}\n{format_exc()}".encode()) - return 777 - - return 0 - + lines_values = np.clip(3 * 1e-3 * lines_values / quantity_max_value, 0.5, max_lw) + if not is3d: + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] + # edgecolors = [colors[i] for i in line_idx] + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + + elif quantity in (pqCurrent, pqCapacity): + line_idx = [i for i in range(lines_lines.shape[0]) if i not in isolated_idxs and i not in switch_idxs] + colors = [color3 if v > 100 and not np.isnan(v) else color1 for v in lines_values[line_idx]] -# dss_progress_bar = None -# dss_progress_desc = '' + if quantity_max_value == 0: + quantity_max_value = max(lines_values) + lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) + if not is3d: + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle='-', color=colors, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) + + elif quantity != pqNone: + if quantity == pqPower: + quantity_suffix = ' kW' + if quantity_max_value == 0: + #lines_values *= 1e-3 + + # For compatibility with the official version, loop through all lines instead + # of the actual plotted lines + element = DSS.ActiveCircuit.ActiveCktElement + + quantity_max_value = max( + element.TotalPowers[0] + for _ in DSS.ActiveCircuit.Lines + if element.Enabled + ) #* 0.001 + else: + #TODO:may need workaround about GeneralPlotQuantity + quantity_max_value = max(lines_values) -@api_util.ffi.def_extern() -def dss_python_cb_write(ctx, message_str, message_type: int, message_size: int, message_subtype: int): - global dss_progress_bar - global dss_progress_desc + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] + if not is3d: + ax.add_collection(LineCollection( + lines_lines[line_idx, :], + linewidths=np.clip(0.5 + 3 * lines_values[line_idx] / quantity_max_value, 0.5, max_lw), + linestyle=LINES_STYLE_CODE.get(ls, 'solid'), + color=color1, + capstyle='round' + )) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + else: + #TODO: handle 1 and 3 phase, etc.? + if not is3d: + ax.add_collection(LineCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) + # else: + # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) + # ax.set_xlim(np.min(lines_lines[:, :, 0]), np.max(lines_lines[:, :, 0])) + # ax.set_ylim(np.min(lines_lines[:, :, 1]), np.max(lines_lines[:, :, 1])) - # DSS = _ctx2dss(ctx) - - message_str = api_util.ffi.string(message_str).decode(api_util.codec) - if message_type == api_util.lib.DSSMessageType_Error: - #print('DSS Error:', message_str, file=sys.stderr) - pass - elif message_type in (api_util.lib.DSSMessageType_ProgressCaption, api_util.lib.DSSMessageType_ProgressFormCaption): - #dss_progress_desc = message_str - # print('Progress Caption:', message_str, file=sys.stderr) - pass - elif message_type == api_util.lib.DSSMessageType_Progress: - #print('DSS Progress:', message_str, file=sys.stderr) - pass - elif message_type == api_util.lib.DSSMessageType_FireOffEditor: - link_file(message_str) - # try: - # # print('DSSMessageType_FireOffEditor') - # with open(message_str, 'r') as f: - # text = f.read() - - # IPython.display.display({'text/plain': text}, raw=True) - # except: - # print(f'Could not display file "{message_str}"') - # return 1 + transformers_lines, *_ = self._get_branch_data(DSS, DSS.ActiveCircuit.Transformers, bus_coords) - elif message_type == api_util.lib.DSSMessageType_ProgressPercent: - try: - pass - # n = int(message_str) - # desc = '' - # if n == 0 and dss_progress_bar is not None: - # dss_progress_bar = None + if not is3d: + lc_transformers = LineCollection(transformers_lines, linewidth=3, linestyle='solid', color='gray') + ax.add_collection(lc_transformers) + + lines_lines, lines_values, lines_styles, *_ = self._get_gic_line_data(bus_coords, single_ph_line_style=single_ph_line_style, three_ph_line_style=three_ph_line_style) + if len(lines_lines) != 0: + if quantity_max_value == 0: + quantity_max_value = max(lines_values) + + lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls] + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + + + + # 'Daisysize' + # 'Markercode', 'Nodewidth' # NodeMarkerCode + + branch_marker_options = [ + ('MarkSwitches', 'SwitchMarkerCode', None, DSS.ActiveCircuit.Lines, switch_idxs), + ('MarkFuses', 'FuseMarkerCode', 'FuseMarkerSize', DSS.ActiveCircuit.Fuses, None), + ('MarkRegulators', 'RegMarkerCode', 'RegMarkerSize', DSS.ActiveCircuit.RegControls, None), + ('MarkRelays', 'RelayMarkerCode', 'RelayMarkerSize', DSS.ActiveCircuit.Relays, None), + ('MarkReclosers', 'RecloserMarkerCode', 'RecloserMarkerSize', DSS.ActiveCircuit.Reclosers, None) + ] + + point_marker_options = [ + ('MarkTransformers', 'TransMarkerCode', 'TransMarkerSize', DSS.ActiveCircuit.Transformers, None), + ('MarkCapacitors', 'CapMarkerCode', 'CapMarkerSize', DSS.ActiveCircuit.Capacitors, None), + ('MarkPVSystems', 'PVMarkerCode', 'PVMarkerSize', DSS.ActiveCircuit.PVSystems, None), + ('MarkStorage', 'StoreMarkerCode', 'StoreMarkerSize', DSS.ActiveCircuit.Storages, None), + ] + + pmarkers = Markers + if pmarkers is not None: + for (mark_opt, code_opt, size_opt, objs, idxs) in branch_marker_options: + # print(mark_opt, pmarkers[mark_opt]) + if not pmarkers[mark_opt]: + continue + + marker_code = pmarkers[code_opt] + marker_size = pmarkers[size_opt] + #TODO: use marker_size? + marker_dict = get_marker_dict(marker_code) + if mark_opt == 'MarkRegulators': + for obj in objs: + DSS.ActiveCircuit.Transformers.Name = obj.Transformer + bus = remove_nodes(DSS.ActiveCircuit.ActiveCktElement.BusNames[obj.Winding - 1]) + coords = bus_coords.get(bus) + if coords is None: + continue + ax.plot(*coords, color='red', **marker_dict) - # if dss_progress_bar is None: - # dss_progress_bar = tqdm(total=100, desc=dss_progress_desc) + else: + #TODO? branch_lines = self._get_branch_data(DSS, objs, bus_coords, idxs=idxs) + pass - # if n < 0: - # del dss_progress_bar - # dss_progress_bar = None - # return 0 + for (mark_opt, code_opt, size_opt, objs, idxs) in point_marker_options: + if not pmarkers[mark_opt]: + continue + + marker_code = pmarkers[code_opt] + marker_size = pmarkers[size_opt] - # dss_progress_bar.n = n - # dss_progress_bar.refresh() -# if n == 100: -# dss_progress_bar.close() - except: - import traceback - traceback.print_exc() - print('DSS Progress:', message_str) - - # else: - # # print(message_type) - # # print(message_str) - # IPython.display.display({'text/plain': message_str}, raw=True) - else: - # do nothing for now... - pass + points = self._get_point_data(DSS, objs, bus_coords) + + # if marker_code not in MARKER_MAP: + #marker_code = 25 + + marker_dict = get_marker_dict(marker_code) + #marker_dict['markersize'] *= (marker_size / 2.0)**2 + marker_dict['markersize'] *= (marker_size / 1.2)**2 + + #marker_dict['marker'] = marker_dict['marker'].vertices + #marker_dict.pop('markersize') + #marker_dict.pop('markerfacecolor') + # print(mark_opt, marker_dict['marker']) + # pprint(marker_dict) + ax.plot(points[:, 0], points[:, 1], ls='', color='red', **marker_dict) + #ax.plot(points[:, 0], points[:, 1], color='red', ls='', marker=6, alpha=1) + + for bus_marker in bus_markers: + name = bus_marker['Name'] + bus = DSS.ActiveCircuit.Buses[name] + if not bus.Coorddefined: + raise RuntimeError('Bus markers: coordinates are not defined for bus "{name}"') + + marker_dict = get_marker_dict(bus_marker['Code']) + marker_size = bus_marker['Size'] + marker_dict['markersize'] *= (marker_size / 6) + ax.plot(bus.x, bus.y, ls='', color=bus_marker['Color'], **marker_dict) + + + ax.set_xlabel('X') + ax.set_ylabel('Y') + if not given_ax: + if quantity != pqNone: + ax.set_title('{}:{}, max={:g}{}'.format(DSS.ActiveCircuit.Name.upper(), quantity_str[quantity], quantity_max_value, quantity_suffix)) + ax.autoscale_view() + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + fig.set_layout_engine(layout='tight') + + if do_labels: + coords_to_names = {} + for name, coords in bus_coords.items(): + prev = coords_to_names.get(coords) + if prev: + coords_to_names[coords] = prev + ',' + name + else: + coords_to_names[coords] = name + + for coords, name in coords_to_names.items(): + ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) + + + def dss_scatter_plot(self, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + x = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) + y = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) + vcomplex = np.empty(shape=(DSS.ActiveCircuit.NumBuses, 3), dtype=complex) + x.fill(np.nan) + y.fill(np.nan) + vcomplex.fill(np.nan) + for idx, b in enumerate(DSS.ActiveCircuit.Buses): + if not b.Coorddefined: + continue + + x[idx] = b.x + y[idx] = b.y + vnodes = asarray(b.puVoltages).view(dtype=complex) + nnodes = min(3, len(vnodes)) + vcomplex[idx, :nnodes] = vnodes[:nnodes] - return 0 + vabs = np.abs(vcomplex) + del vcomplex + with suppress_warnings(): + vmean = np.mean(vabs, axis=1, where=np.isfinite(vabs)) + + title = '{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude') + if include_3d in ('both', '2d'): + fig, ax = plt.subplots(1, 1, constrained_layout=True)#, figsize=(8, 7)) + dss_circuit_plot(DSS, fig=fig, ax=ax, Color1='k') + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + sc = ax.scatter(x, y, c=vmean) + fig.colorbar(sc, label='V1 (pu)') + ax.set_title(title) + + if include_3d in ('both', '3d'): + bus_coords = {} + for idx, b in enumerate(DSS.ActiveCircuit.Buses): + if b.Coorddefined: + bus_coords[b.Name] = (b.x, b.y, vmean[idx]) + + fig = plt.figure()#figsize=(7, 7)) + ax = fig.add_subplot(projection='3d') + dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True, Color1='k') + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + # if is3d: + # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) + # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) + # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) -@api_util.ffi.def_extern() -def dss_python_cb_plot(ctx, paramsStr): - params = json.loads(api_util.ffi.string(paramsStr)) - result = 0 - try: - DSS = IDSS._get_instance(ctx=ctx) - result = dss_plot(DSS, **params) - if _do_show: - plt.show() - except: - from traceback import print_exc - print('DSS: Error while plotting. Parameters:', params, file=sys.stderr) - print_exc() - return 0 if result is None else result + sc = ax.scatter(x, y, vmean, c='k', s=2) -_original_allow_forms = None -_do_show = True -_enabled = False + segs = [] + el = DSS.ActiveCircuit.ActiveCktElement + for pd in DSS.ActiveCircuit.PDElements: + buses = el.BusNames + if len(buses) != 2: + continue -def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True, ctx: IDSS = None): - """ - Enables the plotting subsystem from DSS-Extensions. + seg = [] + for b in buses: + c = bus_coords.get(nodot(b), None) + if c is not None: + seg.append(c) + + if len(seg) == 2: + segs.append(seg) + + segs = np.array(segs, dtype=float) + seg_v = (segs[:, 0, 2] + segs[:, 1, 2]) / 2 + + lc3d = Line3DCollection(segs) + ax.add_collection(lc3d) + lc3d.set_array(seg_v) + #fig.colorbar(sc, label='V1 (pu)') + ax.set_title(title) + + + def dss_visualize_plot(self, + *, + Quantity: str = None, + ElementType: str = None, + ElementName: str = None, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + + XMAX = 300 + #pprint(kwargs) + quantity = Quantity + + # Fix for backend v0.13.1 + quantity = { + 'Power': 'Powers', + 'Current': 'Currents', + 'Voltage': 'Voltages', + }.get(quantity, quantity) + + element = DSS.ActiveCircuit.ActiveCktElement + etype, ename = ElementType, ElementName + nconds = element.NumConductors + # nphases = element.NumPhases + buses = element.BusNames[:2] # max 2 terminals + vbases = [max(1, 1000 * DSS.ActiveCircuit.Buses[nodot(b)].kVBase) for b in buses] + + # assert DSS.ActiveCircuit.ActiveCktElement.Name == ElementType + '.' + ElementName + fig, ax = plt.subplots(1, gridspec_kw=dict(left=0.05, right=0.95, bottom=0.05, top=0.92))#, figsize=(8.6, 7)) + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + ax.grid(False) + + y = 20 + 10 * nconds + box_xy0 = np.array([100, 10]) + box_xy1 = np.array([XMAX - 100, y]) + box_wh = box_xy1 - box_xy0 + middle_box = patches.Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') + ax.text(XMAX / 2, 10 + (y - 10) / 2, f'{etype}.{ename.upper()}', ha='center', va='center', fontweight='bold', rotation='vertical') + ax.add_patch(middle_box) + ax.plot([0, 300], [0, 0], color='gray', lw=7) + + ax.plot([-5] * 2, [5, y - 5], color='k', lw=7) + ax.text(25, y, buses[0].upper(), ha='left') + if len(buses) > 1: + ax.plot([XMAX + 5] * 2, [5, y - 5], color='k', lw=7) + ax.text(XMAX - 25, y, buses[1].upper(), ha='right') + + voltage = (quantity == 'Voltages') - Set plot3d to `True` to try to reproduce some of the plots from the - alternative OpenDSS Visualization Tool / OpenDSS Viewer addition - to OpenDSS. + if quantity == 'Powers': + values = 1e-3 * (asarray(element.Voltages).view(dtype=complex) * np.conj(asarray(element.Currents).view(dtype=complex))) + unit = 'kVA' + elif voltage: + values = asarray(element.Voltages).view(dtype=complex) + unit = 'pu' + elif quantity == 'Currents': + values = asarray(element.Currents).view(dtype=complex) + unit = 'A' + + ax.set_title(f'{etype}.{ename.upper()} {quantity} ({unit})') + size = 'x-small' + + def _get_text(): + v = values[bus_idx * nconds + cond] + if quantity == 'Powers': + arrow_text = f"{v.real:-.6g} {'-' if v.imag < 0 else '+'} j{abs(v.imag):g}" + else: + if quantity == 'Voltages': + v /= vbase + arrow_text = f"{np.abs(v):-.6g} {unit} ∠ {np.angle(v, deg=True):.2f}°" - Use `show` to control whether this backend should call `pyplot.show()` - or leave that to the system or the user. If the user plans to customize - the figure, it is better to set `show=False` in order to preserve the - figures, since `pyplot.show()` discards them. - """ + return arrow_text - global include_3d - global _original_allow_forms - global _do_show - global _enabled - global DSSPlotCtx + for bus_idx, vbase in enumerate(vbases): + for cond in range(nconds): + if cond < (nconds - 1): + weight = 'bold' + lw = 2 + else: + weight = 'normal' + lw = 0.6667 + + if bus_idx: + arrow_x = XMAX + 5 + arrow_y = y - (cond + 1) * 10.0 + dx = box_xy1[0] - arrow_x + ax.text(arrow_x - 20, arrow_y + 2, _get_text(), ha='right', fontweight=weight, size=size) + if voltage: + plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) + x = XMAX - 4 * (cond) - 1 + ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) + else: + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) + + else: + arrow_x = -5 + arrow_y = y - (cond + 1) * 10.0 + dx = box_xy0[0] + 5 + ax.text(arrow_x + 20, arrow_y + 2, _get_text(), ha='left', fontweight=weight, size=size) + if voltage: + plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) + x = 4 * (cond) + 1 + ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) + else: + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) + + if quantity == 'Currents': + # Residual + v = -np.sum(values[(nconds * bus_idx):(nconds * (bus_idx + 1))]) + txt = f"{np.abs(v):-.6g} A ∠ {np.angle(v, deg=True):.2f}°" + + if bus_idx: + arrow_x = XMAX + 5 + arrow_y = -10 + dx = box_xy1[0] - arrow_x + ax.text(arrow_x - 5, arrow_y + 2, txt, ha='right', fontweight='normal', size=size) + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) + else: + arrow_x = -5 + arrow_y = -10 + dx = box_xy0[0] + 5 + ax.text(arrow_x + 5, arrow_y + 2, txt, ha='left', fontweight='normal', size=size) + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) + + ax.set_xlim(-20, XMAX + 20) + ax.set_ylim(-15, y + 5) + + + def dss_general_data_plot(self, + *, + PlotType: str = None, + ObjectName: str = None, + ValueIndex: int = None, + Color1: str = None, + Color2: str = None, + Labels: bool = None, + MinScaleIsSpecified: bool = None, + MaxScaleIsSpecified: bool = None, + MinScale: float = None, + MaxScale: float = None, + + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + + if not MaxScaleIsSpecified: + MaxScale = None + + if not MinScaleIsSpecified: + MinScale = None + + is_general = PlotType == 'GeneralData' + ValueIndex = max(1, ValueIndex - 1) + fn = ObjectName + do_labels = Labels + color1 = Color1 + color2 = Color2 + + # Whenever we add Pandas as a dependency, this could be + # rewritten to avoid all the extra/slow work + exp = re.compile('[,=\t]') + with open(fn, 'r') as f: + line = f.readline().rstrip() + field = exp.split(line)[ValueIndex].strip() #TODO: Is this right?! + f.seek(0) + # Find min and max + names, vals = [], [] + for line in f: + if not line: + continue - if ctx is not None: - DSSPlotCtx = ctx + data = exp.split(line) + name, val = data[0], data[ValueIndex] + if len(val): + names.append(name) + vals.append(float(val)) + + vals = np.asarray(vals) + min_val = np.min(vals) + max_val = np.max(vals) + + # Do some sanity checking on the numbers. Don't want to include negative numbers in autoadd plot + if not is_general: + if min_val < 0.0: + min_val = 0.0 + if max_val < 0.0: + max_val = 0.0 + + if MaxScaleIsSpecified: + max_val = MaxScale # Override with user specified value + if MinScaleIsSpecified: + min_val = MinScale # Override with user specified value + + diff = max_val - min_val + if diff == 0.0: + diff = max_val + if diff == 0.0: + diff = 1.0 # Everything is zero + + sidxs = np.argsort(vals) + bus: IBus = DSS.ActiveCircuit.ActiveBus + data = [] + labels = [] + colors = [] + c1 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color1)) + c2 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color2)) + for i in sidxs: + name, val = names[i], vals[i] + if DSS.ActiveCircuit.SetActiveBus(name) <= 0 or not bus.Coorddefined: + continue - _do_show = show - _enabled = True + if is_general: + data.append((bus.x, bus.y, val)) + s = ((val - min_val) / diff) + colors.append(c2*s + c1*(1-s)) + # InterpolateGradientColor(Color1, Color2, (GenPlotItem.Value - MinValue) / Diff), + else: # ptAutoAddLogPlot + data.append((bus.x, bus.y, val)) + # GetAutoColor((GenPlotItem.Value - MinValue) / Diff), + + if do_labels: + labels.append(bus.Name) - if plot3d and plot2d: - include_3d = 'both' - elif plot3d and not plot2d: - include_3d = '3d' - elif plot2d and not plot3d: - include_3d = '2d' + data = np.asarray(data) - api_util.lib.DSS_RegisterPlotCallback(api_util.lib.dss_python_cb_plot) - api_util.lib.DSS_RegisterMessageCallback(api_util.lib.dss_python_cb_write) - _original_allow_forms = DSSPlotCtx.AllowForms - DSSPlotCtx.AllowForms = True -def disable(): - global _enabled - _enabled = False - api_util.lib.DSS_RegisterPlotCallback(api_util.ffi.NULL) - api_util.lib.DSS_RegisterMessageCallback(api_util.ffi.NULL) - if _original_allow_forms is not None: - DSSPlotCtx.AllowForms = _original_allow_forms + dss_circuit_plot(DSS, **kwargs) + #fig = plt.figure(figsize=(8, 7)) + plt.title(f'{field}, Max={max_val:.3g}') + ax = plt.gca() + #if not is3d: + #ax.set_aspect('equal', 'datalim') + ax.scatter(data[:, 0], data[:, 1], c=colors, zorder=10) + # ax.colorbar() -DSV_LINE_STYLES = { - 0: 'solid', - 1: 'dashed', - 2: 'dotted', - 3: 'dashdot', - 4: (0, (3, 5, 1, 5, 1, 5)), -} + #ax.autoscale_view() + #ax.get_xaxis().get_major_formatter().set_scientific(False) + #ax.get_yaxis().get_major_formatter().set_scientific(False) + #fig.set_layout_engine(layout='tight') -def _int_to_color(v: int): - return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) + # marker_code = MarkerIdx -DSS_ITEMS = { - 'BoldLabel', - 'Caption', - 'Center', - 'ChartCaption', - 'Circle', - 'ClickOn', - 'Curve', - 'DataColor', - 'Draw', - 'FStyle', - 'KeepAspect', - 'KeyClass', - 'Label', - 'Line', - 'Marker', - 'Move', - 'NoScales', - 'PctRim', - 'Range', - 'Rect', - 'SetProp', - 'Text', - 'TxtAlign', - 'Width', - 'Xlabel', - 'Ylabel', -} + # NodeMarkerWidth: int + # MarkerIdx = NodeMarkerCode -class IPlotting: - def __init__(self, dss: IDSS): - self.dss = dss + # marker_code = pmarkers[code_opt] + # marker_size = pmarkers[size_opt] + #marker_dict = get_marker_dict(marker_code) + # ax.plot(*coords, color='red', **marker_dict) + #MarkSpecialClasses -class DSVHandler: - def __init__(self, fn: str): - self.fn = fn - self.fig, self.ax = plt.subplots() - self.ax.get_xaxis().get_major_formatter().set_scientific(False) - self.ax.get_yaxis().get_major_formatter().set_scientific(False) - self.xy = [0.0, 0.0] - self.line_width = 1 - self.fig_caption = None - self.color = 'k' - self.key_class = None - self.no_scales = False - self.bold = True - self.txt_align = 'left' + def dss_matrix_plot(self, + *, + MatrixType: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + # plot_id = kwargs.get('PlotId', None) + if MatrixType == 'IncMatrix': + title = 'Incidence matrix' + data = DSS.ActiveCircuit.Solution.IncMatrix[:-1] + else: + title = 'Laplacian matrix' + data = DSS.ActiveCircuit.Solution.Laplacian[:-1] - def BoldLabel(self, param_str: str): - self.bold = int(param_str.strip()) != 0 + x, y, v = data[0::3], data[1::3], data[2::3] + m = coo.coo_matrix((v, (x, y))) + #fig, [ax, ax2] = plt.subplots(1, 2, figsize=(8.6 * 2, 8.6), constrained_layout=True, num=title) + + if include_3d in ('both', '2d'): + fig = plt.figure(constrained_layout=True)#, num=plot_id) #, figsize=(8.6, 8.6)) + ax = fig.add_subplot(1, 1, 1) + ax.grid(True) + ax.spy(m, marker='s', markersize=1, color=Color1) + ax.set_xlabel('Column') + ax.set_ylabel('Row') + ax.set_title(title) + + if include_3d in ('both', '3d'): + fig = plt.figure()#figsize=(8.6, 8.6), num=plot_id + '_3D') + ax2 = fig.add_subplot(1, 1, 1, projection='3d') + ax2.scatter(x, y, v, c=v, marker='s') + ax2.set_xlabel('Column') + ax2.set_ylabel('Row') + ax2.set_zlabel('Value') + + def dss_daisy_plot(self, + *, + DaisyBusList: List[str] = None, + Quantity: str = None, + Labels: bool = None, + DaisySize: float = None, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + + dss_circuit_plot(DSS, **kwargs) + + # print(params['DaisySize']) + ax = plt.gca() + XMIN, XMAX = ax.get_xlim() + quantity = str_to_pq.get(Quantity, pqNone) + daisy_bus_list = DaisyBusList + do_labels = Labels + daisy_size = DaisySize + + ax.set_title(f'Device Locations / {quantity_str[quantity]}') + element = DSS.ActiveCircuit.ActiveCktElement + + if len(daisy_bus_list) == 0: + for g in DSS.ActiveCircuit.Generators: + if element.Enabled: + daisy_bus_list.append(element.BusNames[0]) + + counts = np.zeros(shape=(DSS.ActiveCircuit.NumBuses + 1,), dtype=np.int32) + for b in daisy_bus_list: + idx = DSS.ActiveCircuit.SetActiveBus(b) + if idx > 0: + counts[idx] += 1 + + radius = 0.005 * daisy_size * (XMAX - XMIN) + lines = [] + pointx, pointy = [], [] + for bidx in np.nonzero(counts)[0]: + bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] + if not bus.Coorddefined: + continue - def Caption(self, param_str: str): - self.fig_caption = param_str.strip().strip('"') - self.fig.canvas.manager.set_window_title(self.fig_caption) + cnt = counts[bidx] + angle0 = 0 + angle = np.pi * 2.0 / cnt + for j in range(cnt): + Xc = bus.x + 2 * radius * np.cos(angle * j + angle0) + Yc = bus.y + 2 * radius * np.sin(angle * j + angle0) + lines.append([(bus.x, bus.y), (Xc, Yc)]) + pointx.append(Xc) + pointy.append(Yc) + + lc = LineCollection(lines, linewidth=1, colors='r') + ax.add_collection(lc) + ax.scatter(pointx, pointy, marker='o', color='yellow', edgecolors='red', s=100, zorder=10) - def ChartCaption(self, param_str: str): - self.ax.set_title(param_str.strip().strip('"')) + if not do_labels: + return + for bidx in np.nonzero(counts)[0]: + bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] + if not bus.Coorddefined: + continue - def Center(self, param_str: str): - *int_params, text = param_str.split(',') - x, y, s = [int(v.strip()) for v in int_params] - text = text.strip().strip('"') - if '/_' in text: - text = text.replace('/_', '∠') + '°' + ax.text(bus.x, bus.y, bus.Name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - if '->' in text: - text = text.replace('->', '→') - s = s * 1.5 - elif '<-' in text: - text = text.replace('<-', '←') - s = s * 1.5 - elif '^' in text: - text = text.replace('^', '↑') - s = s * 1.5 - self.ax.text(x, y, text, horizontalalignment='center', fontsize=s * 8 / 13.) + def dss_di_plot(self, + *, + CaseName: str = None, + MeterName: str = None, + Registers: List[int] = None, + CaseYear: str = None, + PeakDay: bool = None, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + caseYear, caseName, meterName = CaseYear, CaseName, MeterName + plotRegisters, peakDay = Registers, PeakDay + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', meterName + '.csv') - def Circle(self, param_str: str): - params = param_str.split(',') - x, y = float(params[0]), float(params[1]) - fc = _int_to_color(int(params[4])) - ec = _int_to_color(int(params[3])) - self.ax.scatter(x, y, marker='o', color=fc, edgecolors=ec, s=50, zorder=10, linewidths=0.5) + if len(plotRegisters) == 0: + raise RuntimeError("No register indices were provided for DI_Plot") + if not os.path.exists(fn): + fn = fn[:-4] + '_1.csv' - def ClickOn(self, param_str: str): - #TODO - pass + # Whenever we add Pandas as a dependency, this could be + # rewritten to avoid all the extra/slow work + selected_data = [] + day_data = [] + mult = 1 if peakDay else 0.001 + # If the file doesn't exist, let the exception raise + with open(fn, 'r') as f: + header = f.readline().rstrip() + allRegisterNames = [unquote(field) for field in header.strip().strip(' \t,').split(',')] + registerNames = [allRegisterNames[i] for i in plotRegisters] - def Curve(self, param_str: str): - *int_params, curve_name, rest = param_str.split(',', 7) - npts, color, width, style, curve_markers, curve_marker = [int(v.strip()) for v in int_params] - if curve_markers: - marker_dict = get_marker_dict(curve_marker) - else: - marker_dict = {} - - data = np.fromstring(rest, dtype=float, sep=',') - self.ax.plot(data[:npts], data[npts:], lw=width/2.0, label=curve_name.strip().strip('"'), color=_int_to_color(color), ls=DSV_LINE_STYLES[style], **marker_dict) - # self.ax.minorticks_on() + if not len(registerNames): + raise RuntimeError("Could not find any register name in the file") + for line in f: + if not line: + continue - def DataColor(self, param_str: str): - self.color = _int_to_color(int(param_str)) + rawValues = line.split(',') + selValues = [float(rawValues[0]), *(float(rawValues[i]) for i in plotRegisters)] + if not peakDay: + selected_data.append(selValues) + else: + day_data.append(selValues) + if len(day_data) == 24: + max_vals = [max(x) for x in zip(*day_data)] + max_vals[0] = day_data[0][0] + day_data = [] + selected_data.append(max_vals) + + if day_data: + max_vals = [max(x) for x in zip(*day_data)] + max_vals[0] = day_data[0][0] + day_data = [] + selected_data.append(max_vals) + + vals = np.asarray(selected_data, dtype=float) + fig, ax = plt.subplots(1) + icolor = -1 + for idx, name in enumerate(registerNames, start=1): + icolor += 1 + ax.plot(vals[:, 0], vals[:, idx] * mult, label=name, color=Colors[icolor % len(Colors)]) + ax.set_title(f'{caseName}, Yr={caseYear}') + ax.set_xlabel('Hour') + ax.set_ylabel('MW, MWh or MVA') + ax.legend() + ax.grid() - def Draw(self, param_str: str): - if not self.no_scales: - # Currently not used since Move/Draw is emulated with axhline - return + + def _plot_yearly_case(self, caseName: str, meterName: str, plotRegisters: List[int], icolor: int, ax, registerNames: List[str]): + DSS = self.DSS + anyData = True + xvalues = [] + all_yvalues = [[] for _ in plotRegisters] + for caseYear in range(0, 21): + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'Totals_1.csv') + if not os.path.exists(fn): + continue + + with open(fn, 'r') as f: + f.readline() # Skip the header + # Get started - initialize Registers 1 + registerVals = [float(x) * 0.001 for x in f.readline().split(',')] + if len(registerVals): + xvalues.append(registerVals[7]) + + if len(xvalues) == 0: + raise RuntimeError('No data to plot') + + for caseYear in range(0, 21): + if meterName.lower() in ('totals', 'systemmeter', 'totals_1', 'systemmeter_1'): + suffix = '' if meterName.endswith('_1') else '_1' + meterName = meterName.lower().replace('totals', 'Totals').replace('systemmeter', 'SystemMeter') + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', f'{meterName}{suffix}.csv') + searchForMeterLine = False + else: + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'EnergyMeterTotals_1.csv') + searchForMeterLine = True + + if not os.path.exists(fn): + continue + + with open(fn, 'r') as f: + header = f.readline() + if len(registerNames) == 0: + allRegisterNames = [unquote(field) for field in header.strip(' \t,').split(',')] + registerNames.extend(allRegisterNames[i] for i in plotRegisters) + + if not searchForMeterLine: + line = f.readline() + else: + for line in f: + label, rest = line.split(',', 1) + if label.strip().lower() == meterName.lower(): + line = f'{caseYear},{rest}' + else: + raise RuntimeError("Meter not found") + + registerVals = [float(x) * 0.001 for x in line.strip(' \t,').split(',')] + if len(registerVals): + for yvalues, idx in zip(all_yvalues, plotRegisters): + yvalues.append(registerVals[idx]) - x0, y0 = self.xy - x1, y1 = [float(v.strip().strip('"')) for v in param_str.split(',')] - self.ax.plot([x0, x1], [y0, y1], color=self.color, lw=self.line_width/2.0) + for yvalues, idx, regName in zip(all_yvalues, plotRegisters, registerNames): + marker_code = MARKER_SEQ[icolor % len(MARKER_SEQ)] + ax.plot(xvalues, yvalues, label=f'{caseName}:{meterName}:{regName}', color=Colors[icolor % len(Colors)], **get_marker_dict(marker_code)) + icolor += 1 + return icolor - def FStyle(self, param_str: str): - fstyle = int(param_str.strip().strip('"')) - # if fstyle != 0: - # print('Unhandled font style:', fstyle) + def dss_yearly_curve_plot(self, *, + MeterName: str = None, + CaseNames: List[str] = None, + Registers: List[str] = None, + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + caseNames, meterName, plotRegisters = CaseNames, MeterName, Registers - def KeepAspect(self, param_str: str): - try: - v = int(param_str.strip().strip('"')) - except: - v = 1 + fig, ax = plt.subplots(1) + icolor = 0 + registerNames = [] + for caseName in caseNames: + icolor = _plot_yearly_case(DSS, caseName, MeterName, plotRegisters, icolor, ax, registerNames) + + if icolor == 0: + plt.close(fig) + raise RuntimeError('No files found') - if v: - self.ax.set_aspect('equal', 'datalim') + fig.suptitle(f"Yearly Curves for case(s): {', '.join(caseNames)}") + ax.set_title(f"Meter: {meterName}; Registers: {', '.join(registerNames)}", fontsize='small') + ax.set_xlabel('Total Area MW') + ax.set_ylabel('MW, MWh or MVA') + ax.legend() + ax.grid() + + + def dss_comparecases_plot(self, **kwargs: Unpack[PlotParams]): + DSS = self.DSS + print('TODO: dss_comparecases_plot', kwargs) + + + def dss_zone_plot(self, + *, + ObjectName: str, + Quantity: DSSPlotQuantity = DEFAULT_PLOT_PARAMS['Quantity'], + ShowLoops: bool = DEFAULT_PLOT_PARAMS['ShowLoops'], + Dots: bool = DEFAULT_PLOT_PARAMS['Dots'], + Labels: bool = DEFAULT_PLOT_PARAMS['Labels'], + Color1: str = DEFAULT_PLOT_PARAMS['Color1'], + Color3: str = DEFAULT_PLOT_PARAMS['Color3'], + SinglePhLineStyle: int = DEFAULT_PLOT_PARAMS['SinglePhLineStyle'], + ThreePhLineStyle: int = DEFAULT_PLOT_PARAMS['ThreePhLineStyle'], + MaxLineThickness: float = DEFAULT_PLOT_PARAMS['MaxLineThickness'], + MaxScale: float = DEFAULT_PLOT_PARAMS['MaxScale'], + **kwargs: Unpack[PlotParams] + ): + DSS = self.DSS + obj_name = ObjectName + show_loops = ShowLoops + color1 = Color1 + color3 = Color3 + single_ph_line_style = LINES_STYLE_CODE.get(SinglePhLineStyle) + three_ph_line_style = LINES_STYLE_CODE.get(ThreePhLineStyle) + dots = Dots + do_labels = Labels + quantity = str_to_pq.get(Quantity, pqNone) + max_lw = MaxLineThickness + + if MaxScale is not None: + quantity_max_value = MaxScale else: - self.ax.set_aspect('auto') + quantity_max_value = 0 - def KeyClass(self, param_str: str): - self.key_class = int(param_str.strip()) + ActiveCircuit = DSS.ActiveCircuit + if obj_name: + ActiveCircuit.Meters.Name = obj_name + meters = [ActiveCircuit.Meters] + else: + meters = ActiveCircuit.Meters - def Label(self, param_str: str): - *int_params, text, _ = param_str.split(',') - x, y, color_int = [int(v.strip()) for v in int_params] - color = _int_to_color(color_int) - text = text.strip().strip('"') - self.ax.text(x, y, text, - horizontalalignment='center', - fontsize=10 * 8 / 13., - color=color, - backgroundcolor='white', - weight='bold' if self.bold else 'normal' - ) + elem = ActiveCircuit.ActiveCktElement + line = ActiveCircuit.Lines + topo = ActiveCircuit.Topology + icolor = 0 - def Line(self, param_str: str): - #TODO: use LineCollection + #TODO: check if/where we need to transform to lowercase. + bus_coords = dict((b.Name.lower(), (b.x, b.y)) for b in ActiveCircuit.Buses if b.Coorddefined) - *str_params, rest = param_str.split(',', 3) - line_name, bus1, bus2 = [v.strip().strip('"') for v in str_params] - *int_params, rest = rest.split(',', 4) - offset, data_count, num_cust, total_cust = [int(v) for v in int_params] - *dbl_params, rest = rest.split(',', 6) - kv, dist, x1, y1, x2, y2 = [float(v) for v in dbl_params] - int_params = rest.split(',') - #TODO: markers - color, width, style, dots, mark_center, center_marker_code, node_marker_code, node_marker_size = [int(v) for v in int_params] + meter_marker_dict = get_marker_dict(24) + meter_marker_dict['markersize'] *= (3 / 3.5)**2 - if dots: - node_marker_dict = get_marker_dict(node_marker_code) - node_marker_dict['markersize'] *= max(1, np.sqrt(node_marker_size) - 1) * node_marker_dict['markersize'] / 7.0 - else: - node_marker_dict = {} - - self.ax.plot([x1, x2], [y1, y2], color=_int_to_color(color), lw=width / 2.0, ls=DSV_LINE_STYLES[style], solid_capstyle='round', **node_marker_dict) - - if mark_center: - center_marker_dict = get_marker_dict(center_marker_code) - self.ax.scatter((x1 + x2) / 2, (y1 + y2) / 2, color=_int_to_color(color), **center_marker_dict) + lines1, lines1_colors, labels1 = [], [], [] + lines3, lines3_colors, labels3 = [], [], [] - def Marker(self, param_str: str): - params = param_str.split(',') - x, y = float(params[0]), float(params[1]) - c, symbol, marker_size = [int(v) for v in params[2:]] - marker_dict = get_marker_dict(symbol) - marker_dict['markersize'] *= max(1, np.sqrt(marker_size) - 1) * marker_dict['markersize'] / 7.0 - self.ax.plot(x, y, ls=None, color=_int_to_color(c), **marker_dict) + # lw1, lw3 will initially hold the values, later transformed to actual widths + lw1, lw3 = [], [] + if quantity in (pqCurrent, pqCapacity): + capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) - def Move(self, param_str: str): - x, y = [float(v.strip().strip('"')) for v in param_str.split(',')] - if self.no_scales: - self.xy = [x, y] - else: - self.ax.axhline(y, color=self.color, lw=self.line_width / 2.0) + coords_to_names = {} + def _name_coords(c, name): + prev = coords_to_names.get(c) + if prev is None: + coords_to_names[c] = name + return + elif prev == name: + return + + if prev.endswith(',' + name) or prev.startswith(name + ',') or (',' + name + ',') in prev: + return - def NoScales(self, param_str: str): - self.no_scales = True - self.ax.get_xaxis().set_visible(False) - self.ax.get_yaxis().set_visible(False) + coords_to_names[c] = prev + ',' + name - def PctRim(self, param_str: str): - self.ax.margins(float(param_str) / 100.0) + def _add_line(element, color): + br_name = element.Name + bus1, bus2 = element.BusNames[:2] + bus1, bus2 = nodot(bus1).lower(), nodot(bus2).lower() + c1 = bus_coords.get(bus1) + c2 = bus_coords.get(bus2) + lw = 1 + if not c1 or not c2: + return None, None + if do_labels: + _name_coords(c1, f'{bus1}({feeder_name})') + _name_coords(c2, f'{bus2}({feeder_name})') - def Range(self, param_str: str): - pass + if quantity == pqPower: + lw = element.TotalPowers[0] + elif quantity == pqVoltage: + lw = 1 + elif quantity == pqLosses: + lw = 0 + try: + if element.Name.startswith('Line.'): + lw = 1e-3 * abs(element.Losses[0] / line.Length) + except: + pass + elif quantity in (pqCurrent, pqCapacity): + lw = capacities.get(element.Name, np.NaN) + + if (element.NumPhases == 1): + lines1.append([c1, c2]) + lines1_colors.append(color) + labels1.append(br_name) + lw1.append(lw) + return lines1_colors, len(lines1_colors) - 1 + else: + lines3.append([c1, c2]) + lines3_colors.append(color) + labels3.append(br_name) + lw3.append(lw) + return lines3_colors, len(lines3_colors) - 1 - def Rect(self, param_str: str): - left, bottom, right, top = [int(v) for v in param_str.split(',')] - r = patches.Rectangle((left, bottom), right - left, top - bottom, fill=True, ec='k', fc='#c0c0c0') - self.ax.add_patch(r) + fig, ax = plt.subplots(1) + for meter in meters: + if not elem.Enabled: + continue + feeder_name = meter.Name + branches = meter.AllBranchesInZone + if not branches: + continue + + # Meter marker + _ = topo.First + coords = bus_coords.get(elem.BusNames[meter.MeteredTerminal - 1]) + if coords: + plt.plot(*coords, color='red', **meter_marker_dict) - def SetProp(self, param_str: str): - if int(param_str.rsplit(',', 1)[-1]) != 0: - self.ax.grid(which='both', ls='--') - else: - self.ax.grid(False) + feeder_color = color1 if show_loops else Colors[icolor % len(Colors)] + icolor += 1 + + br_idx = topo.First + while br_idx != 0: + if not elem.Enabled: + continue + lcs, lidx = _add_line(elem, feeder_color) + if show_loops: + looped = (topo.LoopedBranch != 0) + if looped: + # The looped PDE is set as active by LoopedBranch + _add_line(elem, color3) + # Adjust the original to color3 + if lidx is not None: + lcs[lidx] = color3 + + br_idx = topo.Next - def Text(self, param_str: str): - *int_params, text = param_str.split(',') - x, y, c, s = [int(v.strip()) for v in int_params] - text = text.strip().strip('"') - self.ax.text(x, y, text, ha=self.txt_align, va='center', fontsize=s * 10 / 13.) + lw1 = np.asarray(lw1) + lw3 = np.asarray(lw3) - def TxtAlign(self, param_str: str): - v = int(param_str) - if v == 1: - self.txt_align = 'left' - return + if quantity_max_value == 0: + lw1_max_value = 0 + lw3_max_value = 0 + if len(lw1): + lw1_max_value = np.nanmax(lw1) + if np.isfinite(lw1_max_value): + quantity_max_value = max(quantity_max_value, lw1_max_value) + if len(lw3): + lw3_max_value = np.nanmax(lw3) + if np.isfinite(lw3_max_value): + quantity_max_value = max(quantity_max_value, lw3_max_value) - if v == 2: - self.txt_align = 'center' - return + if quantity_max_value == 0: + quantity_max_value = 1 + + lw1 = np.clip(3 * lw1 / quantity_max_value, 0.5, max_lw) + lw3 = np.clip(3 * lw3 / quantity_max_value, 0.5, max_lw) + lines1 = np.asarray(lines1) + lines3 = np.asarray(lines3) + lc1 = LineCollection(lines1, linewidth=lw1, colors=lines1_colors, linestyle=single_ph_line_style) + lc3 = LineCollection(lines3, linewidth=lw3, colors=lines3_colors, linestyle=three_ph_line_style) + ax.add_collection(lc1) + ax.add_collection(lc3) + if dots: + for lines, lc in ((lines1, lc1), (lines3, lc3)): + ax.scatter(lines[:, 0, 0].ravel(), lines[:, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) + ax.scatter(lines[:, 1, 0].ravel(), lines[:, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) - if v == 3: - self.txt_align = 'right' - return + ax.set_title(f'Meter Zone: {obj_name}' if obj_name else 'All Meter Zones') - - def Width(self, param_str: str): - self.line_width = int(param_str.strip().strip('"')) + for coords, name in coords_to_names.items(): + ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - - def Xlabel(self, param_str: str): - self.ax.set_xlabel(param_str.strip().strip('"')) + ax.set_aspect('equal', 'datalim') + ax.autoscale() + + + +dss_plot_methods = { + 'Scatter': 'dss_scatter_plot', + 'Daisy': 'dss_daisy_plot', + 'TShape': 'dss_tshape_plot', + 'PriceShape': 'dss_priceshape_plot', + 'LoadShape': 'dss_loadshape_plot', + 'Monitor': 'dss_monitor_plot', + 'Circuit': 'dss_circuit_plot', + 'Profile': 'dss_profile_plot', + 'Visualize': 'dss_visualize_plot', + 'YearlyCurve': 'dss_yearly_curve_plot', + 'Matrix': 'dss_matrix_plot', + 'GeneralData': 'dss_general_data_plot', + 'DI': 'dss_di_plot', +# 'CompareCases': 'dss_comparecases_plot', + 'MeterZones': 'dss_zone_plot' +} - - def Ylabel(self, param_str: str): - self.ax.set_ylabel(param_str.strip().strip('"')) - - def parse(self): - with open(self.fn, 'r') as f: - for l in f: - l = l.strip() - if not l: - continue +def dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): + try: + ptype = kwargs['PlotType'] + if ptype not in dss_plot_methods: + raise NotImplementedError(f'ERROR: not implemented plot type "{ptype}"') + return -1 - item_name, *rest = l.split(',', 1) - item_name = item_name.strip() - if item_name not in DSS_ITEMS: - raise NotImplemented(f'"{item_name}" DSV item is not implemented') + with ToggleAdvancedTypes(DSS, False), warnings.catch_warnings(): + warnings.simplefilter("ignore") + func = getattr(plotter, dss_plot_methods.get(ptype)) + return 0, (DSS, **kwargs) + + except Exception as ex: + from traceback import format_exc + # print('DSS: Error while plotting. Parameters:', kwargs, file=sys.stderr) + DSS._errorPtr[0] = 777 + DSS._lib.Error_Set_Description(f"Error in the plot backend: {ex}\n{format_exc()}".encode()) + return 777, None + + return 0, None + - # print(item, repr(rest)[:100]) - getattr(self, item_name)(rest[0] if rest else '') # let the exception propagate on error +@api_util.ffi.def_extern() +def dss_python_cb_plot(ctx, paramsStr): + params = json.loads(api_util.ffi.string(paramsStr)) + result = 0 + try: + DSS = IDSS._get_instance(ctx=ctx) + result, fig = dss_plot(DSS, **params) if _do_show: - plt.show() - else: - return self.fig, self.ax + fig.show() + except: + from traceback import print_exc + print('DSS: Error while plotting. Parameters:', params, file=sys.stderr) + print_exc() + return 0 if result is None else result + +_original_allow_forms = None +_do_show = True +_enabled = False + +def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True, ctx: IDSS = None): + """ + Enables the plotting subsystem from DSS-Extensions. + + Set plot3d to `True` to try to reproduce some of the plots from the + alternative OpenDSS Visualization Tool / OpenDSS Viewer addition + to OpenDSS. + + Use `show` to control whether this backend should call `pyplot.show()` + or leave that to the system or the user. If the user plans to customize + the figure, it is better to set `show=False` in order to preserve the + figures, since `pyplot.show()` discards them. + """ + + global include_3d + global _original_allow_forms + global _do_show + global _enabled + global DSSPlotCtx + + if ctx is not None: + DSSPlotCtx = ctx + + _do_show = show + _enabled = True + + if plot3d and plot2d: + include_3d = 'both' + elif plot3d and not plot2d: + include_3d = '3d' + elif plot2d and not plot3d: + include_3d = '2d' + + api_util.lib.DSS_RegisterPlotCallback(api_util.lib.dss_python_cb_plot) + api_util.lib.DSS_RegisterMessageCallback(api_util.lib.dss_python_cb_write) + _original_allow_forms = DSSPlotCtx.AllowForms + DSSPlotCtx.AllowForms = True + +def disable(): + global _enabled + _enabled = False + api_util.lib.DSS_RegisterPlotCallback(api_util.ffi.NULL) + api_util.lib.DSS_RegisterMessageCallback(api_util.ffi.NULL) + if _original_allow_forms is not None: + DSSPlotCtx.AllowForms = _original_allow_forms -def plot_dsv(fn: str): +def plot_dsv(fn: Union[str, FilePath]): return DSVHandler(fn).parse() __all__ = ['enable', 'disable', 'plot_dsv', ] diff --git a/dss/plot2.py b/dss/plot2.py new file mode 100644 index 00000000..c5fdacab --- /dev/null +++ b/dss/plot2.py @@ -0,0 +1,2698 @@ +""" +This module provides a **work-in-progress** implementation of the original OpenDSS plots +using the new features from DSS C-API v0.12+ and common Python modules such as matplotlib. + +This is not a complete implementation and there are known limitations, but should suffice +for many use-cases. We'd like to add another backend later. +""" +from __future__ import annotations +import os, re, json, sys, warnings +from typing import List, TYPE_CHECKING, Optional, Tuple, Dict +from typing_extensions import TypedDict, Unpack +from . import api_util +from . import DSS as DSSPlotCtx +from ._cffi_api_util import CffiApiUtil +from .IDSS import IDSS +from .IBus import IBus +from ._cffi_api_util import Iterable as DSSIterable +from enum import Enum, IntEnum +import numpy as np +from numpy import asarray +from numpy.testing import suppress_warnings +from pathlib import Path as FilePath +try: + from matplotlib import pyplot as plt + from matplotlib.path import Path + from matplotlib.collections import LineCollection + from mpl_toolkits.mplot3d.art3d import Line3DCollection + import matplotlib.patches as patches + import matplotlib.colors + import scipy.sparse.coo as coo +except: + raise ImportError("SciPy and matplotlib are required to use this module.") + +if TYPE_CHECKING: + from altdss.AltDSS import IAltDSS + +class DSSPlotType(Enum): + AutoAddLog = 'AutoAddLog' + Circuit = 'Circuit' + Daisy = 'Daisy' + Energy = 'Energy' + Evolution = 'Evolution' + GeneralData = 'GeneralData' + LoadShape = 'LoadShape' + Matrix = 'Matrix' + MeterZones = 'MeterZones' + Monitor = 'Monitor' + PhaseVoltage = 'PhaseVoltage' + PriceShape = 'PriceShape' + Profile = 'Profile' + Scatter = 'Scatter' + TShape = 'TShape' + + +(pqVoltage, pqCurrent, pqPower, pqLosses, pqCapacity, pqNone) = range(6) + +class DSSPlotQuantity(Enum): + Capacities = 'Capacities' + Currents = 'Currents' + Losses = 'Losses' + Powers = 'Powers' + Voltages = 'Voltages' + none = 'None' + + +class ProfileScale(Enum): + pukm = 'pukm' + kft120 = '120kft' + + +class ObjMarkers(TypedDict): + NodeMarkerCode: Optional[int] + NodeMarkerWidth: Optional[float] + + MarkTransformers: Optional[bool] + TransMarkerCode: Optional[int] + TransMarkerSize: Optional[float] + + MarkCapacitors: Optional[bool] + CapMarkerCode: Optional[int] + CapMarkerSize: Optional[float] + + MarkPVSystems: Optional[bool] + PVMarkerCode: Optional[int] + PVMarkerSize: Optional[float] + + MarkFuses: Optional[bool] + FuseMarkerCode: Optional[int] + FuseMarkerSize: Optional[float] + + MarkReclosers: Optional[bool] + RecloserMarkerCode: Optional[int] + RecloserMarkerSize: Optional[float] + + MarkRegulators: Optional[bool] + RegMarkerCode: Optional[int] + RegMarkerSize: Optional[float] + + MarkRelays: Optional[bool] + RelayMarkerCode: Optional[int] + RelayMarkerSize: Optional[float] + + MarkStorage: Optional[bool] + StoreMarkerCode: Optional[int] + StoreMarkerSize: Optional[float] + + MarkSwitches: Optional[bool] + SwitchMarkerCode: Optional[int] + + +class BusMarker(TypedDict): + Name: str + Color: str + Code: int + Size: float + + +class DSSPlotPhases(IntEnum): + PROFILE3PH = -1 # Default + PROFILEALL = -2 # All + PROFILEALLPRI = -3 # Primary + PROFILELL3PH = -4 # LL3Ph + PROFILELLALL = -5 # LLAll + PROFILELLPRI = -6 # LLPrimary + + +class PlotParams(TypedDict): + PlotType: DSSPlotType + MatrixType: str + MaxScale: float + MinScale: float + Dots: bool + Labels: bool + ShowLoops: bool + ShowSubs: bool + Quantity: str + ObjectName: str + PlotId: str #TODO + ValueIndex: int + PhasesToPlot: DSSPlotPhases + ProfileScale: str + Channels: List[int] + Bases: Optional[List[float]] + SinglePhLineStyle: int + ThreePhLineStyle: int + Color1: str + Color2: str + Color3: str + TriColorMax: float + TriColorMid: float + MaxScaleIsSpecified: bool + MinScaleIsSpecified: bool + DaisyBusList: List[str] + DaisySize: float + MaxLineThickness: float + MarkerParams: Optional[ObjMarkers] + BusMarkers: Optional[List[BusMarker]] + + Registers: List[int] + PeakDay: bool + MeterName: str + CaseName: str + CaseYear: int + + +DEFAULT_MARKER_PARAMS = ObjMarkers( + MarkTransformers=False, + TransMarkerCode=None, + TransMarkerSize=None, + + MarkCapacitors=False, + CapMarkerCode=None, + CapMarkerSize=None, + + MarkPVSystems=False, + PVMarkerCode=None, + PVMarkerSize=None, + + MarkStorage=False, + StoreMarkerCode=None, + StoreMarkerSize=None, + + MarkSwitches=False, + SwitchMarkerCode=None, + + MarkFuses=False, + FuseMarkerCode=None, + FuseMarkerSize=None, + + MarkRegulators=False, + RegMarkerCode=None, + RegMarkerSize=None, + + MarkRelays=False, + RelayMarkerCode=None, + RelayMarkerSize=None, + + MarkReclosers=False, + RecloserMarkerCode=None, + RecloserMarkerSize=None, +) + +DEFAULT_PLOT_PARAMS = PlotParams( + PlotType=DSSPlotType.Circuit, + Quantity=DSSPlotQuantity.Powers, + Channels=[1, 3, 5], + MarkerParams=DEFAULT_MARKER_PARAMS, + BusMarkers=[], + Color1='#0000FF', + Color2='#008000', + Color3='#FF0000', + TriColorMax=0.85, + TriColorMid=0.50, + ThreePhLineStyle=1, + SinglePhLineStyle=1, + ProfileScale=ProfileScale.pukm, + PhasesToPlot=DSSPlotPhases.PROFILE3PH, + DaisyBusList=[], + MaxLineThickness=10, + Dots=False, + Labels=False, + ShowLoops=False, + ShowSubs=False, + MinScaleIsSpecified=False, + MaxScaleIsSpecified=False, + MinScale=0.0, + MaxScale=None, +) + +try: + from IPython import get_ipython + from IPython.display import FileLink, display, display_html, HTML + from IPython.core.magic import register_cell_magic + ipython = get_ipython() + if ipython is None: + raise ImportError + + import html + + def link_file(fn): + relfn = os.path.relpath(fn, os.getcwd()) + if relfn.startswith('..'): + # cannot show in the notebook :( + display(HTML(f'

File output ("{html.escape(relfn)}") outside current workspace.

')) + else: + display(FileLink(relfn, result_html_prefix=f'File output ("{html.escape(fn)}"): ')) + + def show(text): + display(text) + + + @register_cell_magic + def dss(line, cell): + if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_odd: + DSSPlotCtx.Text.Commands(cell) + else: + for line in cell.split('\n'): + DSSPlotCtx(line) + res = DSSPlotCtx.Text.Result + if res.endswith('.DSV'): + if _enabled and FilePath(res).exists(): + plot_dsv(res) + + DSSPlotCtx.AllowChangeDir = False +except: + def link_file(fn): + print(f'Output file: "{fn}"') + + def show(text): + print(text) + + + #FileLink('path_to_file/filename.extension') + +# import os +# import html +# import tqdm +# from tqdm.notebook import tqdm +# import IPython.display + +include_3d = '2d' # '2d' (default), '3d' (prefer 3d), 'both' + +str_to_pq = { + 'Voltages': pqVoltage, + 'Currents': pqCurrent, + 'Powers': pqPower, + 'Losses': pqLosses, + 'Capacities': pqCapacity, +} + +quantity_str = {v: k for k, v in str_to_pq.items()} +quantity_str[pqLosses] = 'Loss Density' + +str_to_pq.update({ + 'Voltage': pqVoltage, + 'Current': pqCurrent, + 'Power': pqPower, + 'Loss': pqLosses, + 'Capacity': pqCapacity +}) + +# Markers +DSS_MARKER_37 = Path([(0.0, -0.5), (0.0, 0.196), (-0.36, -0.5), (0.361, -0.5)], [1, 2, 1, 2]) +DSS_MARKER_38 = Path([(0.0, -0.23), (0.0, 0.196), (-0.36, 0.196), (0.361, 0.196), (-0.36, -0.23), (0.361, -0.23)], [1, 2, 1, 2, 1, 2]) +DSS_MARKER_20 = Path([(-0.23, -0.147), (0.0, 0.13), (0.23, -0.147)], [1, 2, 2]) +DSS_MARKER_21 = Path([(-0.28, -0.147), (0.0, 0.13), (0.28, -0.147)], [1, 2, 2]) +DSS_MARKER_22 = Path([(-0.23, 0.147), (0.0, -0.13), (0.23, 0.147)], [1, 2, 2]) +DSS_MARKER_23 = Path([(-0.28, 0.147), (0.0, -0.13), (0.28, 0.147)], [1, 2, 2]) + +MARKER_MAP = { + # marker, size multiplier (1=normal, 2=small, 3=tiny), fill + 0: (',', 1, 1), + 1: ('+', 3, 1), + 2: ('+', 2, 1), + 3: ('+', 1, 1), + 4: ('x', 3, 1), + 5: ('x', 2, 1), + 6: ('x', 1, 1), + 7: ('s', 3, 1), + 8: ('s', 2, 1), + 9: ('s', 1, 1), + 10: ('s', 3, 0), + 11: ('s', 2, 0), + 12: ('s', 1, 0), + 13: ('D', 3, 1), + 14: ('D', 2, 1), + 15: ('D', 1, 1), + 16: ('o', 2, 0), + 17: ('o', 1, 0), + 18: ('s', 1, 0.5), + 19: ('D', 1, 0), + 20: (DSS_MARKER_20, 2, 0), + 21: (DSS_MARKER_21, 1, 0), + 22: (DSS_MARKER_22, 2, 0), + 23: (DSS_MARKER_23, 1, 0), + 24: ('o', 1, 1), + 25: ('X', 1, 1), + 26: ('o', 2, 1), + 27: ('o', 3, 0), + 28: ('o', 3, 1), + 29: (DSS_MARKER_22, 3, 1), + 30: (DSS_MARKER_23, 2, 1), + 31: ('v', 3, 0), + 32: ('v', 2, 0), + 33: (7, 1, 0), + 34: (7, 2, 1), + 35: ('^', 1, 0), + 36: (6, 1, 1), + 37: (DSS_MARKER_37, 1, 0), + 38: (DSS_MARKER_38, 1, 0), + 39: ('$⊕$', 1, 1), # normal (circled plus) + 40: (8, 2, 0), # small + 41: (8, 2, 1), # small + 42: (8, 1, 0), # normal + 43: (8, 1, 1), # normal + 44: (9, 2, 0), # small + 45: (9, 2, 1), # small + 46: (9, 1, 0), # normal + 47: (9, 1, 1), # normal +} + +LINES_STYLE_CODE = {1: '-', 2: '--', 3: ':', 4: '-.', 5: (0, (5, 1, 1, 1, 1, 1)), 6: (0, (0, 1))} + +Colors = [ + '#000000', + '#FF0000', + '#0000FF', + '#FF00FF', + '#008000', + '#80FF00', + '#FF8040', + '#DADE21', + '#B56AFF', + '#804000', + '#808000', + '#0000A0', + '#FF8080', + '#000080', + '#7F7F7F', + '#8E0F7B', + '#07968E' +] + +sizes = np.array([0, 9, 6, 4], dtype=float) * 0.7 + +MARKER_SEQ = (5, 15, 2, 8, 26, 36, 39, 19, 18) + + +DSV_LINE_STYLES = { + 0: 'solid', + 1: 'dashed', + 2: 'dotted', + 3: 'dashdot', + 4: (0, (3, 5, 1, 5, 1, 5)), +} + +DSS_ITEMS = { + 'BoldLabel', + 'Caption', + 'Center', + 'ChartCaption', + 'Circle', + 'ClickOn', + 'Curve', + 'DataColor', + 'Draw', + 'FStyle', + 'KeepAspect', + 'KeyClass', + 'Label', + 'Line', + 'Marker', + 'Move', + 'NoScales', + 'PctRim', + 'Range', + 'Rect', + 'SetProp', + 'Text', + 'TxtAlign', + 'Width', + 'Xlabel', + 'Ylabel', +} + +def get_marker_dict(dss_code): + marker, size, fill = MARKER_MAP[dss_code] + res = dict( + marker=marker, + markersize=sizes[size], + markerfacecolor=None if fill else 'none', + # fillstyle='full' if fill else 'none', + alpha=0.5 if fill not in (0, 1) else 1, + markeredgecolor='none' if dss_code == 39 else None, + markeredgewidth=1 + ) + for k, v in list(res.items()): + if v is None: + del res[k] + + return res + +def nodot(b): + return b.split('.', 1)[0] + +def unquote(field: str): + field = field.strip() + if field[0] == '"' and field[-1] == '"': + return field[1:-1] + + return field + +node_re = re.compile(r'(.*?)(\.[0-9])*$') + +def remove_nodes(bus): + match = node_re.match(bus) + return match.group(1) + +def _int_to_color(v: int): + return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) + +class ToggleAdvancedTypes: + def __init__(self, dss: IDSS, value: bool): + self._value = value + self._dss = dss + self._previous = self._dss.AdvancedTypes + + def __enter__(self): + if self._value != self._previous: + self._dss.AdvancedTypes = self._value + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._value != self._previous: + self._dss.AdvancedTypes = self._previous + + +class DSVHandler: + def __init__(self, fn: Union[str, FilePath]): + self.fn = fn + self.fig, self.ax = plt.subplots() + self.ax.get_xaxis().get_major_formatter().set_scientific(False) + self.ax.get_yaxis().get_major_formatter().set_scientific(False) + self.xy = [0.0, 0.0] + self.line_width = 1 + self.fig_caption = None + self.color = 'k' + self.key_class = None + self.no_scales = False + self.bold = True + self.txt_align = 'left' + + + def BoldLabel(self, param_str: str): + self.bold = int(param_str.strip()) != 0 + + + def Caption(self, param_str: str): + self.fig_caption = param_str.strip().strip('"') + self.fig.canvas.manager.set_window_title(self.fig_caption) + + + def ChartCaption(self, param_str: str): + self.ax.set_title(param_str.strip().strip('"')) + + + def Center(self, param_str: str): + *int_params, text = param_str.split(',') + x, y, s = [int(v.strip()) for v in int_params] + text = text.strip().strip('"') + if '/_' in text: + text = text.replace('/_', '∠') + '°' + + if '->' in text: + text = text.replace('->', '→') + s = s * 1.5 + elif '<-' in text: + text = text.replace('<-', '←') + s = s * 1.5 + elif '^' in text: + text = text.replace('^', '↑') + s = s * 1.5 + + self.ax.text(x, y, text, horizontalalignment='center', fontsize=s * 8 / 13.) + + + def Circle(self, param_str: str): + params = param_str.split(',') + x, y = float(params[0]), float(params[1]) + fc = _int_to_color(int(params[4])) + ec = _int_to_color(int(params[3])) + self.ax.scatter(x, y, marker='o', color=fc, edgecolors=ec, s=50, zorder=10, linewidths=0.5) + + + def ClickOn(self, param_str: str): + #TODO + pass + + + def Curve(self, param_str: str): + *int_params, curve_name, rest = param_str.split(',', 7) + npts, color, width, style, curve_markers, curve_marker = [int(v.strip()) for v in int_params] + if curve_markers: + marker_dict = get_marker_dict(curve_marker) + else: + marker_dict = {} + + data = np.fromstring(rest, dtype=float, sep=',') + self.ax.plot(data[:npts], data[npts:], lw=width/2.0, label=curve_name.strip().strip('"'), color=_int_to_color(color), ls=DSV_LINE_STYLES[style], **marker_dict) + # self.ax.minorticks_on() + + + def DataColor(self, param_str: str): + self.color = _int_to_color(int(param_str)) + + + def Draw(self, param_str: str): + if not self.no_scales: + # Currently not used since Move/Draw is emulated with axhline + return + + x0, y0 = self.xy + x1, y1 = [float(v.strip().strip('"')) for v in param_str.split(',')] + self.ax.plot([x0, x1], [y0, y1], color=self.color, lw=self.line_width/2.0) + + + def FStyle(self, param_str: str): + fstyle = int(param_str.strip().strip('"')) + # if fstyle != 0: + # print('Unhandled font style:', fstyle) + + + def KeepAspect(self, param_str: str): + try: + v = int(param_str.strip().strip('"')) + except: + v = 1 + + if v: + self.ax.set_aspect('equal', 'datalim') + else: + self.ax.set_aspect('auto') + + + def KeyClass(self, param_str: str): + self.key_class = int(param_str.strip()) + + + def Label(self, param_str: str): + *int_params, text, _ = param_str.split(',') + x, y, color_int = [int(v.strip()) for v in int_params] + color = _int_to_color(color_int) + text = text.strip().strip('"') + self.ax.text(x, y, text, + horizontalalignment='center', + fontsize=10 * 8 / 13., + color=color, + backgroundcolor='white', + weight='bold' if self.bold else 'normal' + ) + + + def Line(self, param_str: str): + #TODO: use LineCollection + + *str_params, rest = param_str.split(',', 3) + line_name, bus1, bus2 = [v.strip().strip('"') for v in str_params] + *int_params, rest = rest.split(',', 4) + offset, data_count, num_cust, total_cust = [int(v) for v in int_params] + *dbl_params, rest = rest.split(',', 6) + kv, dist, x1, y1, x2, y2 = [float(v) for v in dbl_params] + int_params = rest.split(',') + #TODO: markers + color, width, style, dots, mark_center, center_marker_code, node_marker_code, node_marker_size = [int(v) for v in int_params] + + if dots: + node_marker_dict = get_marker_dict(node_marker_code) + node_marker_dict['markersize'] *= max(1, np.sqrt(node_marker_size) - 1) * node_marker_dict['markersize'] / 7.0 + else: + node_marker_dict = {} + + self.ax.plot([x1, x2], [y1, y2], color=_int_to_color(color), lw=width / 2.0, ls=DSV_LINE_STYLES[style], solid_capstyle='round', **node_marker_dict) + + if mark_center: + center_marker_dict = get_marker_dict(center_marker_code) + self.ax.scatter((x1 + x2) / 2, (y1 + y2) / 2, color=_int_to_color(color), **center_marker_dict) + + + def Marker(self, param_str: str): + params = param_str.split(',') + x, y = float(params[0]), float(params[1]) + c, symbol, marker_size = [int(v) for v in params[2:]] + marker_dict = get_marker_dict(symbol) + marker_dict['markersize'] *= max(1, np.sqrt(marker_size) - 1) * marker_dict['markersize'] / 7.0 + self.ax.plot(x, y, ls=None, color=_int_to_color(c), **marker_dict) + + + def Move(self, param_str: str): + x, y = [float(v.strip().strip('"')) for v in param_str.split(',')] + if self.no_scales: + self.xy = [x, y] + else: + self.ax.axhline(y, color=self.color, lw=self.line_width / 2.0) + + + def NoScales(self, param_str: str): + self.no_scales = True + self.ax.get_xaxis().set_visible(False) + self.ax.get_yaxis().set_visible(False) + + + def PctRim(self, param_str: str): + self.ax.margins(float(param_str) / 100.0) + + + def Range(self, param_str: str): + pass + + + def Rect(self, param_str: str): + left, bottom, right, top = [int(v) for v in param_str.split(',')] + r = patches.Rectangle((left, bottom), right - left, top - bottom, fill=True, ec='k', fc='#c0c0c0') + self.ax.add_patch(r) + + + def SetProp(self, param_str: str): + if int(param_str.rsplit(',', 1)[-1]) != 0: + self.ax.grid(which='both', ls='--') + else: + self.ax.grid(False) + + + def Text(self, param_str: str): + *int_params, text = param_str.split(',') + x, y, c, s = [int(v.strip()) for v in int_params] + text = text.strip().strip('"') + self.ax.text(x, y, text, ha=self.txt_align, va='center', fontsize=s * 10 / 13.) + + + def TxtAlign(self, param_str: str): + v = int(param_str) + if v == 1: + self.txt_align = 'left' + return + + if v == 2: + self.txt_align = 'center' + return + + if v == 3: + self.txt_align = 'right' + return + + + def Width(self, param_str: str): + self.line_width = int(param_str.strip().strip('"')) + + + def Xlabel(self, param_str: str): + self.ax.set_xlabel(param_str.strip().strip('"')) + + + def Ylabel(self, param_str: str): + self.ax.set_ylabel(param_str.strip().strip('"')) + + def parse(self): + with open(self.fn, 'r') as f: + for l in f: + l = l.strip() + if not l: + continue + + item_name, *rest = l.split(',', 1) + item_name = item_name.strip() + if item_name not in DSS_ITEMS: + raise NotImplemented(f'"{item_name}" DSV item is not implemented') + + # print(item, repr(rest)[:100]) + getattr(self, item_name)(rest[0] if rest else '') # let the exception propagate on error + + if _do_show: + self.fig.show() + else: + return self.fig, self.ax + + +class DSSMPLPlotter: + def __init__(self, dss: IDSS): + self.dss = dss + + def dss_monitor_plot(DSS: IDSS, + *, + ObjectName: str = None, + Channels: List[int] = None, # TODO: allow channel names too + Bases: List[float] = None, + **kwargs: Unpack[PlotParams] + ): + monitor = DSS.ActiveCircuit.Monitors + monitor.Name = ObjectName + data = monitor.AsMatrix() + if data is None or len(data) == 0: + raise ValueError("There is not data to plot in the monitor. Hint: check the solution mode, solve the circuit and retry.") + + channels = Channels + num_ch = monitor.NumChannels + channels = [ch for ch in channels if ch >= 1 and ch <= num_ch] + if len(channels) == 0: + raise IndexError("No valid channel numbers were specified.") + + bases = Bases + header = list(monitor.Header) + if len(monitor.dblHour) < len(monitor.dblFreq): + header.insert(0, 'Frequency') + header.insert(1, 'Harmonic') + xlabel = 'Frequency (Hz)' + h = data[:, 0] + else: + header.insert(0, 'Hour') + header.insert(1, 'Seconds') + h = data[:, 0] * 3600 + data[:, 1] + total_seconds = max(h) - min(h) + if total_seconds < 7200: + xlabel = 'Time (s)' + else: + xlabel = 'Time (h)' + h /= 3600 + + separate = False + if separate: + fig, axs = plt.subplots(len(channels), sharex=True)#, figsize=(8, 9)) + icolor = -1 + for ax, base, ch in zip(axs, bases, channels): + ch += 1 + icolor += 1 + ax.plot(h, data[:, ch] / base, color=Colors[icolor % len(Colors)]) + ax.grid() + ax.set_ylabel(header[ch]) + + else: + fig, ax = plt.subplots(1) + icolor = -1 + for base, ch in zip(bases, channels): + ch += 1 + icolor += 1 + ax.plot(h, data[:, ch] / base, label=header[ch], color=Colors[icolor % len(Colors)]) + + ax.grid() + ax.legend() + ax.set_ylabel('Mag') # Where "Mag" comes from? + + ax.set_title(ObjectName) + ax.set_xlabel(xlabel) + + + def dss_tshape_plot(DSS: IDSS, + *, + ObjectName: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] + ): + # There is no dedicated API yet but we can move to the Obj API + name = ObjectName + DSS.Text.Command = f'? tshape.{name}.temp' + p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + try: + DSS.Text.Command = f'? tshape.{name}.hour' + h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + except: + h = np.array([]) + + try: + interval = f'? tshape.{name}.interval' # hours + interval = float(DSS.Text.Result) + except: + interval = 1 + + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"TShape.{ObjectName}") + + if not h.size: + h = interval * np.array(range(len(p))) + + x_unit = 'h' + if h[-1] < 1: + h *= 3600 + x_unit = 's' + + color1 = Color1 + ax.plot(h, p, color=color1, label="Price") + ax.set_title(f"TShape = {ObjectName}") + ax.set_xlabel(f'Time ({x_unit})') + ax.set_ylabel('Temperature') + + ax.grid(ls='--') + plt.tight_layout() + + + + def dss_priceshape_plot(DSS: IDSS, + *, + ObjectName: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] + ): + # There is no dedicated API yet but we can move to the Obj API + name = ObjectName + DSS.Text.Command = f'? priceshape.{name}.price' + p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + try: + DSS.Text.Command = f'? priceshape.{name}.hour' + h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') + except: + h = np.array([]) + + try: + interval = f'? priceshape.{name}.interval' # hours + interval = float(DSS.Text.Result) + except: + interval = 1 + + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"PriceShape.{ObjectName}") + + if not h.size: + h = interval * np.array(range(len(p))) + + x_unit = 'h' + if h[-1] < 1: + h *= 3600 + x_unit = 's' + + color1 = Color1 + + ax.plot(h, p, color=color1, label="Price") + ax.set_title(f"PriceShape = {ObjectName}") + ax.set_xlabel(f'Time ({x_unit})') + ax.set_ylabel('Price') + + ax.grid(ls='--') + plt.tight_layout() + + + def dss_loadshape_plot(DSS: IDSS, + *, + ObjectName: str = None, + Color1: str = None, + Color2: str = None, + **kwargs: Unpack[PlotParams] + ): + # pprint(kwargs) + + ls = DSS.ActiveCircuit.LoadShapes + ls.Name = ObjectName + h = asarray(ls.TimeArray) + p = asarray(ls.Pmult) + q = asarray(ls.Qmult) + + fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"LoadShape.{ObjectName}") + + if not h.size or h is None or len(h) != len(p): + h = ls.HrInterval * np.array(range(len(p))) + + x_unit = 'h' + if h[-1] < 1: + h *= 3600 + x_unit = 's' + + color1 = Color1 + color2 = Color2 + + ax.plot(h, p, color=color1, label="Pmult") + if q.size == p.size: + ax.plot(h, q, color=color2, label="Qmult") + + ax.set_title(f"LoadShape = {ObjectName}") + ax.set_xlabel(f'Time ({x_unit})') + if ls.UseActual: + if q.size == p.size: + ax.set_ylabel('kW, kvar') + else: + ax.set_ylabel('kW') + else: + ax.set_ylabel('p.u.') + + ax.grid(ls='--') + if q.size == p.size: + ax.legend() + plt.tight_layout() + + + def _get_branch_data(DSS: IDSS, + branch_objects: DSSIterable, + bus_coords: Dict[str, Tuple[float, float, float]], + do_values=pqNone, + do_switches=False, + idxs=None, + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 + ): + line_count = branch_objects.Count if not idxs else len(idxs) + lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) + lines.fill(np.nan) + values = np.empty(shape=(line_count, ), dtype=np.float64) + values.fill(np.nan) + lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) + + element = DSS.ActiveCircuit.ActiveCktElement + + if do_switches: + switch_idxs = [] + isolated_idxs = [] + try: + element.IsIsolated + has_is_isolated = True + except: + has_is_isolated = False + isolated_names = set(name.lower() for name in DSS.ActiveCircuit.Topology.AllIsolatedBranches if name) + + extra = [switch_idxs, isolated_idxs] + else: + extra = [] + # def get_buses_line(l): + # b1 = remove_nodes(l.Bus1) + # b2 = remove_nodes(l.Bus2) + + offset = 0 + skip = set() + + # norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu + # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu + # emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu + # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu + + vbs = None + if do_values == pqCurrent: + # Currently the same as pqCapacity to match the OpenDSS impl.; the correct would be: + #max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllMaxCurrents(True))) + try: + max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + except: + max_currents = {} + elem = DSS.ActiveCircuit.ActiveCktElement + for _ in DSS.ActiveCircuit.PDElements: + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + norm_amps = elem.NormalAmps + max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + + elif do_values == pqCapacity: + try: + capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + except: + max_currents = {} + elem = DSS.ActiveCircuit.ActiveCktElement + for _ in DSS.ActiveCircuit.PDElements: + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + norm_amps = elem.NormalAmps + max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + + elif do_values == pqVoltage: + node_volts = dict(zip(DSS.ActiveCircuit.AllNodeNames, asarray(DSS.ActiveCircuit.AllBusVmag) * 1e-3)) + vbs = np.empty(shape=(line_count, ), dtype=np.float64) + vbs.fill(0) + extra.append(vbs) + + if idxs: + l = branch_objects + for idx in idxs: + l.idx = idx + buses = element.BusNames + b1 = remove_nodes(buses[0]) + b2 = remove_nodes(buses[1]) + + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None: + skip.add(idx) + continue + + lines[offset, 0] = fr + lines[offset, 1] = to + offset += 1 + + if do_values == pqNone: + return lines[:offset] + + offset = 0 + for idx in idxs: + if idx in skip: + continue + + l.idx = idx + + if do_values == pqPower: + values[offset] = np.abs(element.TotalPowers[0]) + elif do_values == pqLosses: + values[offset] = abs(element.Losses[0]) / l.Length + elif do_values == pqVoltage: + b2name = nodot(l.Bus2) + b = DSS.ActiveCircuit.Buses[b2name] + vb = b.kVBase + vbs[offset] = vb + value = 1e30 + if vb > 0: + for n in b.Nodes: + if n > 0 and n <= 3: + value = min(value, node_volts[f'{b2name}.{n}'] / vb) + + values[offset] = value + elif do_values == pqCurrent: + values[offset] = max_currents.get(element.Name, np.NaN) + elif do_values == pqCapacity: + values[offset] = capacities.get(element.Name, np.NaN) + + offset += 1 + + return lines[:offset], values[:offset] + + else: + for i, l in enumerate(branch_objects): + buses = element.BusNames + b1 = remove_nodes(buses[0]) + b2 = remove_nodes(buses[1]) + + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None or not element.Enabled: + skip.add(i) + continue + + if do_switches: + if ((has_is_isolated and element.IsIsolated) or + ((not has_is_isolated) and (element.Name.lower() in isolated_names))): + isolated_idxs.append(offset) + + if l.IsSwitch: + #skip.add(i) + switch_idxs.append(offset) + #continue + + lines[offset, 0] = fr + lines[offset, 1] = to + + offset += 1 + + if do_values == pqNone: + return [lines[:offset], None, None] + extra + + offset = 0 + + for i, l in enumerate(branch_objects): + if i in skip: + continue + + if do_values == pqPower: + values[offset] = np.abs(element.TotalPowers[0]) + elif do_values == pqLosses: + values[offset] = abs(element.Losses[0]) / l.Length + elif do_values == pqVoltage: + b2name = nodot(l.Bus2) + b = DSS.ActiveCircuit.Buses[b2name] + vb = b.kVBase + vbs[offset] = vb + value = 1e30 + + if l.Phases < 3: + lines_styles[offset] = 1 + + if vb > 0: + for n in b.Nodes: + if n > 0 and n <= 3: + value = min(value, node_volts[f'{b2name}.{n}'] / vb) + + values[offset] = value + elif do_values == pqCurrent: + values[offset] = max_currents.get(element.Name, np.NaN) + elif do_values == pqCapacity: + values[offset] = capacities.get(element.Name, np.NaN) + + lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style + offset += 1 + + return [lines[:offset], values[:offset], lines_styles[:offset]] + extra + + + def _get_point_data(DSS: IDSS, + point_objects: Union[str, Iterable], + bus_coords: Dict[str, Tuple[float, float, float]], + do_values: bool = False + ): + if isinstance(point_objects, str): + cls = point_objects + DSS.SetActiveClass(cls) + point_objects = DSS.ActiveClass + + point_count = point_objects.Count + + points = np.empty(shape=(point_count, 2), dtype=np.float64) + values = np.empty(shape=(point_count, ), dtype=np.float64) + + offset = 0 + skip = set() + element = DSS.ActiveCircuit.ActiveCktElement + for i, _ in enumerate(point_objects): + buses = element.BusNames + all_coords = [] + buses = [remove_nodes(b) for b in buses] + all_coords = [c for c in (bus_coords.get(b) for b in buses) if c] + + if not all_coords: + skip.add(i) + continue + + coords = tuple(sum(c) / len(all_coords) for c in zip(*all_coords)) + + points[offset] = coords + offset += 1 + + if not do_values: + return points[:offset] + + offset = 0 + for i, _ in enumerate(point_objects): + if i in skip: + continue + + values[offset] = np.abs(element.TotalPowers[0]) + offset += 1 + + return points[:offset], values[:offset] + + + def dss_profile_plot(DSS: IDSS, + *, + PhasesToPlot: int = None, + ProfileScale: float = None, + **kwargs: Unpack[PlotParams] + ): + if len(DSS.ActiveCircuit.Meters) == 0: + raise RuntimeError(f"An EnergyMeter is required to use 'plot profile'") + + vmin = DSS.ActiveCircuit.Settings.NormVminpu + vmax = DSS.ActiveCircuit.Settings.NormVmaxpu + if ProfileScale == '120kft': + xlabel = 'Distance (kft)' + ylabel = '120 Base Voltage' + DenomLN = 1.0 / 120.0 + # DenomLL = 1.732 / 120.0 + LenScale = 3.2809 + # RangeScale = 120.0 + else: + xlabel = 'Distance (km)' + ylabel = 'p.u. Voltage' + DenomLN = 1.0 + # DenomLL = 1.732 + LenScale = 1.0 + # RangeScale = 1.0 + + busnode_to_index = {(bn.rsplit('.', 1)[0], int(bn.rsplit('.', 1)[1])): num for (num, bn) in enumerate(DSS.ActiveCircuit.AllNodeNames)} + bus_to_kvbase = {b.Name: b.kVBase for b in DSS.ActiveCircuit.Buses} + puV = asarray(DSS.ActiveCircuit.AllBusVmagPu) / DenomLN + distances = {name: d for (name, d) in zip(DSS.ActiveCircuit.AllBusNames, asarray(DSS.ActiveCircuit.AllBusDistances) * LenScale)} + linewidths = [] + segments = [] + colors = [] + linestyles = [] + seg_phases = [] + pri_only = (PhasesToPlot == DSSPlotPhases.PROFILEALLPRI) + if PhasesToPlot in [DSSPlotPhases.PROFILEALL, DSSPlotPhases.PROFILEALLPRI, DSSPlotPhases.PROFILE3PH]: + phases = (1, 2, 3) + else: + phases = PhasesToPlot + try: + _ = iter(phases) + except: + phases = [phases] + + for em in DSS.ActiveCircuit.Meters: + branch_names = em.AllBranchesInZone + br: str + for br in branch_names: + if not br.startswith('Line.'): + continue + + ls = '-' + lw = 2 + + DSS.ActiveCircuit.Lines.Name = br[len('Line.'):] + + if DSSPlotPhases.PROFILE3PH == PhasesToPlot and DSS.ActiveCircuit.Lines.Phases < 3: + continue + + bus1 = nodot(DSS.ActiveCircuit.Lines.Bus1) + bus2 = nodot(DSS.ActiveCircuit.Lines.Bus2) + + # Plot all phases present (between 1 and 3) + for iphs in phases: + try: + b1n_idx = busnode_to_index[(bus1, iphs)] + b2n_idx = busnode_to_index[(bus2, iphs)] + except: + continue + + if bus_to_kvbase[bus1] < 1.0: + if pri_only: + continue + ls = ':' + lw = 1 + + segments.append(((distances[bus1], puV[b1n_idx]), (distances[bus2], puV[b2n_idx]))) + colors.append(Colors[iphs - 1]) + seg_phases.append(iphs) + linestyles.append(ls) + linewidths.append(lw) + #TODO: NodeMarkerCode, NodeMarkerWidth + + if include_3d in ('both', '2d'): + fig = plt.figure()#figsize=(9, 5)) + ax = fig.add_subplot(1, 1, 1) + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): + ax.set_title('L-L Voltage Profile') + else: + ax.set_title('L-N Voltage Profile') + + + lc = LineCollection(segments, linewidth=linewidths, colors=colors, linestyles=linestyles) + + # ax.set_title('{}:{}, max: {:3g}'.format(DSS.ActiveCircuit.Name, quantity, quantity_max_value)) + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + ax.add_collection(lc) + ax.autoscale_view() + ax.axhline(vmin, color='darkred', ls='-', lw=3) + ax.axhline(vmax, color='darkred', ls='-', lw=3) + ax.grid(ls='--') + plt.tight_layout() + + if include_3d in ('both', '3d'): + fig2 = plt.figure()#figsize=(7, 7)) + ax2 = fig2.add_subplot(1, 1, 1, projection='3d') + ax2.set_xlabel(xlabel) + ax2.set_ylabel(ylabel) + if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): + ax2.set_title('L-L Voltage Profile') + else: + ax2.set_title('L-N Voltage Profile') + + segments_3d = [ + [(*p, ph) for p in seg] for seg, ph in zip(segments, seg_phases) + ] + rseg = np.ravel(segments) + max_x = np.max(rseg[::2]) + max_y = np.max(rseg[1::2]) + min_y = np.min(rseg[1::2]) + lc3d = Line3DCollection(segments_3d, colors=colors, linestyles=linestyles) + ax2.add_collection(lc3d) + ax2.set_xlabel(xlabel) + ax2.set_ylabel(ylabel) + ax2.set_zlabel('Phase') + xl = [0, max_x] + yl = [min(min_y, vmin) - 0.05, min(max_y, vmax) + 0.05] + maxph = np.max(seg_phases) + 1 + ax2.set_xlim(xl) + ax2.set_ylim(yl) + ax2.set_zlim(0, maxph) + ax2.plot_surface( + np.array([xl, xl]), + np.array([[vmax, vmax]] * 2), + np.array([[0, 0], [maxph, maxph]]), + color='k', + alpha=0.5 + ) + ax2.plot_surface( + np.array([xl, xl]), + np.array([[vmin, vmin]] * 2), + np.array([[0, 0], [maxph, maxph]]), + color='k', + alpha=0.5 + ) + ax2.autoscale_view() + + + def _get_gic_line_data_altdss( + altdss: IAltDSS, + bus_coords: Dict[str, Tuple[float, float, float]], + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 + ): + branch_objects = altdss.GICLine + line_count = len(branch_objects)# if not idxs else len(idxs) + lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) + lines.fill(np.nan) + values = np.empty(shape=(line_count, ), dtype=np.float64) + values.fill(np.nan) + lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) + offset = 0 + # skip = set() + + # GIC lines are not exposed nicely in the classic API, so we'll use the new Obj API + for gic_line in altdss.GICLine: + if not gic_line.enabled: + continue + + b1 = remove_nodes(gic_line.bus1) + b2 = remove_nodes(gic_line.bus2) + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None: + # skip.add(idx) + continue + + lines[offset, 0] = fr + lines[offset, 1] = to + + lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style + values[offset] = gic_line.MaxCurrent(1) + offset += 1 + + return lines[:offset], values[:offset], lines_styles[:offset] + + + def _get_gic_line_data(DSS: IDSS, + bus_coords: Dict[str, Tuple[float, float]], + single_ph_line_style: int = 1, + three_ph_line_style: int = 1 + ): + try: + return _get_gic_line_data_altdss( + DSS.to_altdss(), + bus_coords, + single_ph_line_style=single_ph_line_style, + three_ph_line_style=three_ph_line_style + ) + except: + pass + + # Fallback for Oddie and COM + DSS.ActiveCircuit.SetActiveClass('GICLine') + aclass = DSS.ActiveCircuit.ActiveClass + line_count = aclass.Count# if not idxs else len(idxs) + lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) + lines.fill(np.nan) + values = np.empty(shape=(line_count, ), dtype=np.float64) + values.fill(np.nan) + lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) + offset = 0 + # skip = set() + + # GIC lines are not exposed nicely in the classic API + elem = DSS.ActiveCircuit.ActiveCktElement + idx = aclass.First + while idx != 0: + buses = elem.BusNames + b1 = remove_nodes(buses[0]) + b2 = remove_nodes(buses[1]) + fr = bus_coords.get(b1) + to = bus_coords.get(b2) + + if fr is None or to is None: + # skip.add(idx) + continue + + lines[offset, 0] = fr + lines[offset, 1] = to + + lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style + currents = np.abs(asarray(elem.Currents).view(dtype=complex)) + max_current = np.max(currents[:elem.NumConductors]) + values[offset] = max_current + offset += 1 + + return lines[:offset], values[:offset], lines_styles[:offset] + + + def dss_circuit_plot(DSS: IDSS, + *, + fig=None, + ax=None, + is3d=False, + Quantity: str = None, + Dots: bool = False, + Color1: str = None, + Color2: str = None, + Color3: str = None, + SinglePhLineStyle: int = None, + ThreePhLineStyle: int = None, + MaxLineThickness: float = None, + BusMarkers: List[BusMarker] = None, + Labels: bool = None, + Markers: ObjMarkers = None, + MaxScale: float = None, + MaxScaleIsSpecified: bool = None, + **kwargs: Unpack[PlotParams] + ): + if not MaxScaleIsSpecified: + MaxScale = None + + quantity = str_to_pq.get(Quantity, pqNone) + dots = Dots + color1 = Color1 + color2 = Color2 + color3 = Color3 + single_ph_line_style = SinglePhLineStyle + three_ph_line_style = ThreePhLineStyle + max_lw = MaxLineThickness + bus_markers = BusMarkers or [] + do_labels = Labels + + norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu + # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu + emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu + # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu + + # bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if (b.x, b.y) != (0.0, 0.0)) + bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if b.Coorddefined) + + if fig is None: + fig = plt.figure()#figsize=(8, 7)) + + given_ax = ax is not None + if not given_ax: + ax = plt.gca() + else: + plt.sca(ax) + + if not is3d: + ax.set_aspect('equal', 'datalim') + + lines_lines, lines_values, lines_styles, switch_idxs, isolated_idxs, *extra = self._get_branch_data( + DSS, + DSS.ActiveCircuit.Lines, + bus_coords, + do_values=quantity, + do_switches=True, + single_ph_line_style=single_ph_line_style, + three_ph_line_style=three_ph_line_style + ) + + if isolated_idxs: + line_idx = isolated_idxs + if not is3d: + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#ff00ff', capstyle='round')) + + if switch_idxs: + line_idx = switch_idxs + if not is3d: + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#000000', capstyle='round')) + + switch_idxs = set(switch_idxs) + isolated_idxs = set(isolated_idxs) + #lc_lines = LineCollection(lines_lines, linewidths=0.5, color=color1)# + 3 * lines_values / np.max(lines_values), linestyle='solid', color=color1) + quantity_max_value = MaxScale if MaxScale is not None else 0.0 + + quantity_suffix = '' + + if lines_lines is not None and len(lines_lines) > 0: + if quantity in (pqVoltage,): + colors = [] + for v in lines_values: + if v > norm_min_volts or np.isnan(v): + colors.append(color1) + elif v > emerg_min_volts: + colors.append(color2) + else: + colors.append(color3) + + + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] + if not is3d: + edgecolors = [colors[i] for i in line_idx] + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=edgecolors, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) + + # if is3d: + # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) + # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) + # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) + + quantity_max_value = 0 + elif quantity in (pqLosses,): + + if quantity_max_value == 0: + # quantity_max_value = max(lines_values) * 1e-3 + # For compatibility with the official version, loop through all lines instead + # of the actual plotted lines + element = DSS.ActiveCircuit.ActiveCktElement + quantity_max_value = max( + abs(element.Losses[0] / line.Length) + for line in DSS.ActiveCircuit.Lines + if element.Enabled + ) * 0.001 + + lines_values = np.clip(3 * 1e-3 * lines_values / quantity_max_value, 0.5, max_lw) + if not is3d: + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] + # edgecolors = [colors[i] for i in line_idx] + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + + elif quantity in (pqCurrent, pqCapacity): + line_idx = [i for i in range(lines_lines.shape[0]) if i not in isolated_idxs and i not in switch_idxs] + colors = [color3 if v > 100 and not np.isnan(v) else color1 for v in lines_values[line_idx]] + + if quantity_max_value == 0: + quantity_max_value = max(lines_values) + + lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) + if not is3d: + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle='-', color=colors, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) + + elif quantity != pqNone: + if quantity == pqPower: + quantity_suffix = ' kW' + if quantity_max_value == 0: + #lines_values *= 1e-3 + + # For compatibility with the official version, loop through all lines instead + # of the actual plotted lines + element = DSS.ActiveCircuit.ActiveCktElement + + quantity_max_value = max( + element.TotalPowers[0] + for _ in DSS.ActiveCircuit.Lines + if element.Enabled + ) #* 0.001 + else: + #TODO:may need workaround about GeneralPlotQuantity + quantity_max_value = max(lines_values) + + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] + if not is3d: + ax.add_collection(LineCollection( + lines_lines[line_idx, :], + linewidths=np.clip(0.5 + 3 * lines_values[line_idx] / quantity_max_value, 0.5, max_lw), + linestyle=LINES_STYLE_CODE.get(ls, 'solid'), + color=color1, + capstyle='round' + )) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + else: + #TODO: handle 1 and 3 phase, etc.? + if not is3d: + ax.add_collection(LineCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) + # else: + # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) + # ax.set_xlim(np.min(lines_lines[:, :, 0]), np.max(lines_lines[:, :, 0])) + # ax.set_ylim(np.min(lines_lines[:, :, 1]), np.max(lines_lines[:, :, 1])) + + transformers_lines, *_ = self._get_branch_data(DSS, DSS.ActiveCircuit.Transformers, bus_coords) + + if not is3d: + lc_transformers = LineCollection(transformers_lines, linewidth=3, linestyle='solid', color='gray') + ax.add_collection(lc_transformers) + + lines_lines, lines_values, lines_styles, *_ = self._get_gic_line_data(DSS, bus_coords, single_ph_line_style=single_ph_line_style, three_ph_line_style=three_ph_line_style) + if len(lines_lines) != 0: + if quantity_max_value == 0: + quantity_max_value = max(lines_values) + + lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) + for ls in set(lines_styles): + line_idx = [i for i, c in enumerate(lines_styles) if c == ls] + ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) + if dots: + ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) + + + + # 'Daisysize' + # 'Markercode', 'Nodewidth' # NodeMarkerCode + + branch_marker_options = [ + ('MarkSwitches', 'SwitchMarkerCode', None, DSS.ActiveCircuit.Lines, switch_idxs), + ('MarkFuses', 'FuseMarkerCode', 'FuseMarkerSize', DSS.ActiveCircuit.Fuses, None), + ('MarkRegulators', 'RegMarkerCode', 'RegMarkerSize', DSS.ActiveCircuit.RegControls, None), + ('MarkRelays', 'RelayMarkerCode', 'RelayMarkerSize', DSS.ActiveCircuit.Relays, None), + ('MarkReclosers', 'RecloserMarkerCode', 'RecloserMarkerSize', DSS.ActiveCircuit.Reclosers, None) + ] + + point_marker_options = [ + ('MarkTransformers', 'TransMarkerCode', 'TransMarkerSize', DSS.ActiveCircuit.Transformers, None), + ('MarkCapacitors', 'CapMarkerCode', 'CapMarkerSize', DSS.ActiveCircuit.Capacitors, None), + ('MarkPVSystems', 'PVMarkerCode', 'PVMarkerSize', DSS.ActiveCircuit.PVSystems, None), + ('MarkStorage', 'StoreMarkerCode', 'StoreMarkerSize', 'Storage', None), + ] + + pmarkers = Markers + if pmarkers is not None: + for (mark_opt, code_opt, size_opt, objs, idxs) in branch_marker_options: + # print(mark_opt, pmarkers[mark_opt]) + if not pmarkers[mark_opt]: + continue + + marker_code = pmarkers[code_opt] + marker_size = pmarkers[size_opt] + #TODO: use marker_size? + marker_dict = get_marker_dict(marker_code) + if mark_opt == 'MarkRegulators': + for obj in objs: + DSS.ActiveCircuit.Transformers.Name = obj.Transformer + bus = remove_nodes(DSS.ActiveCircuit.ActiveCktElement.BusNames[obj.Winding - 1]) + coords = bus_coords.get(bus) + if coords is None: + continue + ax.plot(*coords, color='red', **marker_dict) + + else: + #TODO? branch_lines = self._get_branch_data(DSS, objs, bus_coords, idxs=idxs) + pass + + + for (mark_opt, code_opt, size_opt, objs, idxs) in point_marker_options: + if not pmarkers[mark_opt]: + continue + + marker_code = pmarkers[code_opt] + marker_size = pmarkers[size_opt] + + points = self._get_point_data(DSS, objs, bus_coords) + + # if marker_code not in MARKER_MAP: + #marker_code = 25 + + marker_dict = get_marker_dict(marker_code) + #marker_dict['markersize'] *= (marker_size / 2.0)**2 + marker_dict['markersize'] *= (marker_size / 1.2)**2 + + #marker_dict['marker'] = marker_dict['marker'].vertices + #marker_dict.pop('markersize') + #marker_dict.pop('markerfacecolor') + # print(mark_opt, marker_dict['marker']) + # pprint(marker_dict) + ax.plot(points[:, 0], points[:, 1], ls='', color='red', **marker_dict) + #ax.plot(points[:, 0], points[:, 1], color='red', ls='', marker=6, alpha=1) + + for bus_marker in bus_markers: + name = bus_marker['Name'] + bus = DSS.ActiveCircuit.Buses[name] + if not bus.Coorddefined: + raise RuntimeError('Bus markers: coordinates are not defined for bus "{name}"') + + marker_dict = get_marker_dict(bus_marker['Code']) + marker_size = bus_marker['Size'] + marker_dict['markersize'] *= (marker_size / 6) + ax.plot(bus.x, bus.y, ls='', color=bus_marker['Color'], **marker_dict) + + + ax.set_xlabel('X') + ax.set_ylabel('Y') + if not given_ax: + if quantity != pqNone: + ax.set_title('{}:{}, max={:g}{}'.format(DSS.ActiveCircuit.Name.upper(), quantity_str[quantity], quantity_max_value, quantity_suffix)) + ax.autoscale_view() + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + plt.tight_layout() + + if do_labels: + coords_to_names = {} + for name, coords in bus_coords.items(): + prev = coords_to_names.get(coords) + if prev: + coords_to_names[coords] = prev + ',' + name + else: + coords_to_names[coords] = name + + for coords, name in coords_to_names.items(): + ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) + + + def dss_scatter_plot(DSS: IDSS, + **kwargs: Unpack[PlotParams] + ): + x = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) + y = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) + vcomplex = np.empty(shape=(DSS.ActiveCircuit.NumBuses, 3), dtype=complex) + x.fill(np.nan) + y.fill(np.nan) + vcomplex.fill(np.nan) + for idx, b in enumerate(DSS.ActiveCircuit.Buses): + if not b.Coorddefined: + continue + + x[idx] = b.x + y[idx] = b.y + vnodes = asarray(b.puVoltages).view(dtype=complex) + nnodes = min(3, len(vnodes)) + vcomplex[idx, :nnodes] = vnodes[:nnodes] + + vabs = np.abs(vcomplex) + del vcomplex + with suppress_warnings(): + vmean = np.mean(vabs, axis=1, where=np.isfinite(vabs)) + + if include_3d in ('both', '2d'): + fig, ax = plt.subplots(1, 1, constrained_layout=True)#, figsize=(8, 7)) + dss_circuit_plot(DSS, fig=fig, ax=ax, Color1='k') + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + sc = ax.scatter(x, y, c=vmean) + fig.colorbar(sc, label='V1 (pu)') + ax.set_title('{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude')) + + if include_3d in ('both', '3d'): + bus_coords = {} + for idx, b in enumerate(DSS.ActiveCircuit.Buses): + if b.Coorddefined: + bus_coords[b.Name] = (b.x, b.y, vmean[idx]) + + fig = plt.figure()#figsize=(7, 7)) + ax = fig.add_subplot(projection='3d') + dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True, Color1='k') + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + + # if is3d: + # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) + # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) + # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) + + sc = ax.scatter(x, y, vmean, c='k', s=2) + + segs = [] + el = DSS.ActiveCircuit.ActiveCktElement + for pd in DSS.ActiveCircuit.PDElements: + buses = el.BusNames + if len(buses) != 2: + continue + + seg = [] + for b in buses: + c = bus_coords.get(nodot(b), None) + if c is not None: + seg.append(c) + + if len(seg) == 2: + segs.append(seg) + + segs = np.array(segs, dtype=float) + seg_v = (segs[:, 0, 2] + segs[:, 1, 2]) / 2 + lc3d = Line3DCollection(segs) + ax.add_collection(lc3d) + lc3d.set_array(seg_v) + #fig.colorbar(sc, label='V1 (pu)') + ax.set_title('{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude')) + + + def dss_visualize_plot(DSS: IDSS, + *, + Quantity: str = None, + ElementType: str = None, + ElementName: str = None, + **kwargs: Unpack[PlotParams] + ): + XMAX = 300 + #pprint(kwargs) + quantity = Quantity + + # Fix for backend v0.13.1 + quantity = { + 'Power': 'Powers', + 'Current': 'Currents', + 'Voltage': 'Voltages', + }.get(quantity, quantity) + + element = DSS.ActiveCircuit.ActiveCktElement + etype, ename = ElementType, ElementName + nconds = element.NumConductors + # nphases = element.NumPhases + buses = element.BusNames[:2] # max 2 terminals + vbases = [max(1, 1000 * DSS.ActiveCircuit.Buses[nodot(b)].kVBase) for b in buses] + + # assert DSS.ActiveCircuit.ActiveCktElement.Name == ElementType + '.' + ElementName + fig, ax = plt.subplots(1, gridspec_kw=dict(left=0.05, right=0.95, bottom=0.05, top=0.92))#, figsize=(8.6, 7)) + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + ax.grid(False) + + y = 20 + 10 * nconds + box_xy0 = np.array([100, 10]) + box_xy1 = np.array([XMAX - 100, y]) + box_wh = box_xy1 - box_xy0 + middle_box = patches.Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') + ax.text(XMAX / 2, 10 + (y - 10) / 2, f'{etype}.{ename.upper()}', ha='center', va='center', fontweight='bold', rotation='vertical') + ax.add_patch(middle_box) + ax.plot([0, 300], [0, 0], color='gray', lw=7) + + ax.plot([-5] * 2, [5, y - 5], color='k', lw=7) + ax.text(25, y, buses[0].upper(), ha='left') + if len(buses) > 1: + ax.plot([XMAX + 5] * 2, [5, y - 5], color='k', lw=7) + ax.text(XMAX - 25, y, buses[1].upper(), ha='right') + + voltage = (quantity == 'Voltages') + + if quantity == 'Powers': + values = 1e-3 * (asarray(element.Voltages).view(dtype=complex) * np.conj(asarray(element.Currents).view(dtype=complex))) + unit = 'kVA' + elif voltage: + values = asarray(element.Voltages).view(dtype=complex) + unit = 'pu' + elif quantity == 'Currents': + values = asarray(element.Currents).view(dtype=complex) + unit = 'A' + + ax.set_title(f'{etype}.{ename.upper()} {quantity} ({unit})') + size = 'x-small' + + def _get_text(): + v = values[bus_idx * nconds + cond] + if quantity == 'Powers': + arrow_text = f"{v.real:-.6g} {'-' if v.imag < 0 else '+'} j{abs(v.imag):g}" + else: + if quantity == 'Voltages': + v /= vbase + arrow_text = f"{np.abs(v):-.6g} {unit} ∠ {np.angle(v, deg=True):.2f}°" + + return arrow_text + + for bus_idx, vbase in enumerate(vbases): + for cond in range(nconds): + if cond < (nconds - 1): + weight = 'bold' + lw = 2 + else: + weight = 'normal' + lw = 0.6667 + + if bus_idx: + arrow_x = XMAX + 5 + arrow_y = y - (cond + 1) * 10.0 + dx = box_xy1[0] - arrow_x + ax.text(arrow_x - 20, arrow_y + 2, _get_text(), ha='right', fontweight=weight, size=size) + if voltage: + plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) + x = XMAX - 4 * (cond) - 1 + ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) + else: + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) + + else: + arrow_x = -5 + arrow_y = y - (cond + 1) * 10.0 + dx = box_xy0[0] + 5 + ax.text(arrow_x + 20, arrow_y + 2, _get_text(), ha='left', fontweight=weight, size=size) + if voltage: + plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) + x = 4 * (cond) + 1 + ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) + else: + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) + + if quantity == 'Currents': + # Residual + v = -np.sum(values[(nconds * bus_idx):(nconds * (bus_idx + 1))]) + txt = f"{np.abs(v):-.6g} A ∠ {np.angle(v, deg=True):.2f}°" + + if bus_idx: + arrow_x = XMAX + 5 + arrow_y = -10 + dx = box_xy1[0] - arrow_x + ax.text(arrow_x - 5, arrow_y + 2, txt, ha='right', fontweight='normal', size=size) + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) + else: + arrow_x = -5 + arrow_y = -10 + dx = box_xy0[0] + 5 + ax.text(arrow_x + 5, arrow_y + 2, txt, ha='left', fontweight='normal', size=size) + ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) + + ax.set_xlim(-20, XMAX + 20) + ax.set_ylim(-15, y + 5) + + + def dss_general_data_plot(DSS: IDSS, + *, + PlotType: str = None, + ObjectName: str = None, + ValueIndex: int = None, + Color1: str = None, + Color2: str = None, + Labels: bool = None, + MinScaleIsSpecified: bool = None, + MaxScaleIsSpecified: bool = None, + MinScale: float = None, + MaxScale: float = None, + + **kwargs: Unpack[PlotParams] + ): + if not MaxScaleIsSpecified: + MaxScale = None + + if not MinScaleIsSpecified: + MinScale = None + + is_general = PlotType == 'GeneralData' + ValueIndex = max(1, ValueIndex - 1) + fn = ObjectName + do_labels = Labels + color1 = Color1 + color2 = Color2 + + # Whenever we add Pandas as a dependency, this could be + # rewritten to avoid all the extra/slow work + exp = re.compile('[,=\t]') + with open(fn, 'r') as f: + line = f.readline().rstrip() + field = exp.split(line)[ValueIndex].strip() #TODO: Is this right?! + f.seek(0) + # Find min and max + names, vals = [], [] + for line in f: + if not line: + continue + + data = exp.split(line) + name, val = data[0], data[ValueIndex] + if len(val): + names.append(name) + vals.append(float(val)) + + vals = np.asarray(vals) + min_val = np.min(vals) + max_val = np.max(vals) + + # Do some sanity checking on the numbers. Don't want to include negative numbers in autoadd plot + if not is_general: + if min_val < 0.0: + min_val = 0.0 + if max_val < 0.0: + max_val = 0.0 + + if MaxScaleIsSpecified: + max_val = MaxScale # Override with user specified value + if MinScaleIsSpecified: + min_val = MinScale # Override with user specified value + + diff = max_val - min_val + if diff == 0.0: + diff = max_val + if diff == 0.0: + diff = 1.0 # Everything is zero + + sidxs = np.argsort(vals) + bus: IBus = DSS.ActiveCircuit.ActiveBus + data = [] + labels = [] + colors = [] + c1 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color1)) + c2 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color2)) + for i in sidxs: + name, val = names[i], vals[i] + if DSS.ActiveCircuit.SetActiveBus(name) <= 0 or not bus.Coorddefined: + continue + + if is_general: + data.append((bus.x, bus.y, val)) + s = ((val - min_val) / diff) + colors.append(c2*s + c1*(1-s)) + # InterpolateGradientColor(Color1, Color2, (GenPlotItem.Value - MinValue) / Diff), + else: # ptAutoAddLogPlot + data.append((bus.x, bus.y, val)) + # GetAutoColor((GenPlotItem.Value - MinValue) / Diff), + + if do_labels: + labels.append(bus.Name) + + data = np.asarray(data) + + + dss_circuit_plot(DSS, **kwargs) + + #fig = plt.figure(figsize=(8, 7)) + plt.title(f'{field}, Max={max_val:.3g}') + ax = plt.gca() + #if not is3d: + #ax.set_aspect('equal', 'datalim') + + ax.scatter(data[:, 0], data[:, 1], c=colors, zorder=10) + # ax.colorbar() + + #ax.autoscale_view() + #ax.get_xaxis().get_major_formatter().set_scientific(False) + #ax.get_yaxis().get_major_formatter().set_scientific(False) + #plt.tight_layout() + + # marker_code = MarkerIdx + + # NodeMarkerWidth: int + # MarkerIdx = NodeMarkerCode + + # marker_code = pmarkers[code_opt] + # marker_size = pmarkers[size_opt] + #marker_dict = get_marker_dict(marker_code) + # ax.plot(*coords, color='red', **marker_dict) + #MarkSpecialClasses + + + def dss_matrix_plot(DSS: IDSS, + *, + MatrixType: str = None, + Color1: str = None, + **kwargs: Unpack[PlotParams] + ): + # plot_id = kwargs.get('PlotId', None) + if MatrixType == 'IncMatrix': + title = 'Incidence matrix' + data = DSS.ActiveCircuit.Solution.IncMatrix[:-1] + else: + title = 'Laplacian matrix' + data = DSS.ActiveCircuit.Solution.Laplacian[:-1] + + x, y, v = data[0::3], data[1::3], data[2::3] + m = coo.coo_matrix((v, (x, y))) + #fig, [ax, ax2] = plt.subplots(1, 2, figsize=(8.6 * 2, 8.6), constrained_layout=True, num=title) + + if include_3d in ('both', '2d'): + fig = plt.figure(constrained_layout=True)#, num=plot_id) #, figsize=(8.6, 8.6)) + ax = fig.add_subplot(1, 1, 1) + ax.grid(True) + ax.spy(m, marker='s', markersize=1, color=Color1) + ax.set_xlabel('Column') + ax.set_ylabel('Row') + ax.set_title(title) + + if include_3d in ('both', '3d'): + fig = plt.figure()#figsize=(8.6, 8.6), num=plot_id + '_3D') + ax2 = fig.add_subplot(1, 1, 1, projection='3d') + ax2.scatter(x, y, v, c=v, marker='s') + ax2.set_xlabel('Column') + ax2.set_ylabel('Row') + ax2.set_zlabel('Value') + + + def dss_daisy_plot(DSS: IDSS, + *, + DaisyBusList: List[str] = None, + Quantity: str = None, + Labels: bool = None, + DaisySize: float = None, + **kwargs: Unpack[PlotParams] + ): + dss_circuit_plot(DSS, **kwargs) + + # print(params['DaisySize']) + + ax = plt.gca() + XMIN, XMAX = ax.get_xlim() + quantity = str_to_pq.get(Quantity, pqNone) + daisy_bus_list = DaisyBusList + do_labels = Labels + daisy_size = DaisySize + + ax.set_title(f'Device Locations / {quantity_str[quantity]}') + element = DSS.ActiveCircuit.ActiveCktElement + + if len(daisy_bus_list) == 0: + for g in DSS.ActiveCircuit.Generators: + if element.Enabled: + daisy_bus_list.append(element.BusNames[0]) + + counts = np.zeros(shape=(DSS.ActiveCircuit.NumBuses + 1,), dtype=np.int32) + for b in daisy_bus_list: + idx = DSS.ActiveCircuit.SetActiveBus(b) + if idx > 0: + counts[idx] += 1 + + radius = 0.005 * daisy_size * (XMAX - XMIN) + lines = [] + pointx, pointy = [], [] + for bidx in np.nonzero(counts)[0]: + bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] + if not bus.Coorddefined: + continue + + cnt = counts[bidx] + angle0 = 0 + angle = np.pi * 2.0 / cnt + for j in range(cnt): + Xc = bus.x + 2 * radius * np.cos(angle * j + angle0) + Yc = bus.y + 2 * radius * np.sin(angle * j + angle0) + lines.append([(bus.x, bus.y), (Xc, Yc)]) + pointx.append(Xc) + pointy.append(Yc) + + + lc = LineCollection(lines, linewidth=1, colors='r') + ax.add_collection(lc) + ax.scatter(pointx, pointy, marker='o', color='yellow', edgecolors='red', s=100, zorder=10) + + if not do_labels: + return + + for bidx in np.nonzero(counts)[0]: + bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] + if not bus.Coorddefined: + continue + + ax.text(bus.x, bus.y, bus.Name, zorder=11, fontsize='xx-small', va='center', clip_on=True) + + +def dss_di_plot(DSS: IDSS, + *, + CaseName: str = None, + MeterName: str = None, + Registers: List[int] = None, + CaseYear: str = None, + PeakDay: bool = None, + **kwargs: Unpack[PlotParams] +): + caseYear, caseName, meterName = CaseYear, CaseName, MeterName + plotRegisters, peakDay = Registers, PeakDay + + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', meterName + '.csv') + + if len(plotRegisters) == 0: + raise RuntimeError("No register indices were provided for DI_Plot") + + if not os.path.exists(fn): + fn = fn[:-4] + '_1.csv' + + # Whenever we add Pandas as a dependency, this could be + # rewritten to avoid all the extra/slow work + selected_data = [] + day_data = [] + mult = 1 if peakDay else 0.001 + + # If the file doesn't exist, let the exception raise + with open(fn, 'r') as f: + header = f.readline().rstrip() + allRegisterNames = [unquote(field) for field in header.strip().strip(' \t,').split(',')] + registerNames = [allRegisterNames[i] for i in plotRegisters] + + if not len(registerNames): + raise RuntimeError("Could not find any register name in the file") + + for line in f: + if not line: + continue + + rawValues = line.split(',') + selValues = [float(rawValues[0]), *(float(rawValues[i]) for i in plotRegisters)] + if not peakDay: + selected_data.append(selValues) + else: + day_data.append(selValues) + if len(day_data) == 24: + max_vals = [max(x) for x in zip(*day_data)] + max_vals[0] = day_data[0][0] + day_data = [] + selected_data.append(max_vals) + + if day_data: + max_vals = [max(x) for x in zip(*day_data)] + max_vals[0] = day_data[0][0] + day_data = [] + selected_data.append(max_vals) + + vals = np.asarray(selected_data, dtype=float) + fig, ax = plt.subplots(1) + icolor = -1 + for idx, name in enumerate(registerNames, start=1): + icolor += 1 + ax.plot(vals[:, 0], vals[:, idx] * mult, label=name, color=Colors[icolor % len(Colors)]) + + ax.set_title(f'{caseName}, Yr={caseYear}') + ax.set_xlabel('Hour') + ax.set_ylabel('MW, MWh or MVA') + ax.legend() + ax.grid() + + +def _plot_yearly_case(DSS: IDSS, caseName: str, meterName: str, plotRegisters: List[int], icolor: int, ax, registerNames: List[str]): + anyData = True + xvalues = [] + all_yvalues = [[] for _ in plotRegisters] + for caseYear in range(0, 21): + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'Totals_1.csv') + if not os.path.exists(fn): + continue + + with open(fn, 'r') as f: + f.readline() # Skip the header + # Get started - initialize Registers 1 + registerVals = [float(x) * 0.001 for x in f.readline().split(',')] + if len(registerVals): + xvalues.append(registerVals[7]) + + if len(xvalues) == 0: + raise RuntimeError('No data to plot') + + for caseYear in range(0, 21): + if meterName.lower() in ('totals', 'systemmeter', 'totals_1', 'systemmeter_1'): + suffix = '' if meterName.endswith('_1') else '_1' + meterName = meterName.lower().replace('totals', 'Totals').replace('systemmeter', 'SystemMeter') + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', f'{meterName}{suffix}.csv') + searchForMeterLine = False + else: + fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'EnergyMeterTotals_1.csv') + searchForMeterLine = True + + if not os.path.exists(fn): + continue + + with open(fn, 'r') as f: + header = f.readline() + if len(registerNames) == 0: + allRegisterNames = [unquote(field) for field in header.strip(' \t,').split(',')] + registerNames.extend(allRegisterNames[i] for i in plotRegisters) + + if not searchForMeterLine: + line = f.readline() + else: + for line in f: + label, rest = line.split(',', 1) + if label.strip().lower() == meterName.lower(): + line = f'{caseYear},{rest}' + else: + raise RuntimeError("Meter not found") + + registerVals = [float(x) * 0.001 for x in line.strip(' \t,').split(',')] + if len(registerVals): + for yvalues, idx in zip(all_yvalues, plotRegisters): + yvalues.append(registerVals[idx]) + + for yvalues, idx, regName in zip(all_yvalues, plotRegisters, registerNames): + marker_code = MARKER_SEQ[icolor % len(MARKER_SEQ)] + ax.plot(xvalues, yvalues, label=f'{caseName}:{meterName}:{regName}', color=Colors[icolor % len(Colors)], **get_marker_dict(marker_code)) + icolor += 1 + + return icolor + + +def dss_yearly_curve_plot(DSS: IDSS, *, + MeterName: str = None, + CaseNames: List[str] = None, + Registers: List[str] = None, + **kwargs: Unpack[PlotParams] +): + caseNames, meterName, plotRegisters = CaseNames, MeterName, Registers + + fig, ax = plt.subplots(1) + icolor = 0 + registerNames = [] + for caseName in caseNames: + icolor = _plot_yearly_case(DSS, caseName, MeterName, plotRegisters, icolor, ax, registerNames) + + if icolor == 0: + plt.close(fig) + raise RuntimeError('No files found') + + fig.suptitle(f"Yearly Curves for case(s): {', '.join(caseNames)}") + ax.set_title(f"Meter: {meterName}; Registers: {', '.join(registerNames)}", fontsize='small') + ax.set_xlabel('Total Area MW') + ax.set_ylabel('MW, MWh or MVA') + ax.legend() + ax.grid() + + +def dss_comparecases_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): + print('TODO: dss_comparecases_plot', kwargs) + + +def dss_zone_plot(DSS: IDSS, + *, + ObjectName: str, + Quantity: DSSPlotQuantity = DEFAULT_PLOT_PARAMS['Quantity'], + ShowLoops: bool = DEFAULT_PLOT_PARAMS['ShowLoops'], + Dots: bool = DEFAULT_PLOT_PARAMS['Dots'], + Labels: bool = DEFAULT_PLOT_PARAMS['Labels'], + Color1: str = DEFAULT_PLOT_PARAMS['Color1'], + Color3: str = DEFAULT_PLOT_PARAMS['Color3'], + SinglePhLineStyle: int = DEFAULT_PLOT_PARAMS['SinglePhLineStyle'], + ThreePhLineStyle: int = DEFAULT_PLOT_PARAMS['ThreePhLineStyle'], + MaxLineThickness: float = DEFAULT_PLOT_PARAMS['MaxLineThickness'], + MaxScale: float = DEFAULT_PLOT_PARAMS['MaxScale'], + **kwargs: Unpack[PlotParams] +): + obj_name = ObjectName + show_loops = ShowLoops + color1 = Color1 + color3 = Color3 + single_ph_line_style = LINES_STYLE_CODE.get(SinglePhLineStyle) + three_ph_line_style = LINES_STYLE_CODE.get(ThreePhLineStyle) + dots = Dots + do_labels = Labels + quantity = str_to_pq.get(Quantity, pqNone) + max_lw = MaxLineThickness + + if MaxScale is not None: + quantity_max_value = MaxScale + else: + quantity_max_value = 0 + + + ActiveCircuit = DSS.ActiveCircuit + + if obj_name: + ActiveCircuit.Meters.Name = obj_name + meters = [ActiveCircuit.Meters] + else: + meters = ActiveCircuit.Meters + + elem = ActiveCircuit.ActiveCktElement + line = ActiveCircuit.Lines + topo = ActiveCircuit.Topology + + icolor = 0 + + #TODO: check if/where we need to transform to lowercase. + bus_coords = dict((b.Name.lower(), (b.x, b.y)) for b in ActiveCircuit.Buses if b.Coorddefined) + + meter_marker_dict = get_marker_dict(24) + meter_marker_dict['markersize'] *= (3 / 3.5)**2 + + lines1, lines1_colors, labels1 = [], [], [] + lines3, lines3_colors, labels3 = [], [], [] + + # lw1, lw3 will initially hold the values, later transformed to actual widths + lw1, lw3 = [], [] + + if quantity in (pqCurrent, pqCapacity): + capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) + + coords_to_names = {} + + def _name_coords(c, name): + prev = coords_to_names.get(c) + if prev is None: + coords_to_names[c] = name + return + elif prev == name: + return + + if prev.endswith(',' + name) or prev.startswith(name + ',') or (',' + name + ',') in prev: + return + + coords_to_names[c] = prev + ',' + name + + + def _add_line(element, color): + br_name = element.Name + bus1, bus2 = element.BusNames[:2] + bus1, bus2 = nodot(bus1).lower(), nodot(bus2).lower() + c1 = bus_coords.get(bus1) + c2 = bus_coords.get(bus2) + lw = 1 + if not c1 or not c2: + return None, None + + if do_labels: + _name_coords(c1, f'{bus1}({feeder_name})') + _name_coords(c2, f'{bus2}({feeder_name})') + + if quantity == pqPower: + lw = element.TotalPowers[0] + elif quantity == pqVoltage: + lw = 1 + elif quantity == pqLosses: + lw = 0 + try: + if element.Name.startswith('Line.'): + lw = 1e-3 * abs(element.Losses[0] / line.Length) + except: + pass + elif quantity in (pqCurrent, pqCapacity): + lw = capacities.get(element.Name, np.NaN) + + if (element.NumPhases == 1): + lines1.append([c1, c2]) + lines1_colors.append(color) + labels1.append(br_name) + lw1.append(lw) + return lines1_colors, len(lines1_colors) - 1 + else: + lines3.append([c1, c2]) + lines3_colors.append(color) + labels3.append(br_name) + lw3.append(lw) + return lines3_colors, len(lines3_colors) - 1 + + + fig, ax = plt.subplots(1) + for meter in meters: + if not elem.Enabled: + continue + + feeder_name = meter.Name + branches = meter.AllBranchesInZone + if not branches: + continue + + # Meter marker + _ = topo.First + coords = bus_coords.get(elem.BusNames[meter.MeteredTerminal - 1]) + if coords: + plt.plot(*coords, color='red', **meter_marker_dict) + + feeder_color = color1 if show_loops else Colors[icolor % len(Colors)] + icolor += 1 + + br_idx = topo.First + while br_idx != 0: + if not elem.Enabled: + continue + + lcs, lidx = _add_line(elem, feeder_color) + if show_loops: + looped = (topo.LoopedBranch != 0) + if looped: + # The looped PDE is set as active by LoopedBranch + _add_line(elem, color3) + # Adjust the original to color3 + if lidx is not None: + lcs[lidx] = color3 + + br_idx = topo.Next + + + lw1 = np.asarray(lw1) + lw3 = np.asarray(lw3) + + if quantity_max_value == 0: + lw1_max_value = 0 + lw3_max_value = 0 + if len(lw1): + lw1_max_value = np.nanmax(lw1) + if np.isfinite(lw1_max_value): + quantity_max_value = max(quantity_max_value, lw1_max_value) + if len(lw3): + lw3_max_value = np.nanmax(lw3) + if np.isfinite(lw3_max_value): + quantity_max_value = max(quantity_max_value, lw3_max_value) + + if quantity_max_value == 0: + quantity_max_value = 1 + + lw1 = np.clip(3 * lw1 / quantity_max_value, 0.5, max_lw) + lw3 = np.clip(3 * lw3 / quantity_max_value, 0.5, max_lw) + lines1 = np.asarray(lines1) + lines3 = np.asarray(lines3) + lc1 = LineCollection(lines1, linewidth=lw1, colors=lines1_colors, linestyle=single_ph_line_style) + lc3 = LineCollection(lines3, linewidth=lw3, colors=lines3_colors, linestyle=three_ph_line_style) + ax.add_collection(lc1) + ax.add_collection(lc3) + if dots: + for lines, lc in ((lines1, lc1), (lines3, lc3)): + ax.scatter(lines[:, 0, 0].ravel(), lines[:, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) + ax.scatter(lines[:, 1, 0].ravel(), lines[:, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) + + ax.set_title(f'Meter Zone: {obj_name}' if obj_name else 'All Meter Zones') + + for coords, name in coords_to_names.items(): + ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) + + ax.set_aspect('equal', 'datalim') + ax.autoscale() + + + +dss_plot_funcs = { + 'Scatter': dss_scatter_plot, + 'Daisy': dss_daisy_plot, + 'TShape': dss_tshape_plot, + 'PriceShape': dss_priceshape_plot, + 'LoadShape': dss_loadshape_plot, + 'Monitor': dss_monitor_plot, + 'Circuit': dss_circuit_plot, + 'Profile': dss_profile_plot, + 'Visualize': dss_visualize_plot, + 'YearlyCurve': dss_yearly_curve_plot, + 'Matrix': dss_matrix_plot, + 'GeneralData': dss_general_data_plot, + 'DI': dss_di_plot, +# 'CompareCases': dss_comparecases_plot, + 'MeterZones': dss_zone_plot +} + +def dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): + try: + ptype = kwargs['PlotType'] + if ptype not in dss_plot_funcs: + raise NotImplementedError(f'ERROR: not implemented plot type "{ptype}"') + return -1 + + with ToggleAdvancedTypes(DSS, False), warnings.catch_warnings(): + warnings.simplefilter("ignore") + return 0, dss_plot_funcs.get(ptype)(DSS, **kwargs) + + except Exception as ex: + from traceback import format_exc + # print('DSS: Error while plotting. Parameters:', kwargs, file=sys.stderr) + DSS._errorPtr[0] = 777 + DSS._lib.Error_Set_Description(f"Error in the plot backend: {ex}\n{format_exc()}".encode()) + return 777, None + + return 0, None + + +# dss_progress_bar = None +# dss_progress_desc = '' + + +@api_util.ffi.def_extern() +def dss_python_cb_write(ctx, message_str, message_type: int, message_size: int, message_subtype: int): + global dss_progress_bar + global dss_progress_desc + + # DSS = _ctx2dss(ctx) + + message_str = api_util.ffi.string(message_str).decode(api_util.codec) + if message_type == api_util.lib.DSSMessageType_Error: + #print('DSS Error:', message_str, file=sys.stderr) + pass + elif message_type in (api_util.lib.DSSMessageType_ProgressCaption, api_util.lib.DSSMessageType_ProgressFormCaption): + #dss_progress_desc = message_str + # print('Progress Caption:', message_str, file=sys.stderr) + pass + elif message_type == api_util.lib.DSSMessageType_Progress: + #print('DSS Progress:', message_str, file=sys.stderr) + pass + elif message_type == api_util.lib.DSSMessageType_FireOffEditor: + link_file(message_str) + # try: + # # print('DSSMessageType_FireOffEditor') + # with open(message_str, 'r') as f: + # text = f.read() + + # IPython.display.display({'text/plain': text}, raw=True) + # except: + # print(f'Could not display file "{message_str}"') + # return 1 + + elif message_type == api_util.lib.DSSMessageType_ProgressPercent: + try: + pass + # n = int(message_str) + # desc = '' + # if n == 0 and dss_progress_bar is not None: + # dss_progress_bar = None + + # if dss_progress_bar is None: + # dss_progress_bar = tqdm(total=100, desc=dss_progress_desc) + + # if n < 0: + # del dss_progress_bar + # dss_progress_bar = None + # return 0 + + + # dss_progress_bar.n = n + # dss_progress_bar.refresh() +# if n == 100: +# dss_progress_bar.close() + except: + import traceback + traceback.print_exc() + print('DSS Progress:', message_str) + + # else: + # # print(message_type) + # # print(message_str) + # IPython.display.display({'text/plain': message_str}, raw=True) + else: + # do nothing for now... + pass + + return 0 + + +@api_util.ffi.def_extern() +def dss_python_cb_plot(ctx, paramsStr): + params = json.loads(api_util.ffi.string(paramsStr)) + result = 0 + try: + DSS = IDSS._get_instance(ctx=ctx) + result, fig = dss_plot(DSS, **params) + if _do_show: + fig.show() + except: + from traceback import print_exc + print('DSS: Error while plotting. Parameters:', params, file=sys.stderr) + print_exc() + return 0 if result is None else result + +_original_allow_forms = None +_do_show = True +_enabled = False + +def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True, ctx: IDSS = None): + """ + Enables the plotting subsystem from DSS-Extensions. + + Set plot3d to `True` to try to reproduce some of the plots from the + alternative OpenDSS Visualization Tool / OpenDSS Viewer addition + to OpenDSS. + + Use `show` to control whether this backend should call `pyplot.show()` + or leave that to the system or the user. If the user plans to customize + the figure, it is better to set `show=False` in order to preserve the + figures, since `pyplot.show()` discards them. + """ + + global include_3d + global _original_allow_forms + global _do_show + global _enabled + global DSSPlotCtx + + if ctx is not None: + DSSPlotCtx = ctx + + _do_show = show + _enabled = True + + if plot3d and plot2d: + include_3d = 'both' + elif plot3d and not plot2d: + include_3d = '3d' + elif plot2d and not plot3d: + include_3d = '2d' + + api_util.lib.DSS_RegisterPlotCallback(api_util.lib.dss_python_cb_plot) + api_util.lib.DSS_RegisterMessageCallback(api_util.lib.dss_python_cb_write) + _original_allow_forms = DSSPlotCtx.AllowForms + DSSPlotCtx.AllowForms = True + +def disable(): + global _enabled + _enabled = False + api_util.lib.DSS_RegisterPlotCallback(api_util.ffi.NULL) + api_util.lib.DSS_RegisterMessageCallback(api_util.ffi.NULL) + if _original_allow_forms is not None: + DSSPlotCtx.AllowForms = _original_allow_forms + + +def plot_dsv(fn: Union[str, FilePath]): + return DSVHandler(fn).parse() + +__all__ = ['enable', 'disable', 'plot_dsv', ] diff --git a/tests/test_general.py b/tests/test_general.py index 8095bd77..3e5a6fc6 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -21,14 +21,15 @@ def setup_function(): DSS.ClearAll() - if not DSS._api_util._is_odd: + DSS.AllowForms = False + DSS.AdvancedTypes = False + DSS.CompatFlags = 0 + + if not DSS._api_util._is_oddie: DSS.AllowEditor = False - DSS.AdvancedTypes = False DSS.AllowChangeDir = True DSS.COMErrorResults = False - DSS.CompatFlags = 0 - DSS.AllowForms = False DSS.Error.UseExceptions = True DSS.Text.Command = 'set DefaultBaseFreq=60' @@ -1002,21 +1003,30 @@ def test_skip_commands(): def test_skip_files(): DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + DSS.Text.Command = 'clear' + + DSS.ActiveCircuit.Settings.SkipCommands = ['clear'] # We need to skip clear since it resets SkipFileRegExp DSS.ActiveCircuit.Settings.SkipFileRegExp = r'.*LineCodes\.DSS' - print(repr(DSS.ActiveCircuit.Settings.SkipFileRegExp)) # This should fail since we won't have the LineCodes with pytest.raises(DSSException): DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + DSS.ActiveCircuit.Settings.SkipCommands = [] DSS.ActiveCircuit.Settings.SkipFileRegExp = '' DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + DSS.Text.Command = 'clear' + DSS.ActiveCircuit.Settings.SkipCommands = ['clear'] DSS.ActiveCircuit.Settings.SkipFileRegExp = None DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + DSS.ActiveCircuit.Settings.SkipCommands = [] + DSS.Text.Command = 'clear' + DSS.ActiveCircuit.Settings.SkipCommands = ['clear'] DSS.ActiveCircuit.Settings.SkipFileRegExp = 'some random string just to test' DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/34Bus/ieee34Mod1.dss"' + DSS.ActiveCircuit.Settings.SkipCommands = [] DSS.ActiveCircuit.Settings.SkipFileRegExp = None diff --git a/tests/test_past_issues.py b/tests/test_past_issues.py index 9d8d5587..4d7ef829 100644 --- a/tests/test_past_issues.py +++ b/tests/test_past_issues.py @@ -13,13 +13,18 @@ def setup_function(): DSS.ClearAll() + DSS.AllowForms = False - if not DSS._api_util._is_odd: + DSS.AdvancedTypes = False + DSS.CompatFlags = 0 + + if not DSS._api_util._is_oddie: DSS.AllowEditor = False - DSS.AdvancedTypes = False DSS.AllowChangeDir = True - DSS.COMErrorResults = True # TODO: change to False - DSS.CompatFlags = 0 + DSS.COMErrorResults = False + + DSS.Error.UseExceptions = True + DSS.Text.Command = 'set DefaultBaseFreq=60' def test_rxmatrix(): @@ -69,7 +74,7 @@ def test_create_with_circuit(): for cls in DSS.Classes: DSS.ClearAll() DSS.NewCircuit(f'test_{cls}') - if cls in ('CapControl', 'RegControl', 'GenDispatcher', 'StorageController', 'Relay', 'Fuse', 'SwtControl', 'ESPVLControl', 'GICsource'): + if cls in ('CapControl', 'RegControl', 'GenDispatcher', 'StorageController', 'Relay', 'Fuse', 'SwtControl', 'ESPVLControl', 'GICsource', 'FMonitor'): with pytest.raises(DSSException): DSS.Text.Command = f'new {cls}.test{cls}' @@ -81,6 +86,8 @@ def test_create_with_circuit(): DSS.Text.Command = f'new {cls}.test{cls}2 element=transformer.testtr capacitor=testcap' elif cls == 'GenDispatcher': DSS.Text.Command = f'new {cls}.test{cls}2 element=transformer.testtr' + elif cls == 'FMonitor': + DSS.Text.Command = f'new {cls}.test{cls}2 element=transformer.testtr' else: DSS.Text.Command = f'new {cls}.test{cls}' From 74b33a240812e6f0b51bc71f24aaa6d32beef90c Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:51:44 -0300 Subject: [PATCH 45/82] Tests/save_outputs: add a more general test for the iterator issue --- tests/save_outputs.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/save_outputs.py b/tests/save_outputs.py index d1826602..5bea0be4 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -223,13 +223,25 @@ def export_dss_api_cls(dss: dss.IDSS, dss_cls): else: items = [dss_cls] - if ((not SAVE_DSSX_OUTPUT) or SAVE_DSSX_OUTPUT_ODD) and lname in ('istorages', 'iwindgens'): - def iter_cls(): - for i in range(dss_cls.Count): - dss_cls.idx = i - yield dss_cls - - items = iter_cls() + try: + if dss_cls.Count == 0: + return + + _ = dss_cls.First + name1 = dss_cls.Name + nxt = dss_cls.Next + name2 = dss_cls.Name + if nxt != 0 and name1 == name2: + print("Replacing iterator for", lname, (name1, name2)) + def iter_cls(): + for i in range(dss_cls.Count): + dss_cls.idx = i + yield dss_cls + + items = iter_cls() + + except: + pass for _ in items: From 2873d707595a3cfc8720edfe4688006f6d8283e9 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 7 Dec 2024 01:26:39 -0300 Subject: [PATCH 46/82] Tests: adjust compare_outputs for missing data --- tests/compare_outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compare_outputs.py b/tests/compare_outputs.py index 0bab27f6..f4f822cd 100644 --- a/tests/compare_outputs.py +++ b/tests/compare_outputs.py @@ -198,7 +198,7 @@ def compare(self, a, b, org_path=None): continue # print(path) - va, vb = a.get(k), b.get(k, MISSING) + va, vb = a.get(k), (b or {}).get(k, MISSING) if vb is MISSING: continue From f4e01736faa99e13ca19314bb4ce9b9c3ddefb29 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:30:16 -0300 Subject: [PATCH 47/82] Tests: minor tweaks, allow loading extra test cases given in a JSON file in an env var. --- tests/_settings.py | 26 ++++++++++++++++++++++++++ tests/save_outputs.py | 4 ++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/_settings.py b/tests/_settings.py index 77983221..7cb789a3 100644 --- a/tests/_settings.py +++ b/tests/_settings.py @@ -262,3 +262,29 @@ Version8/Distrib/Examples/CIM/IEEE13_Assets.dss Version8/Distrib/Examples/CIM/IEEE13_CDPSM.dss '''.strip().split('\n') + +json_test_fns = os.environ.get('DSS_EXTENSIONS_TEST_SYSTEMS', '') +if json_test_fns: + import json + with open(json_test_fns, 'r') as f_test_fns: + config = json.load(f_test_fns) + + extra = config.get('extraTestSystems') + replacements = config.get('testSystems') + + if replacements: + test_filenames = replacements + + if extra: + extra.extend(test_filenames) + test_filenames = extra + + cim_replacements = config.get('cimTestSystems') + cim_extra = config.get('cimExtraTestSystems') + if cim_replacements is not None: + cimxml_test_filenames = cim_replacements + + if cim_extra: + cim_extra.extend(cimxml_test_filenames) + cimxml_test_filenames = cim_extra + diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 5bea0be4..1474d86d 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -247,7 +247,7 @@ def iter_cls(): for _ in items: record = {} for field in fields: - # printv('>', field) + # printv('>', getattr(_, 'Name', '---'), field) try: record[field] = adjust_to_json(dss_cls, field) except DSSException as e: @@ -492,7 +492,7 @@ def get_archive_fn(live_fn, fn_prefix=None): org_fn = fn fixed_fn = fn if not fn.startswith('L!') else fn[2:] line_by_line = fn.startswith('L!') - fn = os.path.join(ROOT_DIR, fixed_fn) + fn = os.path.join(ROOT_DIR, fixed_fn) if not fixed_fn.startswith('/') else fixed_fn json_fn = get_archive_fn(fn) + '.json' try: zip_out.getinfo(json_fn) From f16654038b72a3c776e0a9cc9ef773cf018da104 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:06:49 -0300 Subject: [PATCH 48/82] Migrate to the new settings management; simplify the API utils; some code related to AltDSS-Python will be adjusted in another commit. Exceptions and AdvancedTypes refactored along the other changes in API utils. --- .gitignore | 1 + docs/changelog.md | 38 ++ dss/IActiveClass.py | 2 +- dss/ICircuit.py | 12 +- dss/IDSS.py | 24 +- dss/IError.py | 26 +- dss/ISettings.py | 203 ++++++++- dss/__init__.py | 2 +- dss/_cffi_api_util.py | 908 ++++++++++++++++++++------------------ tests/_settings.py | 2 +- tests/save_outputs.py | 2 +- tests/test_general.py | 38 +- tests/test_past_issues.py | 8 +- 13 files changed, 792 insertions(+), 474 deletions(-) diff --git a/.gitignore b/.gitignore index 6db77324..c5d67d65 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ dss/messages/* docs/apidocs tests/result*.zip tmp/ +electricdss-tst \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 1330ca8d..6e3a63a3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,44 @@ Remember that changes in our alternative OpenDSS engine, currently known as DSS C-API, are always relevant. See [DSS C-API's repository](https://github.com/dss-extensions/dss_capi/) for more information. +## 0.16.x + +### 0.16.0 + +Released on 2024-12-1x. + +- See the extensive backend changes in the next subsection. +- `UseExceptions` and `AdvancedTypes` now only affect the local bindings. That is, a second instance for the same or different DSS engine context can use different settings. This also decouples the option between DSS-Python and OpenDSSDirect.py. It should make it easier to mix and match our Python packages without having to worry too much about this settings. + +#### Backend changes and support for EPRI's distribution + +- DSS-Python-Backend now provides a custom backend which integrates directly with NumPy, called internally "FastDSS", besides the legacy CFFI backend. The CFFI backend is now lighter and faster to compile. + - Since the new FastDSS is tighly integrated with CPython and NumPy, we added the option to disable it using the environment variable `DSS_EXTENSIONS_FASTDSS`. Set it to 0 to disable the backend, using only the CFFI backend. + - As the name implies, FastDSS is fast, even compared to CFFI. If removes most of the overhead of running a few interpreted lines of Python code, interacting directly with the CPython C-API and the NumPy C-API. That is, we do not expect to change it a lot in the future. + - Reading numeric arrays and strings from the API with FastDSS gets a speed boost compared to the old backend. + - Note that the original CFFI backend was already faster for many functions vs. the alternatives. + - The performance of each function varies across all the engines now supported (AltDSS, OpenDSS, OpenDSS-C). AltDSS and its DSS C-API implementation have specific optimizations implemented throughout the years. + - For PyPy and other Python implementations, the CFFI backend is still preferred. We would like to experiment with a backend based on [HPy](https://hpyproject.org/) in the future, if time permits. + +- Although we do not expect users to explore this, the backend now allows loading external DSS C-API libraries in the new struct-style initialization. + +- DSS-Extensions, including DSS-Python and OpenDSSDirect.py, now have good support for EPRI's official OpenDSS binaries, including the Delphi version (mainline) and the new OpenDSS-C. + - The integration started in 2023, after EPRI's Direct DLL API was updated to migrate from Delphi variants, moving closer to our own DSS C-API in some aspects. + - Contributors from DSS-Extensions have collaborated with EPRI on the maintainance and general development of OpenDSS-C in 2024. A few extras from our AltDSS engine have been integrated into OpenDSS-C, still under testing by the time this document was updated. + - The integration is done by wrapping EPRI's binaries with a very thin layer that exposes it with the DSS-Extensions API. Our compatibility layer is called Oddie (originally for OpenDSSDirect.DLL Interface Extender). Functions that are not or cannot be implemented return errors. + - The Delphi version of EPRI's OpenDSS, in OpenDSSDirect.DLL, is copied from the https://github.com/dss-extensions/opendss-svn-mirror -- the OpenDSS versions tracked on Git tags. + - Initially, since EPRI does not distribute OpenDSS-C binaries yet, DSS-Extensions builds shared libraries/DLLs combining Oddie and OpenDSS-C. With the new-style initialization, Oddie adds a single function to the public API. + - Users can build their own binaries for OpenDSS-C (or the Delphi OpenDSS) and use that through the Oddie interface. That is, if a new OpenDSS version is released, users can install EPRI's distribution and load the binaries installed with that. Breaking API/ABI changes may crash though. + **DSS-Extensions will not patch the official EPRI's OpenDSS(-C) beyond build scripts.** That is, if a bug is not in our code, we cannot fix it on EPRI's distribution. We can adjust things in our engine, AltDSS (the main code in DSS C-API's repository). + - On Windows, users will have three alternatives of engine: AltDSS, EPRI's mainline OpenDSS written in Delphi, and EPRI's new OpenDSS-C written in C++. + +- We are open to suggestions on if and how to adjust DSS-Python and OpenDSSDirect.py to better signal that certain functions and objects are not available with EPRI's engine and vice-versa. + +To clear any potential confusion, our custom engine and API will continue to be developed since it has extra features and we are free to experiment and improve it without requiring external approval. + +When using DSS-Extensions or OpenDSS in general, please try to cite the engine version. It will make it much easier to reproduce your work. + + ## 0.15.x ### 0.15.6 diff --git a/dss/IActiveClass.py b/dss/IActiveClass.py index 6c0c0762..879bb785 100644 --- a/dss/IActiveClass.py +++ b/dss/IActiveClass.py @@ -56,7 +56,7 @@ def __iter__(self) -> Iterator[IActiveClass]: def First(self) -> int: ''' Sets first element in the active class to be the active DSS object. - If the object is a CktElement, ActiveCktELement also points to this element. + If the object is a CktElement, ActiveCktElement also points to this element. Returns 0 if none. diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 2fac44c6..63cbb906 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -644,7 +644,7 @@ def FromJSON(self, data: Union[AnyStr, dict], options: DSSJSONFlags = 0): self._lib.Circuit_FromJSON(data, options) - def Save(self, dirOrFilePath: AnyStr, saveFlags: DSSSaveFlags) -> str: + def Save(self, dirOrFilePath: AnyStr, saveFlags: Union[DSSSaveFlags, List[DSSSaveFlags]]) -> str: ''' Equivalent of the "save circuit" DSS command, but allows customization through the `saveFlags` argument, which is a set of bit flags. @@ -668,6 +668,14 @@ def Save(self, dirOrFilePath: AnyStr, saveFlags: DSSSaveFlags) -> str: ''' if not isinstance(dirOrFilePath, bytes): dirOrFilePath = dirOrFilePath.encode() - return self._get_string(self._lib.Circuit_Save(dirOrFilePath, saveFlags)) + + if isinstance(saveFlags, (list, tuple)): + mask = 0 + for v in saveFlags: + mask = mask | v + + saveFlags = mask + + return self._api_util.get_string(self._lib.Circuit_Save(dirOrFilePath, saveFlags)) diff --git a/dss/IDSS.py b/dss/IDSS.py index 28ff85b1..9b443bbc 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -98,6 +98,9 @@ def __init__(self, api_util): Wrap a new DSS context with the DSS-Python API. This is not typically used directly. Refer to `IDSS.NewContext` or `IDSS._get_instance`. + + For Oddie-wrapped libraries (EPRI's OpenDSS and OpenDSS-C), prefer the dedicated constructors and classes + (e.g. `IOddieDSS`, `EPRIOpenDSSC`, `EPRIOpenDSS`). ''' if api_util.ctx not in IDSS._ctx_to_dss: @@ -424,12 +427,16 @@ def COMErrorResults(self) -> bool: This can also be set through the environment variable `DSS_CAPI_COM_DEFAULTS`. Setting it to 0 disables the legacy/COM behavior. The value can be toggled through the API at any time. + **Deprecated:** Use `Settings.COMErrorResults` instead (same behavior, the setting was just moved there for better organization). + **(API Extension)** ''' + warnings.warn('"COMErrorResults" was moved to the Settings interface. This property still works, but will be removed in a future release. Please use `...Settings.COMErrorResults` instead.', DeprecationWarning, stacklevel=2) return self._lib.DSS_Get_COMErrorResults() @COMErrorResults.setter def COMErrorResults(self, Value: bool): + warnings.warn('"COMErrorResults" was moved to the Settings interface. This property still works, but will be removed in a future release. Please use `...Settings.COMErrorResults` instead.', DeprecationWarning, stacklevel=2) self._lib.DSS_Set_COMErrorResults(Value) def NewContext(self) -> IDSS: @@ -449,8 +456,7 @@ def NewContext(self) -> IDSS: ffi = self._api_util.ffi lib = self._api_util.lib_unpatched new_ctx = ffi.gc(lib.ctx_New(), lib.ctx_Dispose) - new_api_util = CffiApiUtil(ffi, lib, new_ctx) - new_api_util._advanced_types = self._api_util._advanced_types + new_api_util = CffiApiUtil(ffi, lib, new_ctx, parent=self._api_util) return IDSS(new_api_util) def __call__(self, cmds: Union[AnyStr, List[AnyStr]]): @@ -513,21 +519,26 @@ def AdvancedTypes(self) -> bool: When disabled, the legacy plain arrays are used and complex numbers cannot be consumed by the Python API. *Defaults to **False** for backwards compatibility.* + + **Deprecated:** Use `Settings.AdvancedTypes` instead (same behavior, the setting was just moved there for better organization). **(API Extension)** ''' - return self._api_util._advanced_types + return self._lib.advanced_types @AdvancedTypes.setter def AdvancedTypes(self, Value: bool): - self._api_util._advanced_types = bool(Value) + warnings.warn('"AdvancedTypes" was moved to the Settings interface. This property still works, but will be removed in a future release. Please use `...Settings.AdvancedTypes` instead.', DeprecationWarning, stacklevel=2) + self._lib.advanced_types = bool(Value) @property def CompatFlags(self) -> int: ''' Controls some compatibility flags introduced to toggle some behavior from the official OpenDSS. - **THE FLAGS ARE GLOBAL, affecting all DSS engines in the process.** + **THE FLAGS ARE GLOBAL, affecting all AltDSS engines in the process.** + CompatFlags for Oddie-loaded instances (OpenDSS and OpenDSS-C engines) are handled by the Oddie code itself, + so it is global for each Oddie library. These flags may change for each version of DSS C-API, but the same value will not be reused. That is, when we remove a compatibility flag, it will have no effect but will also not affect anything else @@ -539,12 +550,15 @@ def CompatFlags(self) -> int: See the enumeration `DSSCompatFlags` for available flags, including description. + **Deprecated:** Use `Settings.CompatFlags` instead (same behavior, the setting was just moved there for better organization). + **(API Extension)** ''' return self._lib.DSS_Get_CompatFlags() @CompatFlags.setter def CompatFlags(self, Value: int): + warnings.warn('"CompatFlags" was moved to the Settings interface. This property still works, but will be removed in a future release. Please use `...Settings.CompatFlags` instead.', DeprecationWarning, stacklevel=2) self._lib.DSS_Set_CompatFlags(Value) diff --git a/dss/IError.py b/dss/IError.py index 50117dd8..7662a856 100644 --- a/dss/IError.py +++ b/dss/IError.py @@ -77,28 +77,28 @@ def UseExceptions(self) -> bool: """ Controls whether the automatic error checking mechanism is enable, i.e., if the DSS engine errors (from the `Error` interface) are mapped exception when - detected. - + detected. + **When disabled, the user takes responsibility for checking for errors.** This can be done through the `Error` interface. When `Error.Number` is not zero, there should be an error message in `Error.Description`. This is compatible - with the behavior on the official OpenDSS (Windows-only COM implementation) when + with the behavior on the official OpenDSS (Windows-only COM implementation) when `AllowForms` is disabled. Users can also use the DSS command `Export ErrorLog` to inspect for errors. - **WARNING:** This is a global setting, affects all DSS instances from DSS-Python, - OpenDSSDirect.py and AltDSS. + With EPRI's OpenDSS engines, in contrast to our main AltDSS engine, users are + also required to set `AllowForms` to `False`, otherwise the engine does not + populate the Error.Number API and the error is consumed by the popup form or + terminal message. + + **NOTE:** this used to be a global settings. Since DSS-Python v0.16.0, + it only affects the target instance. **(API Extension)** """ - return Base._use_exceptions - + return self._lib.using_exceptions + @UseExceptions.setter def UseExceptions(self, value: bool): - Base._enable_exceptions(value) - _UseExceptions = 1 - if value: - self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] | _UseExceptions - else: - self._api_util.settings_ptr[0] = self._api_util.settings_ptr[0] & ~_UseExceptions + self._lib.using_exceptions = value diff --git a/dss/ISettings.py b/dss/ISettings.py index cb3ed2ba..298cbbbf 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -6,9 +6,82 @@ from typing import AnyStr, Union, List from .enums import DSSPropertyNameStyle, CktModels + +class SettingsContext: + def __init__(self, settings): + self._settings = settings + + def __enter__(self): + # Using try...except since the official engine doesn't implement these. + # Only a few are (and can be) implemented through Oddie. + try: + self._AdvancedTypes = self._settings.AdvancedTypes + except: + pass + + try: + self._CompatFlags = self._settings.CompatFlags + except: + pass + + try: + self._IterateDisabled = self._settings.IterateDisabled + except: + pass + + try: + self._PreferLists = self._settings.PreferLists + except: + pass + + try: + self._SkipCommands = self._settings.SkipCommands + except: + pass + + try: + self._SkipFileRegExp = self._settings.SkipFileRegExp + except: + pass + + return self._settings + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + self._settings.AdvancedTypes = self._AdvancedTypes + except: + pass + + try: + self._settings.CompatFlags = self._CompatFlags + except: + pass + + try: + self._settings.IterateDisabled = self._IterateDisabled + except: + pass + + try: + self._settings.PreferLists = self._PreferLists + except: + pass + + try: + self._settings.SkipCommands = self._SkipCommands + except: + pass + + try: + self._settings.SkipFileRegExp = self._SkipFileRegExp + except: + pass + + + class ISettings(Base): __slots__ = [ - '_command_dict' + '_command_dict', ] _columns = [ @@ -29,10 +102,17 @@ class ISettings(Base): 'NormVmaxpu', 'AllowDuplicates', 'ControlTrace', - 'LoadsTerminalCheck', - 'IterateDisabled', + + # Commented since we don't have these on Oddie with the official engine + # 'LoadsTerminalCheck', + # 'IterateDisabled', + # 'SkipCommands', + # 'PreferLists', + # 'SkipFileRegExp', + # 'CompatFlags', ] + def __init__(self, api_util): Base.__init__(self, api_util) num_commands = self._lib.DSS_Executive_Get_NumCommands() @@ -41,6 +121,27 @@ def __init__(self, api_util): for i in range(1, num_commands + 1) } + + def Context(self) -> SettingsContext: + ''' + Returns a Settings context manager. + The context manager saves the values of the tracker settings on enter, + restoring them on exit. This allows code to change the settings within + the context block and they are restored to the initial values automatically. + + Note: this context manager target DSS-Python settings. Use the equivalent for OpenDSSDirect.py. + A few settings are shared at engine level. + + Settings tracked: + - AdvancedTypes + - CompatFlags + - IterateDisabled + - PreferLists + - SkipCommands + - SkipFileRegExp + ''' + return SettingsContext(self) + @property def AllowDuplicates(self) -> bool: ''' @@ -372,7 +473,101 @@ def SkipCommands(self, Value: List[str]): Value = [self._command_dict[cmd_name] for cmd_name in Value] Value, ValuePtr, ValueCount = self._prepare_int32_array(Value) - self._lib.Settings_Set_SkipCommands(ValuePtr, ValueCount) + @property + def AdvancedTypes(self) -> bool: + ''' + When enabled, there are **two side-effects**: + + - **Per DSS Context:** Complex arrays and complex numbers can be returned and consumed by the Python API. + - **Global effect:** The low-level API provides matrix dimensions when available (`EnableArrayDimensions` is enabled). + + As a result, for example, `DSS.ActiveCircuit.ActiveCktElement.Yprim` is returned as a complex matrix instead + of a plain array. + + When disabled, the legacy plain arrays are used and complex numbers cannot be consumed by the Python API. + + *Defaults to **False** for backwards compatibility.* + + **(API Extension)** + ''' + return self._lib.advanced_types + + @AdvancedTypes.setter + def AdvancedTypes(self, Value: bool): + self._lib.advanced_types = bool(Value) + + + @property + def CompatFlags(self) -> int: + ''' + Controls some compatibility flags introduced to toggle some behavior from the official OpenDSS. + + **THE FLAGS ARE GLOBAL, affecting all AltDSS engines in the process.** + CompatFlags for Oddie-loaded instances (OpenDSS and OpenDSS-C engines) are handled by the Oddie code itself, + so it is global for each Oddie library. + + These flags may change for each version of DSS C-API, but the same value will not be reused. That is, + when we remove a compatibility flag, it will have no effect but will also not affect anything else + besides raising an error if the user tries to toggle a flag that was available in a previous version. + + We expect to keep a very limited number of flags. Since the flags are more transient than the other + options/flags, it was preferred to add this generic function instead of a separate function per + flag. + + See the enumeration `DSSCompatFlags` for available flags, including description. + + **(API Extension)** + ''' + return self._lib.DSS_Get_CompatFlags() + + @CompatFlags.setter + def CompatFlags(self, Value: int): + self._lib.DSS_Set_CompatFlags(Value) + + + @property + def PreferLists(self) -> bool: + ''' + Enable this setting to use lists instead of NumPy arrays, where it makes sense. + + This was added for better for compatibility with the COM packages (`comtypes` and `win32com` use lists + and tuples by default) and the original OpenDSSDirect.py releases. + + Current releases of OpenDSSDirect.py, DSS-Python, and AltDSS(-Python) all use NumPy arrays by default. + Users can also activate the related `AdvancedTypes` for a richer experience. + + **(API Extension)** + ''' + return self._lib.prefer_lists + + @PreferLists.setter + def PreferLists(self, value: bool): + self._lib.prefer_lists = value + + @property + def COMErrorResults(self) -> bool: + ''' + If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with the + official OpenDSS COM interface. + + For example, consider the function `Loads_Get_ZIPV`. If there is no active circuit or active load element: + + - In the disabled state (COMErrorResults=False), the function will return "[]", an array with 0 elements. + - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should + be compatible with the return value of the official COM interface. + + Defaults to False/0 (disabled state), starting DSS-Python v0.16. + + This can also be set through the environment variable `DSS_CAPI_COM_DEFAULTS`. Setting it to 0 disables + the legacy/COM behavior. The value can be toggled through the API at any time. + + **(API Extension)** + ''' + return self._lib.DSS_Get_COMErrorResults() + + @COMErrorResults.setter + def COMErrorResults(self, Value: bool): + self._lib.DSS_Set_COMErrorResults(Value) diff --git a/dss/__init__.py b/dss/__init__.py index 07934c52..4615c601 100644 --- a/dss/__init__.py +++ b/dss/__init__.py @@ -16,7 +16,7 @@ if os.path.exists(_properties_mo): lib.DSS_SetPropertiesMO(_properties_mo.encode()) -from ._cffi_api_util import CffiApiUtil, DSSException, set_case_insensitive_attributes +from ._cffi_api_util import CffiApiUtil, AltDSSAPIUtil, DSSException, set_case_insensitive_attributes from .IDSS import IDSS from .Oddie import IOddieDSS, OddieOptions from .enums import * diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 890dcb81..f445a2f0 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -24,14 +24,12 @@ warnings.warn("DSS-Extensions: DSS_EXTENSIONS_FASTDSS environment variable is set to 0; using the legacy full CFFI backend.") except: warnings.warn("DSS-Extensions: Could not import the FastDSS backend; using the legacy full CFFI backend.") - pass + if AltDSS_PyContext is None: # Import the prepared function info if the fast implementation from # AltDSS_PyContext is not available. import dss_python_backend._func_info as _func_info - - # Assumed UTF8; unless the fast C extension (dss_python_backend._fast_strs) is not # used, this now has no effect but left to avoid breaking it for downstream users. @@ -41,10 +39,6 @@ warn_wrong_case = False -_AdvancedTypes = 1 << 1 -_ODDPyStrings = 1 << 2 # TODO: check if we still need this with the new defaults -_UseLists = 1 << 3 - def set_case_insensitive_attributes(use: bool = True, warn: bool = False): ''' @@ -93,20 +87,343 @@ def _is_case_insensitive() -> bool: DssException = DSSException use_com_compat = set_case_insensitive_attributes - class CtxLib: ''' Exposes a CFFI Lib object pre-binding the DSSContext (`ctx`) object to the - `ctx_*` functions. + `ctx_*` functions, suppressing the `ctx_` prefix. This allows much simpler + backwards compatibility. ''' + _CtxSettings_UseExceptions = 1 << 0 + _CtxSettings_AdvancedTypes = 1 << 1 + _CtxSettings_ODDPyStrings = 1 << 2 # TODO: check if we still need this with the new defaults + _CtxSettings_UseLists = 1 << 3 + + def get_float64_array(self, func, *args) -> Float64Array: + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy() + self.DSS_Dispose_PDouble(ptr) + + if cnt[3] and (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + # If the last element is filled, we have a matrix. Otherwise, the + # matrix feature is disabled or the result is indeed a vector + return res.reshape((cnt[2], cnt[3]), order='F') + + return res + + def get_complex128_array(self, func, *args) -> Float64ArrayOrComplexArray: + if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + return self.get_float64_array(func, *args) + + # Currently we use the same as API as get_float64_array, may change later + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() + self.DSS_Dispose_PDouble(ptr) + + if cnt[3]: + # If the last element is filled, we have a matrix. Otherwise, the + # matrix feature is disabled or the result is indeed a vector + return res.reshape((cnt[2], cnt[3]), order='F') + + return res + + def get_fcomplex128_array(self, func, *args) -> Union[ComplexArray, None]: + # Currently we use the same as API as get_float64_array, may change later + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if cnt[0] == 1: # empty + res = None + else: + res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() + self.DSS_Dispose_PDouble(ptr) + + if cnt[3]: + # If the last element is filled, we have a matrix. Otherwise, the + # matrix feature is disabled or the result is indeed a vector + return res.reshape((cnt[2], cnt[3]), order='F') + + return res + + def get_complex128_array2(self, func, *args) -> Float64ArrayOrComplexArray: + if not self.advanced_types: + return self.get_float64_array2(func, *args) + + # Currently we use the same as API as get_float64_array, may change later + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + ptr = self._ffi.cast('double _Complex **', ptr) + res = self._ffi.unpack(ptr[0], cnt[0] >> 1) + self.DSS_Dispose_PDouble(ptr) + return res + + + def get_complex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: + if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + return self.get_float64_array(func, *args) + + # Currently we use the same as API as get_float64_array, may change later + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + try: + assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) + return self._ffi.cast('double _Complex**', ptr)[0][0] + finally: + self.DSS_Dispose_PDouble(ptr) + + def get_fcomplex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: + # Currently we use the same as API as get_float64_array, may change later + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + try: + assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) + return self._ffi.cast('double _Complex**', ptr)[0][0] + finally: + self.DSS_Dispose_PDouble(ptr) + + + def get_complex128_simple2(self, func, *args) -> List[Union[complex, float]]: + if not self.advanced_types: + return self.get_float64_array2(func, *args) + + # Currently we use the same as API as get_float64_array, may change later + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + try: + assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) + return self._ffi.cast('double _Complex**', ptr)[0][0] + finally: + self.DSS_Dispose_PDouble(ptr) + + + def get_float64_gr_array(self) -> Float64Array: + ptr, cnt = self.gr_float64_pointers + settings = self.settings_ptr[0] + if (settings & (1 << 3)): # self.prefer_lists: + return self._unpack(ptr[0], cnt[0]) + if cnt[3] and (settings & (1 << 1)): # self.advanced_types: + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy().reshape((cnt[2], cnt[3]), order='F') + + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy() + + + def get_complex128_gr_array(self) -> ComplexArray: + settings = self.settings_ptr[0] + if not (settings & (1 << 1)): # self.advanced_types: + return self.get_float64_gr_array() + + # Currently we use the same as API as get_float64_array, may change later + ptr, cnt = self.gr_float64_pointers + if (settings & (1 << 3)): # self.prefer_lists: + ptr = self._ffi.cast('double _Complex **', ptr) + return self._unpack(ptr[0], cnt[0] >> 1) + + if cnt[3] and (settings & (1 << 1)): # self.advanced_types: + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') + + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() + + + def get_fcomplex128_gr_array(self) -> ComplexArray: + # This one does not need to check "prefer_lists" + # Currently we use the same as API as get_float64_array, may change later + ptr, cnt = self.gr_float64_pointers + if cnt[3] and (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') + + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() + + + def get_complex128_gr_simple(self) -> Float64ArrayOrSimpleComplex: + if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + return self.get_float64_gr_array() + + # Currently we use the same as API as get_float64_array, may change later + ptr, cnt = self.gr_cfloat64_pointers + assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) + return ptr[0][0] + + + def get_fcomplex128_gr_simple(self) -> complex: + # Currently we use the same as API as get_float64_array, may change later + ptr, cnt = self.gr_cfloat64_pointers + assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) + return ptr[0][0] + + + def get_complex128_gr_simple2(self) -> List[Union[complex, float]]: + if not self.advanced_types: + return self.get_float64_gr_array2() + + # Currently we use the same as API as get_float64_array, may change later + ptr, cnt = self.gr_cfloat64_pointers + assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) + return ptr[0][0] + + + def get_int32_array(self, func: Callable, *args) -> Int32Array: + ptr = self._ffi.new('int32_t**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() + self.DSS_Dispose_PInteger(ptr) + + if cnt[3] and (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + # If the last element is filled, we have a matrix. Otherwise, the + # matrix feature is disabled or the result is indeed a vector + return res.reshape((cnt[2], cnt[3])) + + return res + + + def get_int32_gr_array(self) -> Int32Array: + ptr, cnt = self.gr_int32_pointers + settings = self.settings_ptr[0] + if (settings & (1 << 3)): # self.prefer_lists: + return self._unpack(ptr[0], cnt[0]) + if cnt[3] and (settings & (1 << 1)): # self.advanced_types: + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy().reshape((cnt[2], cnt[3])) + + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() + + + def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: + ptr = self._ffi.new('int8_t**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() + self.DSS_Dispose_PByte(ptr) + + if cnt[3] and self.advanced_types: + # If the last element is filled, we have a matrix. Otherwise, the + # matrix feature is disabled or the result is indeed a vector + return res.reshape((cnt[2], cnt[3])) + + return res + + + def get_int8_gr_array(self) -> Int8Array: + ptr, cnt = self.gr_int8_pointers + settings = self.settings_ptr[0] + if (settings & (1 << 3)): # self.prefer_lists: + return self._unpack(ptr[0], cnt[0]) + if cnt[3] and (settings & (1 << 1)): # self.advanced_types: + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy().reshape((cnt[2], cnt[3]), order='F') + + return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() + + + def get_string_array(self, func: Callable, *args: Any) -> List[str]: + ptr = self._ffi.new('char***') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if not cnt[0]: + res = [] + else: + actual_ptr = ptr[0] + if actual_ptr == self._ffi.NULL: + res = [] + else: + codec = self.codec + str_ptrs = self._unpack(actual_ptr, cnt[0]) + #res = [(str(self._ffi.string(str_ptr).decode(codec)) if (str_ptr != self._ffi.NULL) else None) for str_ptr in str_ptrs] + res = [(self._ffi.string(str_ptr).decode(codec) if (str_ptr != self._ffi.NULL) else u'') for str_ptr in str_ptrs] + + self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) + return res + + + def get_string_array2(self, func, *args): # for compatibility with OpenDSSDirect.py + ptr = self._ffi.new('char***') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + + if not cnt[0]: + res = [] + else: + actual_ptr = ptr[0] + if actual_ptr == self._ffi.NULL: + res = [] + else: + codec = self.codec + res = [(str(self._ffi.string(actual_ptr[i]).decode(codec)) if (actual_ptr[i] != self._ffi.NULL) else '') for i in range(cnt[0])] + if res == [u'']: + # most COM methods return an empty array as an + # array with an empty string + res = [] + + if len(res) == 1 and res[0].lower() == 'none': + res = [] + + self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) + return res + + + def get_float64_array2(self, func, *args): + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if not cnt[0]: + res = [] + else: + res = self._ffi.unpack(ptr[0], cnt[0]) + + self.DSS_Dispose_PDouble(ptr) + return res + + def get_float64_gr_array2(self): + ptr, cnt = self.gr_float64_pointers + return self._ffi.unpack(ptr[0], cnt[0]) + + def get_int32_array2(self, func, *args): + ptr = self._ffi.new('int32_t**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if not cnt[0]: + res = None + else: + res = self._ffi.unpack(ptr[0], cnt[0]) + + self.DSS_Dispose_PInteger(ptr) + return res + + def get_int32_gr_array2(self): + ptr, cnt = self.gr_int32_pointers + return self._ffi.unpack(ptr[0], cnt[0]) + + def get_int8_array2(self, func, *args): + ptr = self._ffi.new('int8_t**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if not cnt[0]: + res = None + else: + res = self._ffi.unpack(ptr[0], cnt[0]) + + self.DSS_Dispose_PByte(ptr) + return res + + def get_int8_gr_array2(self): + ptr, cnt = self.gr_int8_pointers + return self._ffi.unpack(ptr[0], cnt[0]) + + def _get_strs_ctx(self, errorPtr, ctx, func: Callable, *args: Any) -> List[str]: ffi = self._ffi codec = self._api_util.codec + settings = self.settings_ptr[0] ptr = ffi.new('char***') cnt = ffi.new('int32_t[4]') func(ctx, ptr, cnt, *args) - if errorPtr[0] and Base._use_exceptions: + if errorPtr[0] and (settings & 1): # self.using_exceptions: error_num = errorPtr[0] errorPtr[0] = 0 self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) @@ -122,24 +439,35 @@ def _get_strs_ctx(self, errorPtr, ctx, func: Callable, *args: Any) -> List[str]: str_ptrs = ffi.unpack(actual_ptr, cnt[0]) res = [(ffi.string(str_ptr).decode(codec) if (str_ptr) else '') for str_ptr in str_ptrs] + if (settings & (1 << 2)): # self.oddpy_strs: + # originally on get_string_array2, for compatibility with OpenDSSDirect.py + if res == ['']: + # most COM methods return an empty array as an + # array with an empty string + res = [] + + if len(res) == 1 and res[0].lower() == 'none': + res = [] + self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) return res def _get_bool_ctx(self, errorPtr, ctx, func: Callable, *args): result = bool(func(ctx, *args)) - if errorPtr[0] and Base._use_exceptions: + if errorPtr[0] and self.using_exceptions: error_num = errorPtr[0] errorPtr[0] = 0 raise DSSException(error_num, self.Error_Get_Description()) return result != 0 + def _get_str_ctx(self, errorPtr, ctx, func: Callable, *args): codec = self._api_util.codec ffi = self._ffi result = func(ctx, *args) - if errorPtr[0] and Base._use_exceptions: + if errorPtr[0] and self.using_exceptions: error_num = errorPtr[0] errorPtr[0] = 0 raise DSSException(error_num, self.Error_Get_Description()) @@ -173,16 +501,15 @@ def _prepare_api_functions_slow(self, done): t = _func_info.t api_util = self._api_util is_oddie = api_util._is_oddie - wrappers = { - t.fastdss_types_b16: ('', self._get_bool_ctx,), + t.fastdss_types_u16: ('', self._get_bool_ctx,), t.fastdss_types_str: ('', self._get_str_ctx,), t.fastdss_types_strs: ('', self._get_strs_ctx,), - t.fastdss_types_gr_f64s: ('_GR', self._error_checked_ctx_gr, api_util.get_float64_gr_array), - t.fastdss_types_gr_i32s: ('_GR', self._error_checked_ctx_gr, api_util.get_int32_gr_array), - t.fastdss_types_gr_i8s: ('_GR', self._error_checked_ctx_gr, api_util.get_int8_gr_array), - t.fastdss_types_gr_z128: ('_GR', self._error_checked_ctx_gr, api_util.get_complex128_gr_simple), - t.fastdss_types_gr_z128s: ('_GR', self._error_checked_ctx_gr, api_util.get_complex128_gr_array), + t.fastdss_types_gr_f64s: ('_GR', self._error_checked_ctx_gr, self.get_float64_gr_array), + t.fastdss_types_gr_i32s: ('_GR', self._error_checked_ctx_gr, self.get_int32_gr_array), + t.fastdss_types_gr_i8s: ('_GR', self._error_checked_ctx_gr, self.get_int8_gr_array), + t.fastdss_types_gr_z128: ('_GR', self._error_checked_ctx_gr, self.get_complex128_gr_simple), + t.fastdss_types_gr_z128s: ('_GR', self._error_checked_ctx_gr, self.get_complex128_gr_array), } arg_no_wrapper = lambda f: f @@ -217,6 +544,7 @@ def _prepare_api_functions_slow(self, done): def _prepare_api_functions(self, done, settings_ptr): + self._settings_ptr = settings_ptr if AltDSS_PyContext is None: self._prepare_api_functions_slow(done) return @@ -225,18 +553,12 @@ def _prepare_api_functions(self, done, settings_ptr): ffi = self._ffi ctx_int = int(ffi.cast('uintptr_t', ctx)) lib_int = int(ffi.cast('uintptr_t', self._api_util.lib_unpatched)) - self._settings_ptr = settings_ptr settings_ptr_int = int(ffi.cast('uintptr_t', self._settings_ptr)) self._fast = AltDSS_PyContext(ctx_int, lib_int, settings_ptr_int, DSSException, done, self) - def _get_string(self, b) -> str: - if b: - return self._ffi.string(b).decode() - return '' - def _error_checked(self, _errorPtr, f, *args): result = f(*args) - if _errorPtr[0] and Base._use_exceptions: + if _errorPtr[0] and self.using_exceptions: error_num = _errorPtr[0] _errorPtr[0] = 0 raise DSSException(error_num, self.Error_Get_Description()) @@ -245,7 +567,7 @@ def _error_checked(self, _errorPtr, f, *args): def _error_checked_ctx(self, _errorPtr, ctx, f, *args): result = f(ctx, *args) - if _errorPtr[0] and Base._use_exceptions: + if _errorPtr[0] and self.using_exceptions: error_num = _errorPtr[0] _errorPtr[0] = 0 raise DSSException(error_num, self.Error_Get_Description()) @@ -254,19 +576,76 @@ def _error_checked_ctx(self, _errorPtr, ctx, f, *args): def _error_checked_ctx_gr(self, _errorPtr, ctx, f, _res_func, *args): f(ctx, *args) - if _errorPtr[0] and Base._use_exceptions: + if _errorPtr[0] and self.using_exceptions: error_num = _errorPtr[0] _errorPtr[0] = 0 raise DSSException(error_num, self.Error_Get_Description()) return _res_func() + + @property + def using_exceptions(self) -> bool: + return (self.settings_ptr[0] & CtxLib._CtxSettings_UseExceptions) != 0 + + @using_exceptions.setter + def using_exceptions(self, do_enable: bool): + if do_enable: + self.settings_ptr[0] = self.settings_ptr[0] | CtxLib._CtxSettings_UseExceptions + else: + self.settings_ptr[0] = self.settings_ptr[0] & ~CtxLib._CtxSettings_UseExceptions + + + @property + def prefer_lists(self) -> bool: + return (self.settings_ptr[0] & CtxLib._CtxSettings_UseLists) != 0 + + @prefer_lists.setter + def prefer_lists(self, value: bool): + settings_ptr = self.settings_ptr + if value: + settings_ptr[0] = settings_ptr[0] | CtxLib._CtxSettings_UseLists + else: + settings_ptr[0] = settings_ptr[0] & ~CtxLib._CtxSettings_UseLists + + + @property + def oddpy_strs(self) -> bool: + return (self.settings_ptr[0] & CtxLib._CtxSettings_ODDPyStrings) != 0 + + @oddpy_strs.setter + def oddpy_strs(self, value: bool): + settings_ptr = self.settings_ptr + if value: + settings_ptr[0] = settings_ptr[0] | CtxLib._CtxSettings_ODDPyStrings + else: + settings_ptr[0] = settings_ptr[0] & ~CtxLib._CtxSettings_ODDPyStrings + + + + @property + def advanced_types(self) -> bool: + return (self.settings_ptr[0] & CtxLib._CtxSettings_AdvancedTypes) != 0 + + @advanced_types.setter + def advanced_types(self, value: bool): + settings_ptr = self.settings_ptr + if value: + settings_ptr[0] = settings_ptr[0] | CtxLib._CtxSettings_AdvancedTypes + else: + settings_ptr[0] = settings_ptr[0] & ~CtxLib._CtxSettings_AdvancedTypes + def __init__(self, api_util, settings_ptr): self._api_util = api_util # this is not ready, don't use it yet lib = self._lib = api_util.lib_unpatched ctx = self._ctx = api_util.ctx ffi = self._ffi = api_util.ffi + self._unpack = ffi.unpack self.settings_ptr = settings_ptr + self.gr_float64_pointers = api_util.gr_float64_pointers + self.gr_int32_pointers = api_util.gr_int32_pointers + self.gr_int8_pointers = api_util.gr_int8_pointers + self.gr_cfloat64_pointers = api_util.gr_cfloat64_pointers self._errorPtr = _errorPtr = lib.Error_Get_NumberPtr(ctx) #TODO: test if a pointer is better than keeping this @@ -276,7 +655,7 @@ def __init__(self, api_util, settings_ptr): done = set(('Error_Get_Description', 'Error_Get_Number',)) self._prepare_api_functions(done, settings_ptr) - self.Error_Get_Description = lambda: self._get_string(lib.Error_Get_Description(ctx)) + self.Error_Get_Description = lambda: self._api_util.get_string(lib.Error_Get_Description(ctx)) self.Error_Get_Number = lambda: lib.Error_Get_Number(ctx) skip_funcs = { @@ -293,7 +672,7 @@ def __init__(self, api_util, settings_ptr): 'Batch_Create', 'Batch_Filter', ) - # First, process all `ctx_*`` functions + # First, process all `ctx_*` functions for name in dir(lib): if name in done: @@ -302,8 +681,8 @@ def __init__(self, api_util, settings_ptr): if name.startswith(skip_prefixes) and not name.startswith(force_include_prefixes): continue + # Note: NULL function pointers here are fine since CFFI v1.13 (released in 2019). value = getattr(lib, name) - # print('>>>', name) # Keep the basic management functions alone if name in skip_funcs: @@ -317,7 +696,7 @@ def __init__(self, api_util, settings_ptr): if name.endswith('_GR'): # A few GR functions that don't have dedicated low-level mapping - wrapper_func, res_func = self._error_checked_ctx_gr, api_util.get_float64_gr_array + wrapper_func, res_func = self._error_checked_ctx_gr, self.get_float64_gr_array setattr(self, name, partial(wrapper_func, _errorPtr, ctx, value, res_func)) done.add(name) continue @@ -382,7 +761,7 @@ class Base: '_frozen_attrs', ] - _use_exceptions = True + using_exceptions = True _oddpy = False def __init__(self, api_util, prefer_lists=False): @@ -390,41 +769,43 @@ def __init__(self, api_util, prefer_lists=False): self._api_util = api_util self._get_string = api_util.get_string - self._get_fcomplex128_gr_array = api_util.get_fcomplex128_gr_array - self._get_fcomplex128_array = api_util.get_fcomplex128_array - self._get_fcomplex128_simple = api_util.get_fcomplex128_simple - self._get_fcomplex128_gr_simple = api_util.get_fcomplex128_gr_simple - self._lib = api_util._get_lib(prefer_lists, self._oddpy) + lib = self._lib + + #TODO: remove all _get_* after AltDSS is fully migrated + self._get_fcomplex128_gr_array = lib.get_fcomplex128_gr_array + self._get_fcomplex128_array = lib.get_fcomplex128_array + self._get_fcomplex128_simple = lib.get_fcomplex128_simple + self._get_fcomplex128_gr_simple = lib.get_fcomplex128_gr_simple if not prefer_lists: # Use NumPy arrays for most functions - self._get_float64_array = api_util.get_float64_array - self._get_float64_gr_array = api_util.get_float64_gr_array - self._get_int32_array = api_util.get_int32_array - self._get_int32_gr_array = api_util.get_int32_gr_array - self._get_int8_array = api_util.get_int8_array - self._get_int8_gr_array = api_util.get_int8_gr_array - self._get_string_array = api_util.get_string_array + self._get_float64_array = lib.get_float64_array + self._get_float64_gr_array = lib.get_float64_gr_array + self._get_int32_array = lib.get_int32_array + self._get_int32_gr_array = lib.get_int32_gr_array + self._get_int8_array = lib.get_int8_array + self._get_int8_gr_array = lib.get_int8_gr_array + self._get_string_array = lib.get_string_array - self._get_complex128_array = api_util.get_complex128_array - self._get_complex128_simple = api_util.get_complex128_simple - self._get_complex128_gr_array = api_util.get_complex128_gr_array - self._get_complex128_gr_simple = api_util.get_complex128_gr_simple + self._get_complex128_array = lib.get_complex128_array + self._get_complex128_simple = lib.get_complex128_simple + self._get_complex128_gr_array = lib.get_complex128_gr_array + self._get_complex128_gr_simple = lib.get_complex128_gr_simple else: # Classic OpenDSSDirect.py style, using mostly lists - self._get_float64_array = api_util.get_float64_array2 - self._get_float64_gr_array = api_util.get_float64_gr_array2 - self._get_int32_array = api_util.get_int32_array2 - self._get_int32_gr_array = api_util.get_int32_gr_array2 - self._get_int8_array = api_util.get_int8_array2 - self._get_int8_gr_array = api_util.get_int8_gr_array2 - self._get_string_array = api_util.get_string_array2 + self._get_float64_array = lib.get_float64_array2 + self._get_float64_gr_array = lib.get_float64_gr_array2 + self._get_int32_array = lib.get_int32_array2 + self._get_int32_gr_array = lib.get_int32_gr_array2 + self._get_int8_array = lib.get_int8_array2 + self._get_int8_gr_array = lib.get_int8_gr_array2 + self._get_string_array = lib.get_string_array2 - self._get_complex128_array = api_util.get_complex128_array2 - self._get_complex128_simple = api_util.get_complex128_simple2 - self._get_complex128_gr_array = api_util.get_complex128_gr_array2 - self._get_complex128_gr_simple = api_util.get_complex128_gr_simple2 + self._get_complex128_array = lib.get_complex128_array2 + self._get_complex128_simple = lib.get_complex128_simple2 + self._get_complex128_gr_array = lib.get_complex128_gr_array2 + self._get_complex128_gr_simple = lib.get_complex128_gr_simple2 self._prepare_complex128_array = api_util.prepare_complex128_array self._prepare_complex128_simple = api_util.prepare_complex128_simple @@ -434,7 +815,6 @@ def __init__(self, api_util, prefer_lists=False): self._prepare_string_array = api_util.prepare_string_array self._errorPtr = self._api_util._errorPtr - cls = type(self) if cls not in interface_classes: interface_classes.add(cls) @@ -443,26 +823,6 @@ def __init__(self, api_util, prefer_lists=False): cls._dss_attributes = lowercase_map - @staticmethod - def _enable_exceptions(do_enable: bool): - """ - Controls whether the automatic error checking mechanism is enable, i.e., if - the DSS engine errors (from the `Error` interface) are mapped exception when - detected. - - **When disabled, the user takes responsibility for checking for errors.** - This can be done through the `Error` interface. When `Error.Number` is not - zero, there should be an error message in `Error.Description`. This is compatible - with the behavior on the official OpenDSS (Windows-only COM implementation) when - `AllowForms` is disabled. - - Users can also use the DSS command `Export ErrorLog` to inspect for errors. - - **WARNING:** This is a global setting, affects all DSS instances from DSS-Python - and OpenDSSDirect.py. - """ - Base._use_exceptions = bool(do_enable) - def _check_for_error(self, result=None): """ Checks for a DSS engine error (on the default configuration). @@ -475,7 +835,7 @@ def _check_for_error(self, result=None): Note that, **in the future**, we may try showing a popup form like the official OpenDSS does on Windows if AllowForms is True. This behavior is not very portable though and not adequate for automated scripts. """ - if self._errorPtr[0] and Base._use_exceptions: + if self._errorPtr[0] and Base.using_exceptions: error_num = self._errorPtr[0] self._errorPtr[0] = 0 raise DSSException(error_num, self._lib.Error_Get_Description()) @@ -550,7 +910,7 @@ class AltDSSAPIUtil: _altdss: AltDSS - def __init__(self, ffi, lib, ctx=None, is_oddie=False): + def __init__(self, ffi, lib, ctx=None, is_oddie=False, parent: Optional[AltDSSAPIUtil] = None): self._opendssdirect = None self._dss_python = None self._altdss = None @@ -566,6 +926,7 @@ def __init__(self, ffi, lib, ctx=None, is_oddie=False): self._bus_ref_to_name = None self._is_clearing = False self._map_objs = True + self._parent = parent if ctx is None: self.lib = lib ctx = lib.ctx_Get_Prime() @@ -573,60 +934,58 @@ def __init__(self, ffi, lib, ctx=None, is_oddie=False): self.init_buffers() self.settings_ptr = settings_ptr_dsspy = ffi.new('int32_t*') - settings_ptr_dsspy[0] = 0 + + # If a parent is provided, copy the settings + if self._parent is None: + settings_ptr_dsspy[0] = CtxLib._CtxSettings_UseExceptions + else: + settings_ptr_dsspy[0] = self._parent.settings_ptr[0] + self.lib = CtxLib(self, settings_ptr_dsspy) + self.lib_odd = None if ctx not in AltDSSAPIUtil._ctx_to_util: AltDSSAPIUtil._ctx_to_util[ctx] = self self.track_objects = True self.register_callbacks() - self.lib_odd = None - - @property - def _advanced_types(self) -> bool: - return (self.settings_ptr[0] & _AdvancedTypes) != 0 + if self._parent is None : + self.lib_odd = None + elif self._parent.lib_odd is not None: + self.lib_odd = self._get_lib(True) - @_advanced_types.setter - def _advanced_types(self, value: bool): - if value: - self.settings_ptr[0] = self.settings_ptr[0] | _AdvancedTypes - else: - self.settings_ptr[0] = self.settings_ptr[0] & ~_AdvancedTypes - def _get_lib(self, prefer_lists: bool, oddpy: bool): + def _get_lib(self, oddpy: bool): ''' - Returns a context prepared for OpenDSSDirect.py + Returns a context lib, optionally prepared for OpenDSSDirect.py This should be removed as we unify settings across the modules later. ''' - if AltDSS_PyContext is None or not oddpy: - # If the fast module is not available, nothing to do - - if prefer_lists: - self.settings_ptr[0] = self.settings_ptr[0] | _UseLists - else: - self.settings_ptr[0] = self.settings_ptr[0] & (~_UseLists) + if not oddpy: # and (AltDSS_PyContext is None): + if self.lib is None: + if self.settings_ptr is None: + self.settings_ptr = self.ffi.new('int32_t*') + if self._parent is not None: + self.settings_ptr[0] = self._parent.settings_ptr[0] + else: + self.settings_ptr[0] = self.settings_ptr[0] & ~CtxLib._CtxSettings_ODDPyStrings + + self.lib = CtxLib(self, self.settings_ptr) return self.lib + # Check it the current lib is OK if self.lib_odd is not None: - # We already have a prepared object, just ensure the settings are OK - - if prefer_lists: - settings_oddpy_ptr = self.lib_odd.settings_ptr - settings_oddpy_ptr[0] = settings_oddpy_ptr[0] | _ODDPyStrings | _UseLists - else: - settings_oddpy_ptr[0] = (settings_oddpy_ptr[0] | _ODDPyStrings) & (~_UseLists) - return self.lib_odd - settings_oddpy_ptr = self.ffi.new('int32_t*') - if prefer_lists: - settings_oddpy_ptr[0] = self.settings_ptr[0] | _ODDPyStrings | _UseLists + # Return a new lib object with the correct settings + + self.settings_oddpy_ptr = self.ffi.new('int32_t*') + if self._parent is not None: + self.settings_oddpy_ptr[0] = self._parent.settings_oddpy_ptr[0] else: - settings_oddpy_ptr[0] = (self.settings_ptr[0] | _ODDPyStrings) & (~_UseLists) + self.settings_oddpy_ptr[0] = self.settings_ptr[0] | CtxLib._CtxSettings_ODDPyStrings - self.lib_odd = CtxLib(self, settings_oddpy_ptr) + self.lib_odd = CtxLib(self, self.settings_oddpy_ptr) return self.lib_odd @@ -642,7 +1001,7 @@ def _check_for_error(self, result=None): Note that, **in the future**, we may try showing a popup form like the official OpenDSS does on Windows if AllowForms is True. This behavior is not very portable though and not adequate for automated scripts. """ - if self._errorPtr[0] and Base._use_exceptions: + if self._errorPtr[0] and Base.using_exceptions: error_num = self._errorPtr[0] self._errorPtr[0] = 0 raise DSSException(error_num, self.lib.Error_Get_Description()) @@ -815,192 +1174,6 @@ def get_string(self, b) -> str: return self.ffi.string(b).decode(self.codec) return '' - def get_float64_array(self, func, *args) -> Float64Array: - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy() - self.lib.DSS_Dispose_PDouble(ptr) - - if cnt[3] and self._advanced_types: - # If the last element is filled, we have a matrix. Otherwise, the - # matrix feature is disabled or the result is indeed a vector - return res.reshape((cnt[2], cnt[3]), order='F') - - return res - - def get_complex128_array(self, func, *args) -> Float64ArrayOrComplexArray: - if not self._advanced_types: - return self.get_float64_array(func, *args) - - # Currently we use the same as API as get_float64_array, may change later - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() - self.lib.DSS_Dispose_PDouble(ptr) - - if cnt[3]: - # If the last element is filled, we have a matrix. Otherwise, the - # matrix feature is disabled or the result is indeed a vector - return res.reshape((cnt[2], cnt[3]), order='F') - - return res - - def get_fcomplex128_array(self, func, *args) -> Union[ComplexArray, None]: - # Currently we use the same as API as get_float64_array, may change later - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if cnt[0] == 1: # empty - res = None - else: - res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() - self.lib.DSS_Dispose_PDouble(ptr) - - if cnt[3]: - # If the last element is filled, we have a matrix. Otherwise, the - # matrix feature is disabled or the result is indeed a vector - return res.reshape((cnt[2], cnt[3]), order='F') - - return res - - def get_complex128_array2(self, func, *args) -> Float64ArrayOrComplexArray: - if not self._advanced_types: - return self.get_float64_array2(func, *args) - - # Currently we use the same as API as get_float64_array, may change later - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - ptr = self.ffi.cast('double _Complex **', ptr) - res = self.ffi.unpack(ptr[0], cnt[0] >> 1) - self.lib.DSS_Dispose_PDouble(ptr) - return res - - - def get_complex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: - if not self._advanced_types: - return self.get_float64_array(func, *args) - - # Currently we use the same as API as get_float64_array, may change later - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - try: - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return self.ffi.cast('double _Complex**', ptr)[0][0] - finally: - self.lib.DSS_Dispose_PDouble(ptr) - - def get_fcomplex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: - # Currently we use the same as API as get_float64_array, may change later - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - try: - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return self.ffi.cast('double _Complex**', ptr)[0][0] - finally: - self.lib.DSS_Dispose_PDouble(ptr) - - - def get_complex128_simple2(self, func, *args) -> List[Union[complex, float]]: - if not self._advanced_types: - return self.get_float64_array2(func, *args) - - # Currently we use the same as API as get_float64_array, may change later - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - try: - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return self.ffi.cast('double _Complex**', ptr)[0][0] - finally: - self.lib.DSS_Dispose_PDouble(ptr) - - - def get_float64_gr_array(self) -> Float64Array: - ptr, cnt = self.gr_float64_pointers - if cnt[3] and self._advanced_types: - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') - - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=np.float64).copy() - - - def get_complex128_gr_array(self) -> ComplexArray: - if not self._advanced_types: - return self.get_float64_gr_array() - - # Currently we use the same as API as get_float64_array, may change later - ptr, cnt = self.gr_float64_pointers - if cnt[3] and self._advanced_types: - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') - - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() - - - def get_fcomplex128_gr_array(self) -> ComplexArray: - # Currently we use the same as API as get_float64_array, may change later - ptr, cnt = self.gr_float64_pointers - if cnt[3] and self._advanced_types: - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy().reshape((cnt[2], cnt[3]), order='F') - - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() - - - def get_complex128_gr_array2(self) -> List[Union[complex, float]]: - if not self._advanced_types: - return self.get_float64_gr_array2() - - # Currently we use the same as API as get_float64_array, may change later - ptr, cnt = self.gr_float64_pointers - ptr = self.ffi.cast('double _Complex **', ptr) - return self.ffi.unpack(ptr[0], cnt[0] >> 1) - - - def get_complex128_gr_simple(self) -> Float64ArrayOrSimpleComplex: - if not self._advanced_types: - return self.get_float64_gr_array() - - # Currently we use the same as API as get_float64_array, may change later - ptr, cnt = self.gr_cfloat64_pointers - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return ptr[0][0] - - - def get_fcomplex128_gr_simple(self) -> complex: - # Currently we use the same as API as get_float64_array, may change later - ptr, cnt = self.gr_cfloat64_pointers - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return ptr[0][0] - - - def get_complex128_gr_simple2(self) -> List[Union[complex, float]]: - if not self._advanced_types: - return self.get_float64_gr_array2() - - # Currently we use the same as API as get_float64_array, may change later - ptr, cnt = self.gr_cfloat64_pointers - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return ptr[0][0] - - - def get_int32_array(self, func: Callable, *args) -> Int32Array: - ptr = self.ffi.new('int32_t**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() - self.lib.DSS_Dispose_PInteger(ptr) - - if cnt[3] and self._advanced_types: - # If the last element is filled, we have a matrix. Otherwise, the - # matrix feature is disabled or the result is indeed a vector - return res.reshape((cnt[2], cnt[3])) - - return res - - def get_ptr_array(self, func: Callable, *args): ptr = self.ffi.new('void***') cnt = self.ffi.new('int32_t[4]') @@ -1009,137 +1182,10 @@ def get_ptr_array(self, func: Callable, *args): self.lib.DSS_Dispose_PPointer(ptr) return res - - def get_int32_gr_array(self) -> Int32Array: - ptr, cnt = self.gr_int32_pointers - if cnt[3] and self._advanced_types: - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy().reshape((cnt[2], cnt[3])) - - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() - - - def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: - ptr = self.ffi.new('int8_t**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - res = np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() - self.lib.DSS_Dispose_PByte(ptr) - - if cnt[3] and self._advanced_types: - # If the last element is filled, we have a matrix. Otherwise, the - # matrix feature is disabled or the result is indeed a vector - return res.reshape((cnt[2], cnt[3])) - - return res - - - def get_int8_gr_array(self) -> Int8Array: - ptr, cnt = self.gr_int8_pointers - if cnt[3] and self._advanced_types: - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy().reshape((cnt[2], cnt[3]), order='F') - - return np.frombuffer(self.ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() - - - def get_string_array(self, func: Callable, *args: Any) -> List[str]: - ptr = self.ffi.new('char***') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = [] - else: - actual_ptr = ptr[0] - if actual_ptr == self.ffi.NULL: - res = [] - else: - codec = self.codec - str_ptrs = self.ffi.unpack(actual_ptr, cnt[0]) - #res = [(str(self.ffi.string(str_ptr).decode(codec)) if (str_ptr != self.ffi.NULL) else None) for str_ptr in str_ptrs] - res = [(self.ffi.string(str_ptr).decode(codec) if (str_ptr != self.ffi.NULL) else u'') for str_ptr in str_ptrs] - - self.lib.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) - return res - - - def get_string_array2(self, func, *args): # for compatibility with OpenDSSDirect.py - ptr = self.ffi.new('char***') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - - if not cnt[0]: - res = [] - else: - actual_ptr = ptr[0] - if actual_ptr == self.ffi.NULL: - res = [] - else: - codec = self.codec - res = [(str(self.ffi.string(actual_ptr[i]).decode(codec)) if (actual_ptr[i] != self.ffi.NULL) else '') for i in range(cnt[0])] - if res == [u'']: - # most COM methods return an empty array as an - # array with an empty string - res = [] - - if len(res) == 1 and res[0].lower() == 'none': - res = [] - - self.lib.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) - return res - - def set_string_array(self, func: Callable, value: List[AnyStr], *args): value, value_ptr, value_count = self.prepare_string_array(value) func(value_ptr, value_count, *args) - - def get_float64_array2(self, func, *args): - ptr = self.ffi.new('double**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = [] - else: - res = self.ffi.unpack(ptr[0], cnt[0]) - - self.lib.DSS_Dispose_PDouble(ptr) - return res - - def get_float64_gr_array2(self): - ptr, cnt = self.gr_float64_pointers - return self.ffi.unpack(ptr[0], cnt[0]) - - def get_int32_array2(self, func, *args): - ptr = self.ffi.new('int32_t**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = None - else: - res = self.ffi.unpack(ptr[0], cnt[0]) - - self.lib.DSS_Dispose_PInteger(ptr) - return res - - def get_int32_gr_array2(self): - ptr, cnt = self.gr_int32_pointers - return self.ffi.unpack(ptr[0], cnt[0]) - - def get_int8_array2(self, func, *args): - ptr = self.ffi.new('int8_t**') - cnt = self.ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = None - else: - res = self.ffi.unpack(ptr[0], cnt[0]) - - self.lib.DSS_Dispose_PByte(ptr) - return res - - def get_int8_gr_array2(self): - ptr, cnt = self.gr_int8_pointers - return self.ffi.unpack(ptr[0], cnt[0]) - def prepare_float64_array(self, value): if type(value) is not np.ndarray or value.dtype != np.float64: value = np.asarray(value, dtype=np.float64) diff --git a/tests/_settings.py b/tests/_settings.py index 7cb789a3..1244a0a0 100644 --- a/tests/_settings.py +++ b/tests/_settings.py @@ -4,7 +4,7 @@ import faulthandler faulthandler.disable() from dss import DSS -DSS.COMErrorResults = False +DSS.ActiveCircuit.Settings.COMErrorResults = False try: from dss import IOddieDSS except: diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 1474d86d..1bbee793 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -443,7 +443,7 @@ def get_archive_fn(live_fn, fn_prefix=None): elif SAVE_DSSX_OUTPUT: from dss import DSS, DSSCompatFlags - DSS.CompatFlags = 0 # DSSCompatFlags.InvControl9611 + DSS.ActiveCircuit.Settings.CompatFlags = 0 # DSSCompatFlags.InvControl9611 print("Using DSS-Extensions:", DSS.Version) match = re.match('DSS C-API Library version ([^ ]+) revision.* ([0-9]+);.*', DSS.Version) dssx_ver, dssx_timestamp = match.groups() diff --git a/tests/test_general.py b/tests/test_general.py index 3e5a6fc6..0c54f88c 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -22,13 +22,13 @@ def setup_function(): DSS.ClearAll() DSS.AllowForms = False - DSS.AdvancedTypes = False - DSS.CompatFlags = 0 + DSS.ActiveCircuit.Settings.AdvancedTypes = False + DSS.ActiveCircuit.Settings.CompatFlags = 0 if not DSS._api_util._is_oddie: DSS.AllowEditor = False DSS.AllowChangeDir = True - DSS.COMErrorResults = False + DSS.ActiveCircuit.Settings.COMErrorResults = False DSS.Error.UseExceptions = True DSS.Text.Command = 'set DefaultBaseFreq=60' @@ -192,7 +192,7 @@ def test_compat_precision(): DSS.ActiveCircuit.Vsources.First good = DSS.ActiveCircuit.ActiveCktElement.SeqVoltages.view(dtype=complex) - DSS.CompatFlags = DSSCompatFlags.BadPrecision + DSS.ActiveCircuit.Settings.CompatFlags = DSSCompatFlags.BadPrecision bad = DSS.ActiveCircuit.ActiveCktElement.SeqVoltages.view(dtype=complex) assert max(abs(good - bad)) > 1e-6 @@ -210,7 +210,7 @@ def test_compat_activeline(): assert name == Lines.Name - DSS.CompatFlags = DSSCompatFlags.ActiveLine + DSS.ActiveCircuit.Settings.CompatFlags = DSSCompatFlags.ActiveLine with pytest.raises(DSSException): assert name == Lines.Name @@ -275,8 +275,7 @@ def test_pm_threads(): if Parallel.NumCPUs < 4: return # Cannot run in this machine, e.g. won't run on GitHub Actions - if not isinstance(DSS, IOddieDSS): - DSS.AdvancedTypes = True + DSS.ActiveCircuit.Settings.AdvancedTypes = True DSS.Text.Command = 'set parallel=No' fn = os.path.abspath(f'{BASE_DIR}/Version8/Distrib/EPRITestCircuits/ckt5/Master_ckt5.dss') @@ -760,7 +759,7 @@ def test_capacitor_reactor(DSS: IDSS = DSS): from itertools import product kVA = 1329.53 kV = 2.222 - DSS.AdvancedTypes = True + DSS.ActiveCircuit.Settings.AdvancedTypes = True for component, f in product(('Capacitor', 'Reactor'), (50, 60)): bus = 1 @@ -970,10 +969,10 @@ def test_line_parent_compat(): DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.Text.Command = 'new energymeter.m1 element=transformer.sub' DSS.Text.Command = 'solve mode=snap' - DSS.CompatFlags = DSSCompatFlags.ActiveLine + DSS.ActiveCircuit.Settings.CompatFlags = DSSCompatFlags.ActiveLine Lines = DSS.ActiveCircuit.Lines res_compat = Lines.First, Lines.Next, Lines.Name, Lines.Next, Lines.Name, Lines.Parent, Lines.Name, Lines.Parent, Lines.Name - DSS.CompatFlags = 0 + DSS.ActiveCircuit.Settings.CompatFlags = 0 res_no_compat = Lines.First, Lines.Next, Lines.Name, Lines.Next, Lines.Name, Lines.Parent, Lines.Name, Lines.Parent, Lines.Name assert res_no_compat == (1, 2, '632670', 3, '670671', 2, '632670', 1, '650632') @@ -1050,6 +1049,22 @@ def test_busnames_ext(): assert tuple(CE._get_BusNames(True)) == ('sourcebus', '650') +def test_settings_context(): + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + + with DSS.ActiveCircuit.Settings.Context() as settings: + settings.AdvancedTypes = True + settings.PreferLists = True + assert isinstance(DSS.ActiveCircuit.LineLosses, complex) + assert isinstance(DSS.ActiveCircuit.AllBusVmag, list) + + assert not settings.AdvancedTypes + assert not settings.PreferLists + assert isinstance(DSS.ActiveCircuit.LineLosses, np.ndarray) + assert isinstance(DSS.ActiveCircuit.AllBusVmag, np.ndarray) + + + if __name__ == '__main__': DSS.AllowForms = False @@ -1061,4 +1076,5 @@ def test_busnames_ext(): # test_capacitor_reactor() test_loadshape_extended() test_xycurve_extended() - print('DONE!') \ No newline at end of file + print('DONE!') + diff --git a/tests/test_past_issues.py b/tests/test_past_issues.py index 4d7ef829..928f10a8 100644 --- a/tests/test_past_issues.py +++ b/tests/test_past_issues.py @@ -15,13 +15,13 @@ def setup_function(): DSS.ClearAll() DSS.AllowForms = False - DSS.AdvancedTypes = False - DSS.CompatFlags = 0 + DSS.ActiveCircuit.Settings.AdvancedTypes = False + DSS.ActiveCircuit.Settings.CompatFlags = 0 if not DSS._api_util._is_oddie: DSS.AllowEditor = False DSS.AllowChangeDir = True - DSS.COMErrorResults = False + DSS.ActiveCircuit.Settings.COMErrorResults = False DSS.Error.UseExceptions = True DSS.Text.Command = 'set DefaultBaseFreq=60' @@ -99,5 +99,5 @@ def test_ymatrix_csc(): DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.ActiveCircuit.Solution.Solve() - DSS.AdvancedTypes = True + DSS.ActiveCircuit.Settings.AdvancedTypes = True assert np.all(DSS.ActiveCircuit.SystemY == sp.csc_matrix(DSS.YMatrix.GetCompressedYMatrix())) From 320bdaef892cd6b84d156e1735d47d70949da521 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 10 Jan 2025 00:35:36 -0300 Subject: [PATCH 49/82] Tests: update reference values in `test_ctrlqueue.py` --- tests/test_ctrlqueue.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/test_ctrlqueue.py b/tests/test_ctrlqueue.py index b8a0a6fb..544f3a5b 100644 --- a/tests/test_ctrlqueue.py +++ b/tests/test_ctrlqueue.py @@ -80,14 +80,16 @@ def test_ctrlqueue(): # until all cap steps are on (no more available) i = 0 + + # Values updated for OpenDSS 10.1, which include the change to reset YPrimInvalid to false v_step_up = [ - 119.26520933058164, - 118.53391703558978, - 119.00110473442648, - 118.22480242913166, - 118.66765922692467, - 119.1133497058388, - 118.25980207026679, + 119.26520933058164, + 118.53391703558978, + 119.00162278912609, + 118.22495566279100, + 118.66307559565404, + 119.11533205526253, + 118.26023037859353, ] while DSSCapacitors.AvailableSteps > 0: print('DSSCapacitors.AvailableSteps', DSSCapacitors.AvailableSteps) @@ -127,16 +129,17 @@ def test_ctrlqueue(): # Print result print("Capacitor", DSSCapacitors.Name, "States =", tuple(DSSCapacitors.States)) - + + # Values updated for OpenDSS 10.1, which include the change to reset YPrimInvalid to false v_step_down = [ - 121.8764097324052, - 121.1919475267437, - 121.72822436752874, - 121.00188980140766, - 121.51826847553448, - 120.74704094567356, - 121.24330745091815, - 120.41556666716592, + 121.87640973217214, + 121.19194698692986, + 121.72771816928677, + 121.00166364015094, + 121.51952384526496, + 120.74621507897345, + 121.24285846717471, + 120.41741114839155, ] # Now let's reverse Direction and start removing steps From 030549a35b8bcdc8b73d0dd2b39924e689700e81 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:03:06 -0300 Subject: [PATCH 50/82] Tests: add test for LoadShape interpolation. --- tests/test_general.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_general.py b/tests/test_general.py index 0c54f88c..b448d683 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -943,6 +943,35 @@ def test_loadshape_extended(): LS.TimeArray *= 12 npt.assert_allclose(LS.TimeArray / 12, [1, 2, 7]) + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + DSS.Text.Command = 'new loadshape.test npts=3 pmult=[1.1, 2.2, 3.3] qmult=[4.5, 4.6, 4.7] hour=[1, 2, 7]' + DSS.Text.Command = 'BatchEdit Load..* Daily=test' + DSS.ActiveCircuit.Solution.Mode = SolveModes.Daily + DSS.ActiveCircuit.Solution.StepSize = 3600 + DSS.ActiveCircuit.Solution.Number = 1 + + avg_results_ref = [-3850.602050692013, -6933.281474723415, -7515.891810916165, -8086.495438035473, -8645.055975533884, -9190.98268197323, -9725.069704090525, -3850.634926011579] + avg_results = [] + for _ in range(8): + DSS.ActiveCircuit.Solution.Solve() + avg_results.append(DSS.ActiveCircuit.TotalPower[0]) + + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + DSS.Text.Command = 'new loadshape.test npts=3 pmult=[1.1, 2.2, 3.3] qmult=[4.5, 4.6, 4.7] hour=[1, 2, 7] interpolation=edge' + DSS.Text.Command = 'BatchEdit Load..* Daily=test' + DSS.ActiveCircuit.Solution.Mode = SolveModes.Daily + DSS.ActiveCircuit.Solution.StepSize = 3600 + DSS.ActiveCircuit.Solution.Number = 1 + + edge_results_ref = [-3850.602050692013, -6933.281474723415, -6933.300378973458, -6933.299600116585, -6933.2996080286175, -6933.29960833009, -9724.825136610381, -3850.6349396192672] + edge_results = [] + for _ in range(8): + DSS.ActiveCircuit.Solution.Solve() + edge_results.append(DSS.ActiveCircuit.TotalPower[0]) + + npt.assert_allclose(avg_results, avg_results_ref) + npt.assert_allclose(edge_results, edge_results_ref) + def test_xycurve_extended(): From 4d9e7c1dc1cba825d33b96c9d040b42cece25f9f Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:53:26 -0300 Subject: [PATCH 51/82] WIP, probably needs fixes --- dss/_cffi_api_util.py | 311 ++++++++++++++++-------------------------- 1 file changed, 117 insertions(+), 194 deletions(-) diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index f445a2f0..6f88111b 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -149,18 +149,18 @@ def get_fcomplex128_array(self, func, *args) -> Union[ComplexArray, None]: return res - def get_complex128_array2(self, func, *args) -> Float64ArrayOrComplexArray: - if not self.advanced_types: - return self.get_float64_array2(func, *args) + # def get_complex128_array2(self, func, *args) -> Float64ArrayOrComplexArray: + # if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + # return self.get_float64_array2(func, *args) - # Currently we use the same as API as get_float64_array, may change later - ptr = self._ffi.new('double**') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - ptr = self._ffi.cast('double _Complex **', ptr) - res = self._ffi.unpack(ptr[0], cnt[0] >> 1) - self.DSS_Dispose_PDouble(ptr) - return res + # # Currently we use the same as API as get_float64_array, may change later + # ptr = self._ffi.new('double**') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + # ptr = self._ffi.cast('double _Complex **', ptr) + # res = self._unpack(ptr[0], cnt[0] >> 1) + # self.DSS_Dispose_PDouble(ptr) + # return res def get_complex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: @@ -189,19 +189,19 @@ def get_fcomplex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: self.DSS_Dispose_PDouble(ptr) - def get_complex128_simple2(self, func, *args) -> List[Union[complex, float]]: - if not self.advanced_types: - return self.get_float64_array2(func, *args) + # def get_complex128_simple2(self, func, *args) -> List[Union[complex, float]]: + # if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + # return self.get_float64_array2(func, *args) - # Currently we use the same as API as get_float64_array, may change later - ptr = self._ffi.new('double**') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - try: - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return self._ffi.cast('double _Complex**', ptr)[0][0] - finally: - self.DSS_Dispose_PDouble(ptr) + # # Currently we use the same as API as get_float64_array, may change later + # ptr = self._ffi.new('double**') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + # try: + # assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) + # return self._ffi.cast('double _Complex**', ptr)[0][0] + # finally: + # self.DSS_Dispose_PDouble(ptr) def get_float64_gr_array(self) -> Float64Array: @@ -259,16 +259,6 @@ def get_fcomplex128_gr_simple(self) -> complex: return ptr[0][0] - def get_complex128_gr_simple2(self) -> List[Union[complex, float]]: - if not self.advanced_types: - return self.get_float64_gr_array2() - - # Currently we use the same as API as get_float64_array, may change later - ptr, cnt = self.gr_cfloat64_pointers - assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) - return ptr[0][0] - - def get_int32_array(self, func: Callable, *args) -> Int32Array: ptr = self._ffi.new('int32_t**') cnt = self._ffi.new('int32_t[4]') @@ -295,19 +285,19 @@ def get_int32_gr_array(self) -> Int32Array: return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() - def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: - ptr = self._ffi.new('int8_t**') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() - self.DSS_Dispose_PByte(ptr) + # def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: + # ptr = self._ffi.new('int8_t**') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + # res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() + # self.DSS_Dispose_PByte(ptr) - if cnt[3] and self.advanced_types: - # If the last element is filled, we have a matrix. Otherwise, the - # matrix feature is disabled or the result is indeed a vector - return res.reshape((cnt[2], cnt[3])) + # if cnt[3] and (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + # # If the last element is filled, we have a matrix. Otherwise, the + # # matrix feature is disabled or the result is indeed a vector + # return res.reshape((cnt[2], cnt[3])) - return res + # return res def get_int8_gr_array(self) -> Int8Array: @@ -321,100 +311,87 @@ def get_int8_gr_array(self) -> Int8Array: return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() - def get_string_array(self, func: Callable, *args: Any) -> List[str]: - ptr = self._ffi.new('char***') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = [] - else: - actual_ptr = ptr[0] - if actual_ptr == self._ffi.NULL: - res = [] - else: - codec = self.codec - str_ptrs = self._unpack(actual_ptr, cnt[0]) - #res = [(str(self._ffi.string(str_ptr).decode(codec)) if (str_ptr != self._ffi.NULL) else None) for str_ptr in str_ptrs] - res = [(self._ffi.string(str_ptr).decode(codec) if (str_ptr != self._ffi.NULL) else u'') for str_ptr in str_ptrs] - - self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) - return res - - - def get_string_array2(self, func, *args): # for compatibility with OpenDSSDirect.py - ptr = self._ffi.new('char***') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - - if not cnt[0]: - res = [] - else: - actual_ptr = ptr[0] - if actual_ptr == self._ffi.NULL: - res = [] - else: - codec = self.codec - res = [(str(self._ffi.string(actual_ptr[i]).decode(codec)) if (actual_ptr[i] != self._ffi.NULL) else '') for i in range(cnt[0])] - if res == [u'']: - # most COM methods return an empty array as an - # array with an empty string - res = [] - - if len(res) == 1 and res[0].lower() == 'none': - res = [] - - self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) - return res - - - def get_float64_array2(self, func, *args): - ptr = self._ffi.new('double**') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = [] - else: - res = self._ffi.unpack(ptr[0], cnt[0]) - - self.DSS_Dispose_PDouble(ptr) - return res - - def get_float64_gr_array2(self): - ptr, cnt = self.gr_float64_pointers - return self._ffi.unpack(ptr[0], cnt[0]) - - def get_int32_array2(self, func, *args): - ptr = self._ffi.new('int32_t**') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = None - else: - res = self._ffi.unpack(ptr[0], cnt[0]) - - self.DSS_Dispose_PInteger(ptr) - return res - - def get_int32_gr_array2(self): - ptr, cnt = self.gr_int32_pointers - return self._ffi.unpack(ptr[0], cnt[0]) - - def get_int8_array2(self, func, *args): - ptr = self._ffi.new('int8_t**') - cnt = self._ffi.new('int32_t[4]') - func(ptr, cnt, *args) - if not cnt[0]: - res = None - else: - res = self._ffi.unpack(ptr[0], cnt[0]) - - self.DSS_Dispose_PByte(ptr) - return res - - def get_int8_gr_array2(self): - ptr, cnt = self.gr_int8_pointers - return self._ffi.unpack(ptr[0], cnt[0]) - + # def get_string_array(self, func: Callable, *args: Any) -> List[str]: + # ptr = self._ffi.new('char***') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + # if not cnt[0]: + # res = [] + # else: + # actual_ptr = ptr[0] + # if actual_ptr == self._ffi.NULL: + # res = [] + # else: + # codec = self.codec + # str_ptrs = self._unpack(actual_ptr, cnt[0]) + # #res = [(str(self._ffi.string(str_ptr).decode(codec)) if (str_ptr != self._ffi.NULL) else None) for str_ptr in str_ptrs] + # res = [(self._ffi.string(str_ptr).decode(codec) if (str_ptr != self._ffi.NULL) else u'') for str_ptr in str_ptrs] + + # self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) + # return res + + + # def get_string_array2(self, func, *args): # for compatibility with OpenDSSDirect.py + # ptr = self._ffi.new('char***') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + + # if not cnt[0]: + # res = [] + # else: + # actual_ptr = ptr[0] + # if actual_ptr == self._ffi.NULL: + # res = [] + # else: + # codec = self.codec + # res = [(str(self._ffi.string(actual_ptr[i]).decode(codec)) if (actual_ptr[i] != self._ffi.NULL) else '') for i in range(cnt[0])] + # if res == [u'']: + # # most COM methods return an empty array as an + # # array with an empty string + # res = [] + + # if len(res) == 1 and res[0].lower() == 'none': + # res = [] + + # self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) + # return res + + + # def get_float64_array2(self, func, *args): + # ptr = self._ffi.new('double**') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + # if not cnt[0]: + # res = [] + # else: + # res = self._unpack(ptr[0], cnt[0]) + + # self.DSS_Dispose_PDouble(ptr) + # return res + + # def get_int32_array2(self, func, *args): + # ptr = self._ffi.new('int32_t**') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + # if not cnt[0]: + # res = None + # else: + # res = self._unpack(ptr[0], cnt[0]) + + # self.DSS_Dispose_PInteger(ptr) + # return res + + # def get_int8_array2(self, func, *args): + # ptr = self._ffi.new('int8_t**') + # cnt = self._ffi.new('int32_t[4]') + # func(ptr, cnt, *args) + # if not cnt[0]: + # res = None + # else: + # res = self._unpack(ptr[0], cnt[0]) + + # self.DSS_Dispose_PByte(ptr) + # return res def _get_strs_ctx(self, errorPtr, ctx, func: Callable, *args: Any) -> List[str]: ffi = self._ffi @@ -735,26 +712,10 @@ class Base: __slots__ = [ '_lib', '_api_util', - '_get_string', - '_get_float64_array', - '_get_float64_gr_array', - '_get_int32_array', - '_get_int32_gr_array', - '_get_int8_array', - '_get_int8_gr_array', - '_get_string_array', '_set_string_array', '_prepare_float64_array', '_prepare_int32_array', '_prepare_string_array', - '_get_complex128_array', - '_get_complex128_simple', - '_get_fcomplex128_simple', - '_get_complex128_gr_array', - '_get_complex128_gr_simple', - '_get_fcomplex128_gr_array', - '_get_fcomplex128_array', - '_get_fcomplex128_gr_simple', '_prepare_complex128_array', '_prepare_complex128_simple', '_errorPtr', @@ -764,48 +725,10 @@ class Base: using_exceptions = True _oddpy = False - def __init__(self, api_util, prefer_lists=False): + def __init__(self, api_util): object.__setattr__(self, '_frozen_attrs', False) self._api_util = api_util - self._get_string = api_util.get_string - - self._lib = api_util._get_lib(prefer_lists, self._oddpy) - lib = self._lib - - #TODO: remove all _get_* after AltDSS is fully migrated - self._get_fcomplex128_gr_array = lib.get_fcomplex128_gr_array - self._get_fcomplex128_array = lib.get_fcomplex128_array - self._get_fcomplex128_simple = lib.get_fcomplex128_simple - self._get_fcomplex128_gr_simple = lib.get_fcomplex128_gr_simple - - if not prefer_lists: - # Use NumPy arrays for most functions - self._get_float64_array = lib.get_float64_array - self._get_float64_gr_array = lib.get_float64_gr_array - self._get_int32_array = lib.get_int32_array - self._get_int32_gr_array = lib.get_int32_gr_array - self._get_int8_array = lib.get_int8_array - self._get_int8_gr_array = lib.get_int8_gr_array - self._get_string_array = lib.get_string_array - - self._get_complex128_array = lib.get_complex128_array - self._get_complex128_simple = lib.get_complex128_simple - self._get_complex128_gr_array = lib.get_complex128_gr_array - self._get_complex128_gr_simple = lib.get_complex128_gr_simple - else: - # Classic OpenDSSDirect.py style, using mostly lists - self._get_float64_array = lib.get_float64_array2 - self._get_float64_gr_array = lib.get_float64_gr_array2 - self._get_int32_array = lib.get_int32_array2 - self._get_int32_gr_array = lib.get_int32_gr_array2 - self._get_int8_array = lib.get_int8_array2 - self._get_int8_gr_array = lib.get_int8_gr_array2 - self._get_string_array = lib.get_string_array2 - - self._get_complex128_array = lib.get_complex128_array2 - self._get_complex128_simple = lib.get_complex128_simple2 - self._get_complex128_gr_array = lib.get_complex128_gr_array2 - self._get_complex128_gr_simple = lib.get_complex128_gr_simple2 + self._lib = api_util._get_lib(self._oddpy) self._prepare_complex128_array = api_util.prepare_complex128_array self._prepare_complex128_simple = api_util.prepare_complex128_simple From b8ef6c9b9b3344b78d6c9528a1a64a6998951272 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sun, 20 Apr 2025 00:08:03 -0300 Subject: [PATCH 52/82] Move AllowDOScmd, AllowChangeDir, AllowEditor to Settings; implement ShowPanel for Oddie; tweak some docstrings. --- dss/IDSS.py | 12 +++++++- dss/IGenerators.py | 1 + dss/IReactors.py | 2 +- dss/ISettings.py | 68 +++++++++++++++++++++++++++++++++++++++++++- dss/ITransformers.py | 2 +- 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/dss/IDSS.py b/dss/IDSS.py index 9b443bbc..428a9e39 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -331,8 +331,11 @@ def AllowEditor(self) -> bool: If you set to 0 (false), the editor is not executed. Note that other side effects, such as the creation of files, are not affected. + **Deprecated:** Use `Settings.AllowEditor` instead (same behavior, the setting was just moved there for better organization). + **(API Extension)** ''' + warnings.warn('"AllowEditor" was moved to the Settings interface. This property still works, but will be removed in a future release. Please use `...Settings.AllowEditor` instead.', DeprecationWarning, stacklevel=2) return self._lib.DSS_Get_AllowEditor() @AllowEditor.setter @@ -340,7 +343,8 @@ def AllowEditor(self, value: bool): self._lib.DSS_Set_AllowEditor(value) def ShowPanel(self): - pass + if api_util._is_oddie: + self._lib.Text_Set_Command('panel') def NewCircuit(self, name) -> ICircuit: ''' @@ -384,8 +388,11 @@ def AllowChangeDir(self) -> bool: This can also be set through the environment variable DSS_CAPI_ALLOW_CHANGE_DIR. Set it to 0 to disallow changing the active working directory. + **Deprecated:** Use `Settings.AllowChangeDir` instead (same behavior, the setting was just moved there for better organization). + **(API Extension)** ''' + warnings.warn('"AllowChangeDir" was moved to the Settings interface. This property still works, but will be removed in a future release. Please use `...Settings.AllowChangeDir` instead.', DeprecationWarning, stacklevel=2) return self._lib.DSS_Get_AllowChangeDir() @AllowChangeDir.setter @@ -402,8 +409,11 @@ def AllowDOScmd(self) -> bool: This can also be set through the environment variable DSS_CAPI_ALLOW_DOSCMD. Setting it to 1 enables the command. + **Deprecated:** Use `Settings.AllowDOScmd` instead (same behavior, the setting was just moved there for better organization). + **(API Extension)** ''' + warnings.warn('"AllowDOScmd" was moved to the Settings interface. This property still works, but will be removed in a future release. Please use `...Settings.AllowDOScmd` instead.', DeprecationWarning, stacklevel=2) return self._lib.DSS_Get_AllowDOScmd() @AllowDOScmd.setter diff --git a/dss/IGenerators.py b/dss/IGenerators.py index 350a1e8c..4c1098d6 100644 --- a/dss/IGenerators.py +++ b/dss/IGenerators.py @@ -267,6 +267,7 @@ def kva(self, Value: float): def Class(self) -> int: ''' An arbitrary integer number representing the class of Generator so that Generator values may be segregated by class. + No effect on the solution. **(API Extension)** ''' diff --git a/dss/IReactors.py b/dss/IReactors.py index 01ace2ec..4bb22839 100644 --- a/dss/IReactors.py +++ b/dss/IReactors.py @@ -132,7 +132,7 @@ def Bus1(self, Value: AnyStr): def Bus2(self) -> str: ''' Name of 2nd bus. Defaults to all phases connected to first bus, node 0, (Shunt Wye Connection) except when Bus2 is specifically defined. - Not necessary to specify for delta (LL) connection + Not necessary to specify for delta (LL) connection. **(API Extension)** ''' diff --git a/dss/ISettings.py b/dss/ISettings.py index 298cbbbf..aba56691 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -44,6 +44,11 @@ def __enter__(self): except: pass + try: + self._AllowDOScmd = self._settings.AllowDOScmd + except: + pass + return self._settings def __exit__(self, exc_type, exc_val, exc_tb): @@ -77,6 +82,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): except: pass + try: + self._settings.AllowDOScmd = self._AllowDOScmd + except: + pass class ISettings(Base): @@ -553,7 +562,7 @@ def COMErrorResults(self) -> bool: If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with the official OpenDSS COM interface. - For example, consider the function `Loads_Get_ZIPV`. If there is no active circuit or active load element: + For example, consider the property `Loads.ZIPV`. If there is no active circuit or active load element: - In the disabled state (COMErrorResults=False), the function will return "[]", an array with 0 elements. - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should @@ -571,3 +580,60 @@ def COMErrorResults(self) -> bool: @COMErrorResults.setter def COMErrorResults(self, Value: bool): self._lib.DSS_Set_COMErrorResults(Value) + + @property + def AllowDOScmd(self) -> bool: + ''' + If enabled, the `DOScmd` command is allowed. Otherwise, an error is reported if the user tries to use it. + + Defaults to False/0 (disabled state). Users should consider DOScmd deprecated on DSS-Extensions. + + This can also be set through the environment variable DSS_CAPI_ALLOW_DOSCMD. Setting it to 1 enables + the command. + + **(API Extension)** + ''' + return self._lib.DSS_Get_AllowDOScmd() + + @AllowDOScmd.setter + def AllowDOScmd(self, Value: bool): + self._lib.DSS_Set_AllowDOScmd(Value) + + @property + def AllowChangeDir(self) -> bool: + ''' + If disabled, the engine will not change the active working directory during execution. E.g. a "compile" + command will not "chdir" to the file path. + + If you have issues with long paths, enabling this might help in some scenarios. + + Defaults to True (allow changes, backwards compatible) in the 0.10.x versions of DSS C-API. + This might change to False in future versions. + + This can also be set through the environment variable DSS_CAPI_ALLOW_CHANGE_DIR. Set it to 0 to + disallow changing the active working directory. + + **(API Extension)** + ''' + return self._lib.DSS_Get_AllowChangeDir() + + @AllowChangeDir.setter + def AllowChangeDir(self, Value: bool): + self._lib.DSS_Set_AllowChangeDir(Value) + + @property + def AllowEditor(self) -> bool: + ''' + Gets/sets whether running the external editor for "Show" is allowed + + AllowEditor controls whether the external editor is used in commands like "Show". + If you set to 0 (false), the editor is not executed. Note that other side effects, + such as the creation of files, are not affected. + + **(API Extension)** + ''' + return self._lib.DSS_Get_AllowEditor() + + @AllowEditor.setter + def AllowEditor(self, value: bool): + self._lib.DSS_Set_AllowEditor(value) diff --git a/dss/ITransformers.py b/dss/ITransformers.py index 5ae5f9d2..90092ea4 100644 --- a/dss/ITransformers.py +++ b/dss/ITransformers.py @@ -309,7 +309,7 @@ def RdcOhms(self, Value: float): @property def LossesByType(self) -> Float64ArrayOrComplexArray: ''' - Complex array with the losses by type (total losses, load losses, no-load losses), in VA + Complex array with the losses by type (total losses, load losses, no-load losses), in VA, for the current active transformer **(API Extension)** ''' From fb4e3876f3d983217642c2c5f233d5382ccd630f Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 7 May 2025 01:16:06 -0300 Subject: [PATCH 53/82] CapControls: Mark as circuit element --- dss/ICapControls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dss/ICapControls.py b/dss/ICapControls.py index b712b1e1..879bb583 100644 --- a/dss/ICapControls.py +++ b/dss/ICapControls.py @@ -7,6 +7,7 @@ class ICapControls(Iterable): __slots__ = [] + _is_circuit_element = True _columns = [ 'Name', From b8fb66727c883fa7b98a8addc64b1d6994e64c7b Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 7 May 2025 01:19:12 -0300 Subject: [PATCH 54/82] Docs, docstrings and typing updates --- README.md | 8 +- docs/changelog.md | 44 +++++----- docs/conf.py | 2 +- .../UserModels/PyIndMach012/PyIndMach012.py | 4 +- docs/index.md | 8 +- dss/IActiveClass.py | 6 +- dss/IBus.py | 40 +++++---- dss/ICNData.py | 6 +- dss/ICapControls.py | 6 +- dss/ICapacitors.py | 6 +- dss/ICircuit.py | 30 +++---- dss/ICktElement.py | 84 ++++++++++++++----- dss/ICtrlQueue.py | 6 +- dss/IDSS.py | 30 ++++--- dss/IDSSElement.py | 6 +- dss/IDSSEvents.py | 10 +-- dss/IDSSProgress.py | 24 ++++-- dss/IDSSProperty.py | 6 +- dss/IDSS_Executive.py | 6 +- dss/IError.py | 8 +- dss/IFuses.py | 6 +- dss/IGICSources.py | 6 +- dss/IGenerators.py | 6 +- dss/IISources.py | 6 +- dss/ILineCodes.py | 20 ++--- dss/ILineGeometries.py | 14 ++-- dss/ILineSpacings.py | 6 +- dss/ILines.py | 40 +++++---- dss/ILoadShapes.py | 6 +- dss/ILoads.py | 12 +-- dss/IMeters.py | 8 +- dss/IMonitors.py | 8 +- dss/IPDElements.py | 30 +++++-- dss/IPVSystems.py | 6 +- dss/IParallel.py | 6 +- dss/IParser.py | 6 +- dss/IReactors.py | 32 +++---- dss/IReclosers.py | 8 +- dss/IReduceCkt.py | 6 +- dss/IRegControls.py | 6 +- dss/IRelays.py | 8 +- dss/ISensors.py | 6 +- dss/ISettings.py | 18 ++-- dss/ISolution.py | 6 +- dss/IStorages.py | 8 +- dss/ISwtControls.py | 6 +- dss/ITSData.py | 6 +- dss/IText.py | 6 +- dss/ITopology.py | 6 +- dss/ITransformers.py | 16 ++-- dss/IVsources.py | 6 +- dss/IWindGens.py | 6 +- dss/IWireData.py | 6 +- dss/IXYCurves.py | 10 +-- dss/IYMatrix.py | 6 +- dss/IZIP.py | 6 +- dss/Oddie.py | 2 +- dss/__init__.py | 2 +- dss/_cffi_api_util.py | 22 ++--- dss/_types.py | 16 +++- tests/save_outputs.py | 4 +- 61 files changed, 425 insertions(+), 325 deletions(-) diff --git a/README.md b/README.md index c677bb60..5eab4b68 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This package can be used as a companion to [OpenDSSDirect.py](http://github.com/ While we plan to add a lot more functionality into DSS-Python, the main goal of creating a COM-compatible API has been reached in 2018. If you find an unexpected missing feature, please report it! Currently missing features that will be implemented eventually are interactive features and diakoptics (planned for a future version). -This module mimics the COM structure (as exposed via `win32com` or `comtypes`) — see [The DSS instance](https://dss-extensions.org/DSS-Python/#the-dss-instance) as well as [OpenDSS COM/classic APIs](https://dss-extensions.org/classic_api.html) for some docs — effectively enabling multi-platform compatibility at Python level. Compared to other options, it provides easier migration from code that uses the official OpenDSS through COM. See also [OpenDSS: Python APIs](https://dss-extensions.org/python_apis.html). +This module mimics the COM structure (as exposed via `win32com` or `comtypes`) — see [The DSS instance](https://dss-extensions.org/DSS-Python/#the-dss-instance) as well as [OpenDSS COM/classic APIs](https://dss-extensions.org/classic_api.html) for some docs — effectively enabling multi-platform compatibility at Python level. Compared to other options, it provides easier migration from code that uses EPRI's OpenDSS through COM. See also [OpenDSS: Python APIs](https://dss-extensions.org/python_apis.html). Most of the COM documentation can be used as-is, but instead of returning tuples or lists, this module returns/accepts NumPy arrays for numeric data exchange, which is usually preferred by the users. By toggle `DSS.AdvancedTypes`, complex numbers and matrices (shaped arrays) are also used to provide a more modern experience. The module depends mostly on CFFI, NumPy, typing_extensions and, optionally, SciPy.Sparse for reading the sparse system admittance matrix. Pandas and matplotlib are optional dependencies [to enable plotting](https://github.com/dss-extensions/dss_python/blob/master/docs/examples/Plotting.ipynb) and other features. @@ -103,7 +103,7 @@ import win32com.client dss_engine = win32com.client.gencache.EnsureDispatch("OpenDSSEngine.DSS") ``` -or `comtypes` (incidentally, `comtypes` is usually faster than `win32com`, so we recommend it if you need the official OpenDSS COM module): +or `comtypes` (incidentally, `comtypes` is usually faster than `win32com`, so we recommend it if you need EPRI's OpenDSS COM module): ```python import comtypes.client @@ -132,7 +132,7 @@ for i in range(len(voltages) // 2): ## Testing -Since the DLL is built using the Free Pascal compiler, which is not officially supported by EPRI, the results are validated running sample networks provided in the official OpenDSS distribution. The only modifications are done directly by the script, removing interactive features and some other minor issues. Most of the sample files from the official OpenDSS repository are used for validation. +Since the DLL is built using the Free Pascal compiler, which is not officially supported by EPRI, the results are validated running sample networks provided in EPRI's OpenDSS distribution. The only modifications are done directly by the script, removing interactive features and some other minor issues. Most of the sample files from EPRI's OpenDSS repository are used for validation. The validation scripts is `tests/validation.py` and requires the same folder structure as the building process. You need `win32com` to run it on Windows. @@ -143,7 +143,7 @@ As of version 0.11, the full validation suite can be run on the three supported Besides bug fixes, the main functionality of this library is mostly done. Notable desirable features that may be implemented are: - More examples, especially for the extra features. There is a growing documentation hosted at [https://dss-extensions.org/Python/](https://dss-extensions.org/DSS-Python/) and [https://dss-extensions.org/docs.html](https://dss-extensions.org/docs.html); watch also https://github.com/dss-extensions/dss-extensions for more. -- Reports integrated in Python and interactive features on plots. Since most of the plot types from the official OpenDSS are optionally available since DSS-Python 0.14.2, advanced integration and interactive features are planned for a future feature. +- Reports integrated in Python and interactive features on plots. Since most of the plot types from EPRI's OpenDSS are optionally available since DSS-Python 0.14.2, advanced integration and interactive features are planned for a future feature. Expect news about these items by version 1.0. diff --git a/docs/changelog.md b/docs/changelog.md index 6e3a63a3..f14e3c7e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -24,7 +24,7 @@ Released on 2024-12-1x. - Although we do not expect users to explore this, the backend now allows loading external DSS C-API libraries in the new struct-style initialization. -- DSS-Extensions, including DSS-Python and OpenDSSDirect.py, now have good support for EPRI's official OpenDSS binaries, including the Delphi version (mainline) and the new OpenDSS-C. +- DSS-Extensions, including DSS-Python and OpenDSSDirect.py, now have good support for EPRI's OpenDSS binaries, including the Delphi version (mainline) and the new OpenDSS-C. - The integration started in 2023, after EPRI's Direct DLL API was updated to migrate from Delphi variants, moving closer to our own DSS C-API in some aspects. - Contributors from DSS-Extensions have collaborated with EPRI on the maintainance and general development of OpenDSS-C in 2024. A few extras from our AltDSS engine have been integrated into OpenDSS-C, still under testing by the time this document was updated. - The integration is done by wrapping EPRI's binaries with a very thin layer that exposes it with the DSS-Extensions API. Our compatibility layer is called Oddie (originally for OpenDSSDirect.DLL Interface Extender). Functions that are not or cannot be implemented return errors. @@ -85,7 +85,7 @@ Released on 2024-02-12. Released on 2024-02-09. -- Upgrade the backend to [**AltDSS/DSS C-API 0.14.0**](https://github.com/dss-extensions/dss_capi/releases/tag/0.14.0). **A lot** of changes there, please check the changelog. Includes many small bugfixes, improvements, and ports of a few changes from the official OpenDSS codebase, matching OpenDSS v9.8.0.1. +- Upgrade the backend to [**AltDSS/DSS C-API 0.14.0**](https://github.com/dss-extensions/dss_capi/releases/tag/0.14.0). **A lot** of changes there, please check the changelog. Includes many small bugfixes, improvements, and ports of a few changes from EPRI's OpenDSS codebase, matching OpenDSS v9.8.0.1. - Enums: - Move to DSS-Python-Backend to allow easier sharing among all Python packages from DSS-Extensions. @@ -172,7 +172,7 @@ Released on 2023-03-28. - Engine updated to [**DSS C-API 0.13.0**](https://github.com/dss-extensions/dss_capi/releases/tag/0.13.0), which is very compatible with OpenDSS 9.6.1.1 (plus some official SVN commits up to rev 3595, plus our own changes. - **New test suite,** which runs many more files and validates more of the API. We now use `pytest` for some more complex tests, while the numeric validation is done with the new `compare_outputs.py`. - **New `DSS.AdvancedTypes`:** toggle matrix shapes and complex numbers in many of the properties and functions of the API. This is disabled by default. -- **New `DSS.CompatFlags`:** to address some concerns about compatibility, we added a few toggles to toggle behavior that matches the official OpenDSS more closely. This flags will be refined in later releases. +- **New `DSS.CompatFlags`:** to address some concerns about compatibility, we added a few toggles to toggle behavior that matches EPRI's OpenDSS more closely. This flags will be refined in later releases. - Drop deprecated IR classes and a few undocumented functions. - Use **more enums** throughout the code, which helps both readability and documentation. Some enums were complemented. - **`DSS.AdvancedTypes`** toggle: enabling `AdvancedTypes` allows using **array/matrix shapes and complex numbers** as results from properties/functions in the API. @@ -193,25 +193,25 @@ Released on 2023-03-28. - Clean-up several files to ease the transition from Pascal to C++; more enum usage, remove redundant internal properties, rename some class members, etc. Some final steps still remain (that work is done in private branches). - Fixes a couple of minor memory leaks. -- Removed our old *Legacy Models* mechanism. Right now, the API functions still exist, but will have no effect when setting and will throw an error. For a future version, the functions will be removed. This toggle was introduced in 2020, some time after the removal of the legacy models in the official OpenDSS. We believe users had enough time to fully migrate and the extra maintenance burden is not justified anymore. +- Removed our old *Legacy Models* mechanism. Right now, the API functions still exist, but will have no effect when setting and will throw an error. For a future version, the functions will be removed. This toggle was introduced in 2020, some time after the removal of the legacy models in EPRI's OpenDSS. We believe users had enough time to fully migrate and the extra maintenance burden is not justified anymore. - Transition some deprecated and buggy properties to throw specific errors, instead of generic messages. Issue: https://github.com/dss-extensions/dss_capi/issues/118 - `Export` command: When the user provides a filename, use it as-is, otherwise could be an invalid path in case-sensitive file systems (e.g. Linux, most likely). - `Dump` and `Save` commands: in some cases, our internal "hybrid enums" were not being converted correctly for dumps. A few classes had incomplete dump implementations since v0.12.0; some strings needed to be escaped for correct output. - CtrlQueue: adjust string formatting of items; although this doesn't affect the numeric results, the strings from the queue had some truncated numbers. - Property system: For compatibility with the official version, allow autoresizing some arrays when given conflicting number of elements through the text interface or scripts. -- `Like` property: Although not recommended and [deprecated in the official OpenDSS](https://sourceforge.net/p/electricdss/discussion/861977/thread/8b59d21eb6/?limit=25#b57c/f668), the sequence of properties filled in the original copy is also copied. If you use `Like`, remember to check if the copy actually worked since some classes are known to not copy every property correctly. +- `Like` property: Although not recommended and [deprecated in EPRI's OpenDSS](https://sourceforge.net/p/electricdss/discussion/861977/thread/8b59d21eb6/?limit=25#b57c/f668), the sequence of properties filled in the original copy is also copied. If you use `Like`, remember to check if the copy actually worked since some classes are known to not copy every property correctly. - Plotting and UI: The engine side plotting callback system is now complete. There are fixes for `DaisyPlot` and `GeneralDataPlot`, especially multi-platform handling. Changed how some properties are exposed in the JSON interchange to the callbacks. Implement argument handling and callback dispatch for `DI_Plot`, `CompareCases` and `YearlyCurves`. - `New` commands: Fix potential issue with null pointers and duplicate names when `DuplicatesAllowed=False`. - EnergyMeter: Fix error message when the metered element is not a PDElement. - CIMXML export: Fix issues present since v0.12.0; reported in https://github.com/dss-extensions/OpenDSSDirect.py/issues/121 - Parser: properly error out when given excessive number of elements for matrices; implemented due to the report in https://github.com/dss-extensions/OpenDSSDirect.py/issues/122 -- Port most changes from the official OpenDSS up to SVN revision 3595 (OpenDSS v9.6.1.1 + a couple of CIMXML updates); check [OpenDSS v9.6.1.1 README.txt](https://sourceforge.net/p/electricdss/code/3595/tree/trunk/Version8/README.txt) for some complementary info to the list below. +- Port most changes from EPRI's OpenDSS up to SVN revision 3595 (OpenDSS v9.6.1.1 + a couple of CIMXML updates); check [OpenDSS v9.6.1.1 README.txt](https://sourceforge.net/p/electricdss/code/3595/tree/trunk/Version8/README.txt) for some complementary info to the list below. - Relay, UPFC, UPFCControl changes ported. - CIMXML exports: Various updates. - RegControl: More log and debug trace entries. - LoadMult: Set `SystemYChanged` when changing `LoadMult` **through a DSS script or DSS command** (doesn't affect `Solution_Set_LoadMult`) - Port PVSystem, Storage, InvControl, and StorageController changes, including the new grid-forming mode (GFM). For DSS-Extensions, we added a new class InvBasedPCE to avoid some redundancy and make things clearer. - - Port DynamicExp and related functionality. In our implementation, we also add a new class DynEqPCE to avoid some redundant code (could still be improved). the Generator and the new InvBasePCE derive from this new DynEqPCE. **Note**: the `DynamicEq` functionality from the upstream still seems incomplete and some things are not fully implemented or maybe buggy, so we only ported now to remove the burden of porting this down the line. If you find issues, feel free to report here on DSS-Extensions, but we recommended checking first with the official OpenDSS — if the issue is also found in the official version, prefer to report in the official OpenDSS forum first so everyone gets the fixes and our implementation doesn't diverge too much. + - Port DynamicExp and related functionality. In our implementation, we also add a new class DynEqPCE to avoid some redundant code (could still be improved). the Generator and the new InvBasePCE derive from this new DynEqPCE. **Note**: the `DynamicEq` functionality from the upstream still seems incomplete and some things are not fully implemented or maybe buggy, so we only ported now to remove the burden of porting this down the line. If you find issues, feel free to report here on DSS-Extensions, but we recommended checking first with EPRI's OpenDSS — if the issue is also found in the official version, prefer to report in EPRI's OpenDSS forum first so everyone gets the fixes and our implementation doesn't diverge too much. - CktElement/API: add a few new functions related to state variables. - Circuit, Line: port the `LongLineCorrection` flag now that it seems to be fixed upstream. Note that we didn't publish releases with the previous buggy version from the upstream OpenDSS (that applied the long-line correction for everything). - LineSpacing: port side-effect from upstream; changing `nconds` now reallocates and doesn't leak previously allocated memory. Not a common operation, so it's not very relevant. @@ -231,7 +231,7 @@ Released on 2023-03-28. - `Meters_Get_CountBranches`: reimplemented - `Monitors_Get_dblHour`: For harmonics solution, return empty array. Previously, it was returning a large array instead of a single element (`[0]`) array. A small issue adjusted for compatibility with the official COM API results. - `Reactors_Set_Bus1`: Match the side-effects of the property API for two-terminal reactors. - - New `DSS_Set_CompatFlags`/`DSS_Get_CompatFlags` function pair: introduced to address some current and potential future concerns about compatibility of results with the official OpenDSS. See the API docs for more info. + - New `DSS_Set_CompatFlags`/`DSS_Get_CompatFlags` function pair: introduced to address some current and potential future concerns about compatibility of results with EPRI's OpenDSS. See the API docs for more info. - New `DSS_Set_EnableArrayDimensions`/`DSS_Get_EnableArrayDimensions`: for Array results in the API, implement optional matrix sizes; when setting `DSS_Set_EnableArrayDimensions(true)`, the array size pointer will be filled with two extra elements to represent the matrix size (if the data is a matrix instead of a plain vector). For complex number, the dimensions are filled in relation to complex elements instead of double/float64 elements even though we currently reuse the double/float64 array interface. Issue: https://github.com/dss-extensions/dss_capi/issues/113 Note that a couple of SVN changes were ignored on purpose since they introduced potential issues, while many other changes and bug-fixes did not affect the DSS C-API version since our implementation is quite different in some places. @@ -257,12 +257,12 @@ Released on 2022-07-14. - New `ToJSON` functions to dump object properties (power flow state is not included at the moment) - Initial implementation of the new `DSS.Obj` API for direct DSS object and uniform batch manipulation, covering all DSS classes implemented in DSS C-API. The shape of this API may change for the next releases. At the moment it is intended for advanced users. For example, if you get an object handle from the engine and load a new circuit, the handle is invalid and you should not access it anymore (otherwise, crashes are expected). - Initial (work-in-progress) implementation of plotting functions. This will also be finished and polished in following releases. -- Due to some changes ported from the official OpenDSS since 0.10.7, some results may change, especially for circuits that used miles as length units. The same is observed across the official OpenDSS releases. +- Due to some changes ported from EPRI's OpenDSS since 0.10.7, some results may change, especially for circuits that used miles as length units. The same is observed across EPRI's OpenDSS releases. #### DSS C-API 0.12.0 changes -**Includes porting of most official OpenDSS features up to revision 3460.** Check the OpenDSS SVN commits for details. +**Includes porting of most features from EPRI's OpenDSS up to revision 3460.** Check the OpenDSS SVN commits for details. Since version 0.11 accumulated too many changes for too long (nearly 2 years), making it hard to keep two parallel but increasingly distinct codebases, version 0.12 is a stepping stone to the next big version (planned as 0.13) that will contain all of the 0.11 changes. As such, only some of the 0.11 features are included. The previous 0.10.8 changes are also included here. @@ -277,13 +277,13 @@ This version still maintains basic compatibility with the 0.10.x series of relea - Experimental callbacks for plotting and message output. Expect initial support in Python soon after DSS C-API v0.12 is released. - Introduce `AllowChangeDir` mechanism: defaults to enabled state for backwards compatibility. When disabled, the engine will not change the current working directory in any situation. This is exposed through a new pair of functions `DSS_Set_AllowChangeDir` and `DSS_Get_AllowChangeDir`, besides the environment variable `DSS_CAPI_ALLOW_CHANGE_DIR`. - New setting to toggle `DOScmd` command. Can be controlled through the environment variable `DSS_CAPI_ALLOW_DOSCMD` or functions `DSS_Get_AllowDOScmd`/`DSS_Set_AllowDOScmd`. -- Use `OutputDirectory` more. `OutputDirectory` is set to the current `DataPath` if `DataPath` is writable. If not, it's set to a general location (`%LOCALAPPDATA%/dss-extensions` and `/tmp/dss-extensions` since this release). This should make life easier for a user running files from a read-only location. Note that this is only an issue when running a `compile` command. If the user only uses `redirect` commands, the `DataPath` and `OutputDirectory` are left empty, meaning the files are written to the current working directory (CWD), which the user can control through the programming language driving DSS C-API. Note that the official OpenDSS COM behavior is different, since it loads the `DataPath` saved in the registry and modifies the CWD accordingly when OpenDSS is initialized. +- Use `OutputDirectory` more. `OutputDirectory` is set to the current `DataPath` if `DataPath` is writable. If not, it's set to a general location (`%LOCALAPPDATA%/dss-extensions` and `/tmp/dss-extensions` since this release). This should make life easier for a user running files from a read-only location. Note that this is only an issue when running a `compile` command. If the user only uses `redirect` commands, the `DataPath` and `OutputDirectory` are left empty, meaning the files are written to the current working directory (CWD), which the user can control through the programming language driving DSS C-API. Note that EPRI's OpenDSS COM behavior is different, since it loads the `DataPath` saved in the registry and modifies the CWD accordingly when OpenDSS is initialized. - File IO rewritten to drop deprecated Pascal functions and features. This removes some limitations related to long paths due to the legacy implementation being limited to 255 chars. - Reworked `TPowerTerminal` to achieve better memory layout. This makes simulations running `LoadsTerminalCheck=false` and `LoadsTerminalCheck=true` closer in performance, yet disabling the check is still faster. - Use `TFPHashList` where possible (replacing the custom, original THashList implementation from OpenDSS). - New LoadShape functions and internals: - - Port memory-mapped files from the official OpenDSS, used when `MemoryMapping=Yes` from a DSS script while creating a LoadShape object. + - Port memory-mapped files from EPRI's OpenDSS, used when `MemoryMapping=Yes` from a DSS script while creating a LoadShape object. - Release the `LoadShape_Set_Points` function, which can be used for faster LoadShape input, memory-mapping externally, shared memory, chunked input, etc. - Some new functions: @@ -291,7 +291,7 @@ This version still maintains basic compatibility with the 0.10.x series of relea - `Circuit_Get_ElementLosses` - `CktElement_Get_NodeRef` -- `DSS_Get_COMErrorResults`/`DSS_Set_COMErrorResults`: New compatibility setting for error/empty result. If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with the official OpenDSS COM interface. +- `DSS_Get_COMErrorResults`/`DSS_Set_COMErrorResults`: New compatibility setting for error/empty result. If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with EPRI's OpenDSS COM interface. For example, consider the function Loads_Get_ZIPV. If there is no active circuit or active load element: - In the disabled state (COMErrorResults=False), the function will return "[]", an array with 0 elements. @@ -303,7 +303,7 @@ This version still maintains basic compatibility with the 0.10.x series of relea the legacy/COM behavior. The value can be toggled through the API at any time. - Drop function aliases: previously deprecated function aliases (`LoadShapes_Set_Sinterval` and `LoadShapes_Get_sInterval`) were removed to simplify the build process. Use `LoadShapes_Set_SInterval` and `LoadShapes_Get_SInterval` instead. -- Monitor headers: From the official OpenDSS, since May 2021, the monitor binary stream doesn't include the header anymore. When porting the change to DSS-Extensions, we took the opportunity to rewrite the related code, simplifying it. As such, the implementation in DSS-Extensions deviates from the official one. Extra blank chars are not included, and fields should be more consistent. As a recommendation, if your code needs to be compatible with both implementations, trimming the fields should be enough. +- Monitor headers: From EPRI's OpenDSS, since May 2021, the monitor binary stream doesn't include the header anymore. When porting the change to DSS-Extensions, we took the opportunity to rewrite the related code, simplifying it. As such, the implementation in DSS-Extensions deviates from the official one. Extra blank chars are not included, and fields should be more consistent. As a recommendation, if your code needs to be compatible with both implementations, trimming the fields should be enough. - Error messages: most messages are now more specific and, if running a DSS script from files, include the file names and line numbers. - Spectrum: To reduce overhead during object edits, now required to exist before the object that uses it. This is consistent with most of the other types in OpenDSS. - New object and batch APIs for direct manipulation of DSS objects and batches of objects @@ -333,8 +333,8 @@ DSS C-API 0.10.7 changes: - Includes an important bug fix related to the `CapRadius` DSS property. If your DSS scripts included the pattern `GMRac=... rad=...` or `GMRac=... diam=...` (in this order and without specifying `CapRadius`), you should upgrade and re-evaluate the results. - This version should be fully API compatible with 0.10.3+. - A reference document listing the DSS commands and properties for all DSS elements is now available at https://github.com/dss-extensions/dss_capi/blob/0.10.x/docs/dss_properties.md -- New functions API ported from the official OpenDSS include: `Bus_Get_AllPCEatBus`, `Bus_Get_AllPDEatBus`, `CktElement_Get_TotalPowers`, `Meters_Get_ZonePCE`. -- The changes ported from the official OpenDSS include the following (check the repository for more details): +- New functions API ported from EPRI's OpenDSS include: `Bus_Get_AllPCEatBus`, `Bus_Get_AllPDEatBus`, `CktElement_Get_TotalPowers`, `Meters_Get_ZonePCE`. +- The changes ported from EPRI's OpenDSS include the following (check the repository for more details): - "Adds LineType property to LineCode and LineGeometry objects." - "Correcting bug found in storage device when operating in idling mode. It was preventing the solution of other test feeders (IEEE 9500)" - "Enabling fuel option for generator, fixing bug found in TotalPower command." @@ -358,7 +358,7 @@ Released on 2020-08-01. DSS C-API 0.10.6 changes: - This version should be fully API compatible with 0.10.3+. The behavior of some functions changed with the new extensions. Especially, empty strings are explicitly return as nulls instead of "\0". This conforms to the behavior already seen in arrays of strings. -- The binary releases now use Free Pascal 3.2.0. We observed the solution process is around 6% faster, and results are even closer to the official OpenDSS. +- The binary releases now use Free Pascal 3.2.0. We observed the solution process is around 6% faster, and results are even closer to EPRI's OpenDSS. - The releases now include both the optimized/default binary and a non-optimized/debug version. See the [Debugging](https://github.com/dss-extensions/dss_capi/blob/0.10.x/docs/debug.md) document for more. - Extended API validation and **Extended Errors** mechanism: - The whole API was reviewed to add basic checks for active circuit and element access. @@ -366,7 +366,7 @@ DSS C-API 0.10.6 changes: - The mechanism can be toggled by API functions `DSS_Set_ExtendedErrors` and `DSS_Get_ExtendedErrors`, or environment variable `DSS_CAPI_EXTENDED_ERRORS=0` to disable (defaults to enabled state). - New **Legacy Models** mechanism: - OpenDSS 9.0+ dropped the old `PVsystem`, `Storage`, `InvControl`, and `StorageController` models, replacing with the new versions previously known as `PVsystem2`, `Storage2`, `InvControl2` and `StorageController2`. - - The behavior and parameters from the new models are different — they are better, more complete and versatile models. Check the official OpenDSS docs and examples for further information. + - The behavior and parameters from the new models are different — they are better, more complete and versatile models. Check EPRI's OpenDSS docs and examples for further information. - The implementation of the new models in DSS C-API was validated successfully with all test cases available. As such, we mirror the decision to make them the default models. - As an extension, we implemented the Legacy Models option. By toggling it, a `clear` command will be issued and the alternative models will be loaded. This should allow users to migrate to the new version but, if something that used to work with the old models stopped working somehow, the user can toggle the old models. The idea is to keep reproducibility of results while we keep updating the engine and the API. - Since EPRI dropped/deprecated the old models, we might drop them too, in a future release. Please open an issue on GitHub or send a message if those old models are important to you. @@ -378,7 +378,7 @@ DSS C-API 0.10.6 changes: - Some bugs found in DSS C-API and also reported upstream (already fixed in SVN): - `CapRadius` DSS property: if the radius was initialized using `GMRac`, `CapRadius` was left uninitialized, resulting in invalid/NaN values. - `Sensors` API: some functions edited capacitors instead of sensors. -- Updated to the official OpenDSS revision 2903, corresponding to versions 9.0.0+. Changes include: +- Updated to EPRI's OpenDSS revision 2903, corresponding to versions 9.0.0+. Changes include: - ExportCIMXML: updated. - Relay: Fix in `GetPropertyValue`. - Line: In `DumpProperties` and `MakePosSequence`, the length is handled differently for lines with `LineGeometry` or `LineSpacing`. @@ -401,7 +401,7 @@ DSS C-API 0.10.5 changes: - Disable builds and distribution of v8-only variation — the extra/missing parallel-machine will be completely merged in a mixed (v7+v8) codebase in the coming months. - This version should be fully API compatible with 0.10.3+. - `Bus` and `CktElement` API functions reworked with some more checks. -- Updated up to revision 2837 of the official OpenDSS code: +- Updated up to revision 2837 of EPRI's OpenDSS code: - Ported changes from SVN (v7 and v8) into DSS C-API v7 variation (v8 was left untouched). - 4 new API level functions (`ActiveClass_Get_ActiveClassParent`, `PVSystems_Get_Pmpp`, `PVSystems_Set_Pmpp`, `PVSystems_Get_IrradianceNow`) - 4 new components: `PVsystem2`, `Storage2`, `InvControl2`, `StorageController2` — *added for early testing, no dedicated API functions yet*. At the moment, please consider them experimental features subject to change. @@ -423,7 +423,7 @@ Released on 2019-11-16. This is a maintenance release. DSS C-API 0.10.4 changes include: -- Updated up to revision 2761 of the official OpenDSS code. The changes affect at least the following components: CIMXML export, `Capacitor`, `InvControl`, `LineGeometry`, `PVsystem`, `StorageController`, `Storage`, `Vsource`, `VCCS`. +- Updated up to revision 2761 of EPRI's OpenDSS code. The changes affect at least the following components: CIMXML export, `Capacitor`, `InvControl`, `LineGeometry`, `PVsystem`, `StorageController`, `Storage`, `Vsource`, `VCCS`. - This version should be fully compatible with 0.10.3. - Fixes issue with long paths on Linux, potentially other platforms too. @@ -462,7 +462,7 @@ Released on 2019-02-28. Released on 2019-02-17. -This is a minor DSS-Python release that contains lots of changes from DSS C-API. See also the [changes from DSS C-API](https://github.com/dss-extensions/dss_capi/blob/ed2a6b322a5e102ba61c6565e5e0eb23247b9221/docs/changelog.md#version-0101) for details, including changes from the official OpenDSS code since 0.10.0. +This is a minor DSS-Python release that contains lots of changes from DSS C-API. See also the [changes from DSS C-API](https://github.com/dss-extensions/dss_capi/blob/ed2a6b322a5e102ba61c6565e5e0eb23247b9221/docs/changelog.md#version-0101) for details, including changes from EPRI's OpenDSS code since 0.10.0. DSS-Python has recently moved from https://github.com/PMeira/dss_python/ to the new https://dss-extensions.org/ and https://github.com/dss-extensions/dss_python/ diff --git a/docs/conf.py b/docs/conf.py index 28cef554..4b41cf3f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ import dss project = 'DSS-Python' -copyright = '2018-2024 Paulo Meira, Dheepak Krishnamurthy, DSS-Extensions contributors' +copyright = '2018-2025 Paulo Meira, Dheepak Krishnamurthy, DSS-Extensions contributors' author = 'Paulo Meira, Dheepak Krishnamurthy, DSS-Extensions contributors' version = dss.__version__ release = dss.__version__ diff --git a/docs/examples/UserModels/PyIndMach012/PyIndMach012.py b/docs/examples/UserModels/PyIndMach012/PyIndMach012.py index ee72464e..0cc28914 100644 --- a/docs/examples/UserModels/PyIndMach012/PyIndMach012.py +++ b/docs/examples/UserModels/PyIndMach012/PyIndMach012.py @@ -1,7 +1,7 @@ ''' A `dss_python` User-Model implementation of the IndMach012 Generator model from OpenDSS. -Based on the following files from the official OpenDSS source code: +Based on the following files from EPRI's OpenDSS source code: - Source/PCElements/IndMach012.pas - Source/IndMach012a/IndMach012Model.pas @@ -9,7 +9,7 @@ Original code by EPRI, licensed under the 3-clause BSD. See OPENDSS_LICENSE. This sample code doesn't interact with the main OpenDSS interface directly, -it only uses the user-model interface. Thus, it is compatible with the official OpenDSS +it only uses the user-model interface. Thus, it is compatible with EPRI's OpenDSS distribution as well as DSS-Python. Note that OpenDSS version 7 has a bug on 64-bit systems and user-models most likely won't run via COM. diff --git a/docs/index.md b/docs/index.md index 63df480e..fb69bd48 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,7 +48,7 @@ In this documentation, since many features from DSS-Python are not available in Independent of which OpenDSS implementation you use, it is good practice to list the specific implementation begin used (e.g. from EPRI or from DSS-Extensions). At the moment we do not generate DOIs for our packages, but users can always cite a specific version on PyPI, e.g. https://pypi.org/project/dss-python/0.12.1/ -Check http://dss-extensions.org and https://github.com/dss-extensions/dss-extensions for links to more documentation and examples. Besides our own documentation, the official OpenDSS documentation is extensive and covers various topics. +Check http://dss-extensions.org and https://github.com/dss-extensions/dss-extensions for links to more documentation and examples. Besides our own documentation, EPRI's OpenDSS documentation is extensive and covers various topics. We recommend looking especially in the following resources: @@ -83,7 +83,7 @@ While OpenDSS relies on windows/forms to report errors, or require the user to c Although there are many classes and modules in DSS-Python, the main usage is typically through the default DSS instance, and that is the most interest aspect for most users. -DSS-Python tries to be a drop-in replacement for the official OpenDSS COM implementation, within reasonable limits. +DSS-Python tries to be a drop-in replacement for EPRI's OpenDSS COM implementation, within reasonable limits. There are two main Python packages that allow instantiating COM objects, `win32com` and `comtypes`. For a quick look into some Python APIs (COM, DSS-Python, OpenDSSDirect.py) for the OpenDSS (official or our alternative @@ -110,7 +110,7 @@ or with `comtypes`: DSS = comtypes.client.CreateObject("OpenDSSEngine.DSS") ``` -Either way, to use DSS-Python and effectively migrate from the official OpenDSS COM interface, you can replace that fragment with: +Either way, to use DSS-Python and effectively migrate from EPRI's OpenDSS COM interface, you can replace that fragment with: ```python from dss import DSS @@ -189,7 +189,7 @@ To enable: ``` After that, running the plot commands from the text interface or compile/redirect scripts will try to use matplotlib to -reproduce most of the plot options from the official OpenDSS. +reproduce most of the plot options from EPRI's OpenDSS. ```python dss.Text.Command = 'compile some_circuit/Master.dss' diff --git a/dss/IActiveClass.py b/dss/IActiveClass.py index 879bb785..82900f32 100644 --- a/dss/IActiveClass.py +++ b/dss/IActiveClass.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from __future__ import annotations from ._cffi_api_util import Base from .enums import DSSJSONFlags diff --git a/dss/IBus.py b/dss/IBus.py index cb35372a..202e1bce 100644 --- a/dss/IBus.py +++ b/dss/IBus.py @@ -1,9 +1,9 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from __future__ import annotations from ._cffi_api_util import Base -from ._types import Float64Array, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex, Int32Array +from ._types import Float64Array, ComplexArray, Complex, Int32Array from typing import List, Union, Iterator, Optional, TYPE_CHECKING if TYPE_CHECKING: @@ -79,7 +79,7 @@ def Coorddefined(self) -> bool: return self._lib.Bus_Get_Coorddefined() @property - def CplxSeqVoltages(self) -> Float64ArrayOrComplexArray: + def CplxSeqVoltages(self) -> ComplexArray: ''' Complex array of Sequence Voltages (0, 1, 2) at this Bus. @@ -132,7 +132,7 @@ def Int_Duration(self) -> float: return self._lib.Bus_Get_Int_Duration() @property - def Isc(self) -> Float64ArrayOrComplexArray: + def Isc(self) -> ComplexArray: ''' Short circuit currents at bus; Complex Array. @@ -178,7 +178,7 @@ def N_interrupts(self) -> float: @property def Name(self) -> str: ''' - Name of Bus + Name of the active Bus Original COM help: https://opendss.epri.com/Name1.html ''' @@ -234,7 +234,7 @@ def TotalMiles(self) -> float: return self._lib.Bus_Get_TotalMiles() @property - def VLL(self) -> Float64ArrayOrComplexArray: + def VLL(self) -> ComplexArray: ''' For 2- and 3-phase buses, returns array of complex numbers representing L-L voltages in volts. Returns -1.0 for 1-phase bus. If more than 3 phases, returns only first 3. @@ -252,7 +252,7 @@ def VMagAngle(self) -> Float64Array: return self._lib.Bus_Get_VMagAngle_GR() @property - def Voc(self) -> Float64ArrayOrComplexArray: + def Voc(self) -> ComplexArray: ''' Open circuit voltage; Complex array. @@ -263,7 +263,7 @@ def Voc(self) -> Float64ArrayOrComplexArray: return self._lib.Bus_Get_Voc_GR() @property - def Voltages(self) -> Float64ArrayOrComplexArray: + def Voltages(self) -> ComplexArray: ''' Complex array of voltages at this bus. @@ -272,7 +272,7 @@ def Voltages(self) -> Float64ArrayOrComplexArray: return self._lib.Bus_Get_Voltages_GR() @property - def YscMatrix(self) -> Float64ArrayOrComplexArray: + def YscMatrix(self) -> ComplexArray: ''' Complex array of Ysc matrix at bus. Column by column. @@ -283,7 +283,7 @@ def YscMatrix(self) -> Float64ArrayOrComplexArray: return self._lib.Bus_Get_YscMatrix_GR() @property - def Zsc0(self) -> Float64ArrayOrSimpleComplex: + def Zsc0(self) -> Complex: ''' Complex Zero-Sequence short circuit impedance at bus. @@ -294,7 +294,7 @@ def Zsc0(self) -> Float64ArrayOrSimpleComplex: return self._lib.Bus_Get_Zsc0_GR() @property - def Zsc1(self) -> Float64ArrayOrSimpleComplex: + def Zsc1(self) -> Complex: ''' Complex Positive-Sequence short circuit impedance at bus. @@ -305,7 +305,7 @@ def Zsc1(self) -> Float64ArrayOrSimpleComplex: return self._lib.Bus_Get_Zsc1_GR() @property - def ZscMatrix(self) -> Float64ArrayOrComplexArray: + def ZscMatrix(self) -> ComplexArray: ''' Complex array of Zsc matrix at bus. Column by column. @@ -325,7 +325,7 @@ def kVBase(self) -> float: return self._lib.Bus_Get_kVBase() @property - def puVLL(self) -> Float64ArrayOrComplexArray: + def puVLL(self) -> ComplexArray: ''' Returns Complex array of pu L-L voltages for 2- and 3-phase buses. Returns -1.0 for 1-phase bus. If more than 3 phases, returns only 3 phases. @@ -343,7 +343,7 @@ def puVmagAngle(self) -> Float64Array: return self._lib.Bus_Get_puVmagAngle_GR() @property - def puVoltages(self) -> Float64ArrayOrComplexArray: + def puVoltages(self) -> ComplexArray: ''' Complex Array of pu voltages at the bus. @@ -352,9 +352,9 @@ def puVoltages(self) -> Float64ArrayOrComplexArray: return self._lib.Bus_Get_puVoltages_GR() @property - def ZSC012Matrix(self) -> Float64ArrayOrComplexArray: + def ZSC012Matrix(self) -> ComplexArray: ''' - Array of doubles (complex) containing the complete 012 Zsc matrix. + Complex array containing the complete 012 Zsc matrix. Only available after Zsc is computed, either through the "ZscRefresh" command, or running a "FaultStudy" solution. Only available for buses with 3 nodes. @@ -413,6 +413,8 @@ def AllPCEatBus(self) -> List[str]: ''' Returns an array with the names of all PCE connected to the active bus + This also includes shunt Capacitors/Reactors. + Original COM help: https://opendss.epri.com/AllPCEatBus.html ''' result = self._lib.Bus_Get_AllPCEatBus() @@ -428,6 +430,8 @@ def AllPDEatBus(self) -> List[str]: ''' Returns an array with the names of all PDE connected to the active bus + This excludes shunt Capacitors/Reactors. + Original COM help: https://opendss.epri.com/AllPDEatBus1.html ''' result = self._lib.Bus_Get_AllPDEatBus() diff --git a/dss/ICNData.py b/dss/ICNData.py index fbf32c11..e435e228 100644 --- a/dss/ICNData.py +++ b/dss/ICNData.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import Union from .enums import LineUnits diff --git a/dss/ICapControls.py b/dss/ICapControls.py index 879bb583..68db4c48 100644 --- a/dss/ICapControls.py +++ b/dss/ICapControls.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import AnyStr, Union from .enums import CapControlModes diff --git a/dss/ICapacitors.py b/dss/ICapacitors.py index c58f8a3e..c47dff34 100644 --- a/dss/ICapacitors.py +++ b/dss/ICapacitors.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Int32Array diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 63cbb906..4a6fe1fa 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from typing import List, AnyStr, Union import json from ._cffi_api_util import Base @@ -46,7 +46,7 @@ from .IGICSources import IGICSources from .IWindGens import IWindGens -from ._types import Float64Array, Int32Array, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex +from ._types import Float64Array, Int32Array, ComplexArray, ComplexMatrix, Complex from .enums import DSSJSONFlags, DSSSaveFlags class ICircuit(Base): @@ -440,7 +440,7 @@ def AllBusVmagPu(self) -> Float64Array: return self._lib.Circuit_Get_AllBusVmagPu_GR() @property - def AllBusVolts(self) -> Float64ArrayOrComplexArray: + def AllBusVolts(self) -> ComplexArray: ''' Complex array of all bus, node voltages from most recent solution @@ -449,7 +449,7 @@ def AllBusVolts(self) -> Float64ArrayOrComplexArray: return self._lib.Circuit_Get_AllBusVolts_GR() @property - def AllElementLosses(self) -> Float64ArrayOrComplexArray: + def AllElementLosses(self) -> ComplexArray: ''' Array of total losses (complex) in each circuit element @@ -485,7 +485,7 @@ def AllNodeNames(self) -> List[str]: return self._lib.Circuit_Get_AllNodeNames() @property - def LineLosses(self) -> Float64ArrayOrSimpleComplex: + def LineLosses(self) -> Complex: ''' Complex total line losses in the circuit @@ -494,7 +494,7 @@ def LineLosses(self) -> Float64ArrayOrSimpleComplex: return self._lib.Circuit_Get_LineLosses_GR() @property - def Losses(self) -> Float64ArrayOrSimpleComplex: + def Losses(self) -> Complex: ''' Total losses in active circuit, complex number (two-element array of double). @@ -544,7 +544,7 @@ def ParentPDElement(self) -> int: return self._lib.Circuit_Get_ParentPDElement() @property - def SubstationLosses(self) -> Float64ArrayOrSimpleComplex: + def SubstationLosses(self) -> Complex: ''' Complex losses in all transformers designated to substations. @@ -553,7 +553,7 @@ def SubstationLosses(self) -> Float64ArrayOrSimpleComplex: return self._lib.Circuit_Get_SubstationLosses_GR() @property - def SystemY(self) -> Float64ArrayOrComplexArray: + def SystemY(self) -> ComplexMatrix: ''' (read-only) System Y matrix (after a solution has been performed). This is deprecated as it returns a dense matrix. Only use it for small systems. @@ -564,7 +564,7 @@ def SystemY(self) -> Float64ArrayOrComplexArray: return self._lib.Circuit_Get_SystemY_GR() @property - def TotalPower(self) -> Float64ArrayOrSimpleComplex: + def TotalPower(self) -> Complex: ''' Total power (complex), kVA delivered to the circuit @@ -573,7 +573,7 @@ def TotalPower(self) -> Float64ArrayOrSimpleComplex: return self._lib.Circuit_Get_TotalPower_GR() @property - def YCurrents(self) -> Float64ArrayOrComplexArray: + def YCurrents(self) -> ComplexArray: ''' Array of doubles containing complex injection currents for the present solution. It is the "I" vector of I=YV @@ -591,7 +591,7 @@ def YNodeOrder(self) -> List[str]: return self._lib.Circuit_Get_YNodeOrder() @property - def YNodeVarray(self) -> Float64ArrayOrComplexArray: + def YNodeVarray(self) -> ComplexArray: ''' Complex array of actual node voltages in same order as SystemY matrix. @@ -599,7 +599,7 @@ def YNodeVarray(self) -> Float64ArrayOrComplexArray: ''' return self._lib.Circuit_Get_YNodeVarray_GR() - def ElementLosses(self, Value: Int32Array) -> Float64ArrayOrComplexArray: + def ElementLosses(self, Value: Int32Array) -> ComplexArray: ''' Array of total losses (complex) in a selection of elements. Use the element indices (starting at 1) as parameter. @@ -661,7 +661,7 @@ def Save(self, dirOrFilePath: AnyStr, saveFlags: Union[DSSSaveFlags, List[DSSSav - `IsOpen`: Export commands to open terminals of elements. - `ToString`: to the result string. Requires "SingleFile" flag. - If `SingleFile` is enabled, the first argument (`dirOrFilePath`) is the file path, + If `SingleFile` is enabled, the path argument (`dirOrFilePath`) is the file path, otherwise it is the folder path. For string output, the argument is not used. **(API Extension)** diff --git a/dss/ICktElement.py b/dss/ICktElement.py index 295fc317..fcca200f 100644 --- a/dss/ICktElement.py +++ b/dss/ICktElement.py @@ -1,14 +1,27 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from __future__ import annotations from ._cffi_api_util import Base from .IDSSProperty import IDSSProperty -from ._types import Float64Array, Int32Array, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex +from ._types import Float64Array, Int32Array, Int32Matrix, ComplexArray, Complex from typing import List, AnyStr, Tuple, Iterator from .enums import OCPDevType as OCPDevTypeEnum class ICktElement(Base): + ''' + The (Active)CktElement interface allows accessing some common properties and + methods shared across circuit elements in the DSS engine. + + Users can enable specific elements by name or use the dedicated interface + (e.g. use `Loads.Name`, `Transformers.First/Next`) and access the properties here. + + If you are new to OpenDSS/AltDSS and this classic interface, please read the following document + for an overview of the "active element" paradigm used by COM and the classic APIs: + + https://dss-extensions.org/classic_api.html#the-active-paradigm + ''' + __slots__ = [ 'Properties' ] @@ -120,7 +133,16 @@ def setVariableByName(self, Idx: AnyStr, Value: float) -> int: # raise DSSException('Invalid variable index or not a PCelement') return Code[0] - def IsOpen(self, Term: int, Phs: int) -> bool: + def IsOpen(self, Term: int, Phs: int = 0) -> bool: + ''' + Indicates if the specified terminal and, optionally, a specific phase conductor is open. + + Provide zero in the `Phs` argument to check if any conductor of the terminal `Term` is open. + + Provide a non-zero phase number in `Phs` to check if a specific phase conductor is open. + + Original COM help: https://opendss.epri.com/IsOpen.html + ''' return self._lib.CktElement_IsOpen(Term, Phs) def Open(self, Term: int, Phs: int): @@ -185,7 +207,7 @@ def _set_BusNames(self, Value: List[AnyStr]): ''' @property - def CplxSeqCurrents(self) -> Float64ArrayOrComplexArray: + def CplxSeqCurrents(self) -> ComplexMatrix: ''' Complex double array of Sequence Currents for all conductors of all terminals of active circuit element. @@ -194,7 +216,7 @@ def CplxSeqCurrents(self) -> Float64ArrayOrComplexArray: return self._lib.CktElement_Get_CplxSeqCurrents_GR() @property - def CplxSeqVoltages(self) -> Float64ArrayOrComplexArray: + def CplxSeqVoltages(self) -> ComplexMatrix: ''' Complex double array of Sequence Voltage for all terminals of active circuit element. @@ -203,7 +225,7 @@ def CplxSeqVoltages(self) -> Float64ArrayOrComplexArray: return self._lib.CktElement_Get_CplxSeqVoltages_GR() @property - def Currents(self) -> Float64ArrayOrComplexArray: + def Currents(self) -> ComplexMatrix: ''' Complex array of currents into each conductor of each terminal @@ -212,9 +234,9 @@ def Currents(self) -> Float64ArrayOrComplexArray: return self._lib.CktElement_Get_Currents_GR() @property - def CurrentsMagAng(self) -> Float64Array: + def CurrentsMagAng(self) -> Float64Matrix: ''' - Currents in magnitude, angle (degrees) format as a array of doubles. + Currents in magnitude, angle (degrees) format as an array of doubles. Original COM help: https://opendss.epri.com/CurrentsMagAng.html ''' @@ -316,7 +338,7 @@ def HasVoltControl(self) -> bool: return self._lib.CktElement_Get_HasVoltControl() @property - def Losses(self) -> Float64ArrayOrSimpleComplex: + def Losses(self) -> Complex: ''' Total losses in the element: two-element double array (complex), in VA (watts, vars) @@ -324,6 +346,15 @@ def Losses(self) -> Float64ArrayOrSimpleComplex: ''' return self._lib.CktElement_Get_Losses_GR() + @property + def AllLosses(self) -> Complex: + ''' + Complex array with the losses by type (total losses, load losses, no-load losses), in VA, for the active circuit element. + + Added in May 2025. Same as `LossesByType` introduced for Transformers in AltDSS/DSS C-API in May 2019. + ''' + return self._lib.CktElement_Get_AllLosses_GR() + @property def Name(self) -> str: ''' @@ -334,7 +365,7 @@ def Name(self) -> str: return self._lib.CktElement_Get_Name() @property - def NodeOrder(self) -> Int32Array: + def NodeOrder(self) -> Int32Matrix: ''' Array of integer containing the node numbers (representing phases, for example) for each conductor of each terminal. @@ -422,7 +453,7 @@ def OCPDevType(self) -> OCPDevTypeEnum: return OCPDevTypeEnum(self._lib.CktElement_Get_OCPDevType()) @property - def PhaseLosses(self) -> Float64ArrayOrComplexArray: + def PhaseLosses(self) -> ComplexArray: ''' Complex array of losses (kVA) by phase @@ -431,7 +462,7 @@ def PhaseLosses(self) -> Float64ArrayOrComplexArray: return self._lib.CktElement_Get_PhaseLosses_GR() @property - def Powers(self) -> Float64ArrayOrComplexArray: + def Powers(self) -> ComplexArray: ''' Complex array of powers (kVA) into each conductor of each terminal @@ -440,7 +471,7 @@ def Powers(self) -> Float64ArrayOrComplexArray: return self._lib.CktElement_Get_Powers_GR() @property - def Residuals(self) -> Float64Array: + def Residuals(self) -> Float64Matrix: ''' Residual currents for each terminal: (magnitude, angle in degrees) @@ -449,7 +480,7 @@ def Residuals(self) -> Float64Array: return self._lib.CktElement_Get_Residuals_GR() @property - def SeqCurrents(self) -> Float64Array: + def SeqCurrents(self) -> Float64Matrix: ''' Double array of symmetrical component currents (magnitudes only) into each 3-phase terminal @@ -458,7 +489,7 @@ def SeqCurrents(self) -> Float64Array: return self._lib.CktElement_Get_SeqCurrents_GR() @property - def SeqPowers(self) -> Float64ArrayOrComplexArray: + def SeqPowers(self) -> ComplexMatrix: ''' Complex array of sequence powers (kW, kvar) into each 3-phase terminal @@ -467,7 +498,7 @@ def SeqPowers(self) -> Float64ArrayOrComplexArray: return self._lib.CktElement_Get_SeqPowers_GR() @property - def SeqVoltages(self) -> Float64Array: + def SeqVoltages(self) -> Float64Matrix: ''' Double array of symmetrical component voltages (magnitudes only) at each 3-phase terminal @@ -476,7 +507,7 @@ def SeqVoltages(self) -> Float64Array: return self._lib.CktElement_Get_SeqVoltages_GR() @property - def Voltages(self) -> Float64ArrayOrComplexArray: + def Voltages(self) -> ComplexMatrix: ''' Complex array of voltages at terminals @@ -485,7 +516,7 @@ def Voltages(self) -> Float64ArrayOrComplexArray: return self._lib.CktElement_Get_Voltages_GR() @property - def VoltagesMagAng(self) -> Float64Array: + def VoltagesMagAng(self) -> Float64Matrix: ''' Voltages at each conductor in magnitude, angle form as array of doubles. @@ -494,7 +525,7 @@ def VoltagesMagAng(self) -> Float64Array: return self._lib.CktElement_Get_VoltagesMagAng_GR() @property - def Yprim(self) -> Float64ArrayOrComplexArray: + def Yprim(self) -> ComplexMatrix: ''' YPrim matrix, column order, complex numbers @@ -502,6 +533,15 @@ def Yprim(self) -> Float64ArrayOrComplexArray: ''' return self._lib.CktElement_Get_Yprim_GR() + @property + def YprimOrder(self) -> int: + ''' + Order (size) of the active circuit element's primite Y matrix (Yprim), typically `NumConductors * NumTerminals` + + **(API Extension)** + ''' + return self._lib.CktElement_Get_YprimOrder() + @property def IsIsolated(self) -> bool: ''' @@ -513,7 +553,7 @@ def IsIsolated(self) -> bool: return self._lib.CktElement_Get_IsIsolated() @property - def TotalPowers(self) -> Float64ArrayOrComplexArray: + def TotalPowers(self) -> ComplexArray: ''' Returns an array with the total powers (complex, kVA) at ALL terminals of the active circuit element. diff --git a/dss/ICtrlQueue.py b/dss/ICtrlQueue.py index b20b6dec..f3f8992b 100644 --- a/dss/ICtrlQueue.py +++ b/dss/ICtrlQueue.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from typing import List diff --git a/dss/IDSS.py b/dss/IDSS.py index 428a9e39..04c4c04b 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from __future__ import annotations import warnings from weakref import WeakKeyDictionary @@ -191,7 +191,7 @@ def to_opendssdirect(self) -> OpenDSSDirect: def is_oddie(self) -> bool: """ Returns True if this instance is based on the Oddie compatibility layer for - the official OpenDSS Direct API (a.k.a. DCSL). + EPRI's OpenDSS Direct API (a.k.a. DCSL). Note that the default engine in DSS-Python has been based on AltDSS since 2018, even though it was not called AltDSS then. @@ -220,7 +220,7 @@ def Start(self, code: int) -> bool: handled automatically, so the users do not need to call it manually, unless using AltDSS/DSS C-API directly without further tools. - On the official OpenDSS, `Start` also does nothing at all in the current + On EPRI's OpenDSS, `Start` also does nothing at all in the current Delphi versions. It is required for OpenDSS-C, but also handled behind the scenes on DSS-Extensions. @@ -312,7 +312,9 @@ def Version(self) -> str: @property def AllowForms(self) -> bool: ''' - Gets/sets whether text output is allowed (DSS-Extensions) or general forms/windows are shown (official OpenDSS). + Indicates whether text output is allowed or forms are used. Disable to silence most output. + + Currently, forms/windows are only used for EPRI's OpenDSS distribution on Windows. Original COM help: https://opendss.epri.com/AllowForms.html ''' @@ -361,7 +363,7 @@ def LegacyModels(self) -> bool: ''' LegacyModels was a flag used to toggle legacy (pre-2019) models for PVSystem, InvControl, Storage and StorageControl. - In the official OpenDSS version 9.0, the old models were removed. They were temporarily present here + In EPRI's OpenDSS version 9.0, the old models were removed. They were temporarily present here but were also removed in DSS C-API v0.13.0. **NOTE**: this property will be removed for v1.0. It is left to avoid breaking the current API too soon. @@ -423,8 +425,8 @@ def AllowDOScmd(self, Value: bool): @property def COMErrorResults(self) -> bool: ''' - If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with the - official OpenDSS COM interface. + If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with + EPRI's OpenDSS COM interface. For example, consider the function `Loads_Get_ZIPV`. If there is no active circuit or active load element: @@ -432,9 +434,11 @@ def COMErrorResults(self) -> bool: - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should be compatible with the return value of the official COM interface. - Defaults to False/0 (disabled state), starting DSS-Python v0.16. + Defaults to false (disabled state) in AltDSS since the v0.15.x series. + + This does not affect the results when using EPRI's OpenDSS distribution through Oddie. - This can also be set through the environment variable `DSS_CAPI_COM_DEFAULTS`. Setting it to 0 disables + This can also be set through the environment variable `DSS_CAPI_COM_DEFAULTS`. Setting it to 1 enables the legacy/COM behavior. The value can be toggled through the API at any time. **Deprecated:** Use `Settings.COMErrorResults` instead (same behavior, the setting was just moved there for better organization). @@ -461,7 +465,7 @@ def NewContext(self) -> IDSS: ''' if self._api_util._is_oddie: - raise NotImplementedError("NewContext is not supported for the official OpenDSS engine.") + raise NotImplementedError("NewContext is not supported for the EPRI's OpenDSS engines.") ffi = self._api_util.ffi lib = self._api_util.lib_unpatched @@ -544,7 +548,7 @@ def AdvancedTypes(self, Value: bool): @property def CompatFlags(self) -> int: ''' - Controls some compatibility flags introduced to toggle some behavior from the official OpenDSS. + Controls some compatibility flags introduced to toggle some behavior from EPRI's OpenDSS. **THE FLAGS ARE GLOBAL, affecting all AltDSS engines in the process.** CompatFlags for Oddie-loaded instances (OpenDSS and OpenDSS-C engines) are handled by the Oddie code itself, diff --git a/dss/IDSSElement.py b/dss/IDSSElement.py index 4035c960..b69dd233 100644 --- a/dss/IDSSElement.py +++ b/dss/IDSSElement.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from __future__ import annotations from ._cffi_api_util import Base from .IDSSProperty import IDSSProperty diff --git a/dss/IDSSEvents.py b/dss/IDSSEvents.py index 90a03bf2..235ebfe1 100644 --- a/dss/IDSSEvents.py +++ b/dss/IDSSEvents.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base, DSSException from .enums import AltDSSEvent from dss_python_backend.events import get_manager_for_ctx @@ -63,7 +63,7 @@ def disconnect(self): class IDSSEvents(Base): """ This interface provides connection to classic the OpenDSS Events - API. For official OpenDSS documentation about this feature, see + API. For EPRI's OpenDSS documentation about this feature, see the document titled "Evaluation of Distribution Reconfiguration Functions in Advanced Distribution Management Systems, Example Assessments of Distribution Automation Using Open Distribution Systems Simulator" (2011), which is available from @@ -73,7 +73,7 @@ class IDSSEvents(Base): VBA/Excel examples of the classic COM usage are found in the folder ["Examples/civinlar model/"](https://sourceforge.net/p/electricdss/code/HEAD/tree/trunk/Version8/Distrib/Examples/civinlar%20model/) ([mirrored here](https://github.com/dss-extensions/electricdss-tst/tree/master/Version8/Distrib/Examples/civinlar%20model), - with minor changes), which is distributed along with the official OpenDSS. + with minor changes), which is distributed along with the EPRI's OpenDSS distribution. For a quick intro, this interface allows connecting an object (event handler) that runs custom actions are three points of the solution process diff --git a/dss/IDSSProgress.py b/dss/IDSSProgress.py index 572cd73a..51464cc4 100644 --- a/dss/IDSSProgress.py +++ b/dss/IDSSProgress.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from typing import AnyStr @@ -8,15 +8,27 @@ class IDSSProgress(Base): __slots__ = [] def Close(self): + ''' + Close progress form + + Typically used with EPRI's OpenDSS, on Windows. Otherwise, it could be a no-op. + ''' self._lib.DSSProgress_Close() def Show(self): + ''' + Show progress form + + Typically used with EPRI's OpenDSS, on Windows. Otherwise, it could be a no-op. + ''' self._lib.DSSProgress_Show() @property def Caption(self) -> str: ''' - (write-only) Caption to appear on the bottom of the DSS Progress form. + Set the caption to appear on the bottom of the DSS Progress form. + + Typically used with EPRI's OpenDSS, on Windows. Otherwise, it could be a no-op. Original COM help: https://opendss.epri.com/Caption.html ''' @@ -29,7 +41,9 @@ def Caption(self, Value: AnyStr): @property def PctProgress(self) -> int: ''' - (write-only) Percent progress to indicate [0..100] + Set the percent progress to indicate [0..100] on the progress form. + + Typically used with EPRI's OpenDSS, on Windows. Otherwise, it could be a no-op. Original COM help: https://opendss.epri.com/PctProgress.html ''' diff --git a/dss/IDSSProperty.py b/dss/IDSSProperty.py index 10bc0fe6..a581a53d 100644 --- a/dss/IDSSProperty.py +++ b/dss/IDSSProperty.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from __future__ import annotations from ._cffi_api_util import Base from typing import AnyStr, Union diff --git a/dss/IDSS_Executive.py b/dss/IDSS_Executive.py index 7b097a43..5c3f5bc7 100644 --- a/dss/IDSS_Executive.py +++ b/dss/IDSS_Executive.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base class IDSS_Executive(Base): diff --git a/dss/IError.py b/dss/IError.py index 7662a856..a47a3844 100644 --- a/dss/IError.py +++ b/dss/IError.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base class IError(Base): @@ -82,7 +82,7 @@ def UseExceptions(self) -> bool: **When disabled, the user takes responsibility for checking for errors.** This can be done through the `Error` interface. When `Error.Number` is not zero, there should be an error message in `Error.Description`. This is compatible - with the behavior on the official OpenDSS (Windows-only COM implementation) when + with the behavior on EPRI's OpenDSS (Windows-only COM implementation) when `AllowForms` is disabled. Users can also use the DSS command `Export ErrorLog` to inspect for errors. diff --git a/dss/IFuses.py b/dss/IFuses.py index 87d2e2f6..9452074b 100644 --- a/dss/IFuses.py +++ b/dss/IFuses.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import List, AnyStr diff --git a/dss/IGICSources.py b/dss/IGICSources.py index 8beafc05..1742355b 100644 --- a/dss/IGICSources.py +++ b/dss/IGICSources.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2023-2024 Paulo Meira -# Copyright (c) 2023-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2023-2025 Paulo Meira +# Copyright (c) 2023-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable class IGICSources(Iterable): diff --git a/dss/IGenerators.py b/dss/IGenerators.py index 4c1098d6..15a24666 100644 --- a/dss/IGenerators.py +++ b/dss/IGenerators.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import List, AnyStr, Union from ._types import Float64Array diff --git a/dss/IISources.py b/dss/IISources.py index 9e14e22b..f4e9c699 100644 --- a/dss/IISources.py +++ b/dss/IISources.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable class IISources(Iterable): diff --git a/dss/ILineCodes.py b/dss/ILineCodes.py index b4fa1abc..c60654e6 100644 --- a/dss/ILineCodes.py +++ b/dss/ILineCodes.py @@ -1,8 +1,8 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable -from ._types import Float64Array +from ._types import Float64Matrix from typing import Union from .enums import LineUnits @@ -55,7 +55,7 @@ def C1(self, Value): self._lib.LineCodes_Set_C1(Value) @property - def Cmatrix(self) -> Float64Array: + def Cmatrix(self) -> Float64Matrix: ''' Capacitance matrix, nF per unit length @@ -64,7 +64,7 @@ def Cmatrix(self) -> Float64Array: return self._lib.LineCodes_Get_Cmatrix_GR() @Cmatrix.setter - def Cmatrix(self, Value: Float64Array): + def Cmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.LineCodes_Set_Cmatrix(ValuePtr, ValueCount) @@ -143,7 +143,7 @@ def R1(self, Value: float): self._lib.LineCodes_Set_R1(Value) @property - def Rmatrix(self) -> Float64Array: + def Rmatrix(self) -> Float64Matrix: ''' Resistance matrix, ohms per unit length @@ -152,7 +152,7 @@ def Rmatrix(self) -> Float64Array: return self._lib.LineCodes_Get_Rmatrix_GR() @Rmatrix.setter - def Rmatrix(self, Value: Float64Array): + def Rmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.LineCodes_Set_Rmatrix(ValuePtr, ValueCount) @@ -191,7 +191,7 @@ def X1(self, Value: float): self._lib.LineCodes_Set_X1(Value) @property - def Xmatrix(self) -> Float64Array: + def Xmatrix(self) -> Float64Matrix: ''' Reactance matrix, ohms per unit length @@ -200,6 +200,6 @@ def Xmatrix(self) -> Float64Array: return self._lib.LineCodes_Get_Xmatrix_GR() @Xmatrix.setter - def Xmatrix(self, Value: Float64Array): + def Xmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.LineCodes_Set_Xmatrix(ValuePtr, ValueCount) diff --git a/dss/ILineGeometries.py b/dss/ILineGeometries.py index 517f6a22..1e8901fb 100644 --- a/dss/ILineGeometries.py +++ b/dss/ILineGeometries.py @@ -1,9 +1,9 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import List, Union -from ._types import Float64Array, Int32Array, Float64ArrayOrComplexArray +from ._types import Float64Array, Float64Matrix, Int32Array, ComplexMatrix from .enums import LineUnits class ILineGeometries(Iterable): @@ -81,15 +81,15 @@ def Phases(self) -> int: def Phases(self, Value: int): self._lib.LineGeometries_Set_Phases(Value) - def Rmatrix(self, Frequency: float, Length: float, Units: int) -> Float64Array: + def Rmatrix(self, Frequency: float, Length: float, Units: int) -> Float64Matrix: '''Resistance matrix, ohms''' return self._lib.LineGeometries_Get_Rmatrix_GR(Frequency, Length, Units) - def Xmatrix(self, Frequency: float, Length: float, Units: int) -> Float64Array: + def Xmatrix(self, Frequency: float, Length: float, Units: int) -> Float64Matrix: '''Reactance matrix, ohms''' return self._lib.LineGeometries_Get_Xmatrix_GR(Frequency, Length, Units) - def Zmatrix(self, Frequency: float, Length: float, Units: int) -> Float64ArrayOrComplexArray: + def Zmatrix(self, Frequency: float, Length: float, Units: int) -> ComplexMatrix: '''Complex impedance matrix, ohms''' return self._lib.LineGeometries_Get_Zmatrix_GR(Frequency, Length, Units) diff --git a/dss/ILineSpacings.py b/dss/ILineSpacings.py index 105239f6..fad005ef 100644 --- a/dss/ILineSpacings.py +++ b/dss/ILineSpacings.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import Union diff --git a/dss/ILines.py b/dss/ILines.py index f80c462c..9f3d3f58 100644 --- a/dss/ILines.py +++ b/dss/ILines.py @@ -1,8 +1,8 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable -from ._types import Float64Array, Float64ArrayOrComplexArray +from ._types import Float64Matrix, ComplexMatrix from typing import AnyStr, Union from .enums import LineUnits @@ -42,7 +42,8 @@ class ILines(Iterable): 'Units', ] - def New(self, Name): + def New(self, Name: str): + '''Create new Line object with the given `Name`''' return self._lib.Lines_New(Name) @property @@ -98,11 +99,11 @@ def C1(self, Value: float): self._lib.Lines_Set_C1(Value) @property - def Cmatrix(self) -> Float64Array: + def Cmatrix(self) -> Float64Matrix: return self._lib.Lines_Get_Cmatrix_GR() @Cmatrix.setter - def Cmatrix(self, Value: Float64Array): + def Cmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.Lines_Set_Cmatrix(ValuePtr, ValueCount) @@ -135,7 +136,7 @@ def Geometry(self, Value: AnyStr): @property def Length(self) -> float: ''' - Length of line section in units compatible with the LineCode definition. + Length of line in units compatible with the LineCode definition. Original COM help: https://opendss.epri.com/Length.html ''' @@ -174,7 +175,7 @@ def NormAmps(self, Value: float): @property def NumCust(self) -> int: ''' - Number of customers on this line section. + Number of customers on this line. *Requires an energy meter with an updated zone.* @@ -259,7 +260,7 @@ def Rho(self, Value: float): self._lib.Lines_Set_Rho(Value) @property - def Rmatrix(self) -> Float64Array: + def Rmatrix(self) -> Float64Matrix: ''' Resistance matrix (full), ohms per unit length. Array of doubles. @@ -268,7 +269,7 @@ def Rmatrix(self) -> Float64Array: return self._lib.Lines_Get_Rmatrix_GR() @Rmatrix.setter - def Rmatrix(self, Value: Float64Array): + def Rmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.Lines_Set_Rmatrix(ValuePtr, ValueCount) @@ -288,7 +289,7 @@ def Spacing(self, Value: AnyStr): @property def TotalCust(self) -> int: ''' - Total Number of customers served from this line section. + Total Number of customers served from this line. Original COM help: https://opendss.epri.com/TotalCust.html ''' @@ -296,6 +297,11 @@ def TotalCust(self) -> int: @property def Units(self) -> LineUnits: + ''' + Length units for the active line. + + Original COM help: https://opendss.epri.com/Units.html + ''' return LineUnits(self._lib.Lines_Get_Units()) @Units.setter @@ -342,7 +348,7 @@ def Xg(self, Value: float): self._lib.Lines_Set_Xg(Value) @property - def Xmatrix(self) -> Float64Array: + def Xmatrix(self) -> Float64Matrix: ''' Reactance matrix (full), ohms per unit length. Array of doubles. @@ -351,12 +357,12 @@ def Xmatrix(self) -> Float64Array: return self._lib.Lines_Get_Xmatrix_GR() @Xmatrix.setter - def Xmatrix(self, Value: Float64Array): + def Xmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.Lines_Set_Xmatrix(ValuePtr, ValueCount) @property - def Yprim(self) -> Float64ArrayOrComplexArray: + def Yprim(self) -> ComplexMatrix: ''' Yprimitive for the active line object (complex array). @@ -365,7 +371,7 @@ def Yprim(self) -> Float64ArrayOrComplexArray: return self._lib.Lines_Get_Yprim_GR() @Yprim.setter - def Yprim(self, Value: Float64ArrayOrComplexArray): + def Yprim(self, Value: ComplexMatrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.Lines_Set_Yprim(ValuePtr, ValueCount) @@ -381,7 +387,7 @@ def SeasonRating(self) -> float: @property def IsSwitch(self) -> bool: ''' - Sets/gets the Line element switch status. Setting it has side-effects to the line parameters. + Line element switch status. Setting it has side-effects to the line parameters. **(API Extension)** ''' diff --git a/dss/ILoadShapes.py b/dss/ILoadShapes.py index 30ccea84..4c8ab277 100644 --- a/dss/ILoadShapes.py +++ b/dss/ILoadShapes.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import AnyStr diff --git a/dss/ILoads.py b/dss/ILoads.py index eb77fe41..cec02ae1 100644 --- a/dss/ILoads.py +++ b/dss/ILoads.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import AnyStr, Union @@ -106,7 +106,7 @@ def CVRwatts(self, Value: float): @property def Cfactor(self) -> float: ''' - Factor relates average to peak kw. Used for allocation with kwh and kwhdays + CFactor relates average to peak kw. Used for allocation with kwh and kwhdays Original COM help: https://opendss.epri.com/Cfactor.html ''' @@ -223,7 +223,9 @@ def PctStdDev(self, Value: float): @property def RelWeight(self) -> float: ''' - Relative Weighting factor for the active LOAD + Relative Weighting factor for the active load. + + This value is used in reliability methods. Original COM help: https://opendss.epri.com/RelWeight.html ''' diff --git a/dss/IMeters.py b/dss/IMeters.py index 8ac7212f..d17917a3 100644 --- a/dss/IMeters.py +++ b/dss/IMeters.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import List, AnyStr from ._types import Float64Array @@ -119,7 +119,7 @@ def SetActiveSection(self, SectIdx: int): @property def AllBranchesInZone(self) -> List[str]: ''' - Wide string list of all branches in zone of the active EnergyMeter object. + List (strings) of all branches in zone of the active EnergyMeter object. Original COM help: https://opendss.epri.com/AllBranchesInZone.html ''' diff --git a/dss/IMonitors.py b/dss/IMonitors.py index 840bffcb..12638334 100644 --- a/dss/IMonitors.py +++ b/dss/IMonitors.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import DSSException, Iterable import numpy as np from typing import List, AnyStr @@ -210,7 +210,7 @@ def Header(self) -> List[str]: @property def Mode(self) -> int: ''' - Set Monitor mode (bitmask integer - see DSS Help) + Monitor mode (bitmask integer - see DSS Help) Original COM help: https://opendss.epri.com/Mode1.html ''' diff --git a/dss/IPDElements.py b/dss/IPDElements.py index 4f7797e3..c5e5ce05 100644 --- a/dss/IPDElements.py +++ b/dss/IPDElements.py @@ -1,12 +1,26 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from __future__ import annotations from ._cffi_api_util import Base from typing import List, AnyStr, Iterator -from ._types import Float64Array, Int32Array, Float64ArrayOrComplexArray +from ._types import Float64Array, Int32Array, ComplexArray class IPDElements(Base): + ''' + The PDElements interface allows accessing some common properties and + methods shared across power delivery elements in the DSS engine. + + Users can iterate on all PD elements directly through this interface, + or enable a PD element through a dedicated interface (e.g. use `Lines.Name`, `Transformers.First/Next`) + and access the properties here. + + If you are new to OpenDSS/AltDSS and this classic interface, please read the following document + for an overview of the "active element" paradigm used by COM and the classic APIs: + + https://dss-extensions.org/classic_api.html#the-active-paradigm + ''' + __slots__ = [] _columns = [ @@ -262,7 +276,7 @@ def AllPctEmerg(self, AllNodes: bool = False) -> Float64Array: return self._lib.PDElements_Get_AllPctEmerg_GR(AllNodes) @property - def AllCurrents(self) -> Float64ArrayOrComplexArray: + def AllCurrents(self) -> ComplexArray: ''' Complex array of currents for all conductors, all terminals, for each PD element. @@ -280,7 +294,7 @@ def AllCurrentsMagAng(self) -> Float64Array: return self._lib.PDElements_Get_AllCurrentsMagAng_GR() @property - def AllCplxSeqCurrents(self) -> Float64ArrayOrComplexArray: + def AllCplxSeqCurrents(self) -> ComplexArray: ''' Complex double array of Sequence Currents for all conductors of all terminals, for each PD elements. @@ -298,7 +312,7 @@ def AllSeqCurrents(self) -> Float64Array: return self._lib.PDElements_Get_AllSeqCurrents_GR() @property - def AllPowers(self) -> Float64ArrayOrComplexArray: + def AllPowers(self) -> ComplexArray: ''' Complex array of powers into each conductor of each terminal, for each PD element. @@ -307,7 +321,7 @@ def AllPowers(self) -> Float64ArrayOrComplexArray: return self._lib.PDElements_Get_AllPowers_GR() @property - def AllSeqPowers(self) -> Float64ArrayOrComplexArray: + def AllSeqPowers(self) -> ComplexArray: ''' Complex array of sequence powers into each 3-phase terminal, for each PD element diff --git a/dss/IPVSystems.py b/dss/IPVSystems.py index 71d48b85..b121664f 100644 --- a/dss/IPVSystems.py +++ b/dss/IPVSystems.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import List, AnyStr diff --git a/dss/IParallel.py b/dss/IParallel.py index 3b2af4dd..7e717782 100644 --- a/dss/IParallel.py +++ b/dss/IParallel.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from ._types import Int32Array diff --git a/dss/IParser.py b/dss/IParser.py index f1478f1e..c844490b 100644 --- a/dss/IParser.py +++ b/dss/IParser.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from ._types import Float64Array from typing import AnyStr diff --git a/dss/IReactors.py b/dss/IReactors.py index 4bb22839..e0604c21 100644 --- a/dss/IReactors.py +++ b/dss/IReactors.py @@ -1,8 +1,8 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from typing import AnyStr -from ._types import Float64Array, Float64ArrayOrSimpleComplex +from ._types import Float64Matrix, Complex from ._cffi_api_util import Iterable class IReactors(Iterable): @@ -188,37 +188,37 @@ def Rp(self, Value: float): self._lib.Reactors_Set_Rp(Value) @property - def Rmatrix(self) -> Float64Array: + def Rmatrix(self) -> Float64Matrix: '''Resistance matrix, ohms at base frequency. Order of the matrix is the number of phases. Mutually exclusive to specifying parameters by kvar or X.''' return self._lib.Reactors_Get_Rmatrix_GR() @Rmatrix.setter - def Rmatrix(self, Value: Float64Array): + def Rmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.Reactors_Set_Rmatrix(ValuePtr, ValueCount) @property - def Xmatrix(self) -> Float64Array: + def Xmatrix(self) -> Float64Matrix: '''Reactance matrix, ohms at base frequency. Order of the matrix is the number of phases. Mutually exclusive to specifying parameters by kvar or X.''' return self._lib.Reactors_Get_Xmatrix_GR() @Xmatrix.setter - def Xmatrix(self, Value: Float64Array): + def Xmatrix(self, Value: Float64Matrix): Value, ValuePtr, ValueCount = self._prepare_float64_array(Value) self._lib.Reactors_Set_Xmatrix(ValuePtr, ValueCount) @property - def Z(self) -> Float64ArrayOrSimpleComplex: + def Z(self) -> Complex: '''Alternative way of defining R and X properties. Enter a 2-element array representing R +jX in ohms.''' return self._lib.Reactors_Get_Z_GR() @Z.setter - def Z(self, Value: Float64ArrayOrSimpleComplex): + def Z(self, Value: Complex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) self._lib.Reactors_Set_Z(ValuePtr, ValueCount) @property - def Z1(self) -> Float64ArrayOrSimpleComplex: + def Z1(self) -> Complex: ''' Positive-sequence impedance, ohms, as a 2-element array representing a complex number. @@ -231,12 +231,12 @@ def Z1(self) -> Float64ArrayOrSimpleComplex: return self._lib.Reactors_Get_Z1_GR() @Z1.setter - def Z1(self, Value: Float64ArrayOrSimpleComplex): + def Z1(self, Value: Complex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) self._lib.Reactors_Set_Z1(ValuePtr, ValueCount) @property - def Z2(self) -> Float64ArrayOrSimpleComplex: + def Z2(self) -> Complex: ''' Negative-sequence impedance, ohms, as a 2-element array representing a complex number. @@ -247,12 +247,12 @@ def Z2(self) -> Float64ArrayOrSimpleComplex: return self._lib.Reactors_Get_Z2_GR() @Z2.setter - def Z2(self, Value: Float64ArrayOrSimpleComplex): + def Z2(self, Value: Complex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) self._lib.Reactors_Set_Z2(ValuePtr, ValueCount) @property - def Z0(self) -> Float64ArrayOrSimpleComplex: + def Z0(self) -> Complex: ''' Zero-sequence impedance, ohms, as a 2-element array representing a complex number. @@ -263,7 +263,7 @@ def Z0(self) -> Float64ArrayOrSimpleComplex: return self._lib.Reactors_Get_Z0_GR() @Z0.setter - def Z0(self, Value: Float64ArrayOrSimpleComplex): + def Z0(self, Value: Complex): Value, ValuePtr, ValueCount = self._prepare_complex128_simple(Value) self._lib.Reactors_Set_Z0(ValuePtr, ValueCount) diff --git a/dss/IReclosers.py b/dss/IReclosers.py index 958777b1..831f07ae 100644 --- a/dss/IReclosers.py +++ b/dss/IReclosers.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import AnyStr @@ -184,7 +184,7 @@ def Reset(self): @property def State(self) -> int: ''' - Get/Set present state of recloser. + Present state of recloser. If set to open (ActionCodes.Open=1), open recloser's controlled element and lock out the recloser. If set to close (ActionCodes.Close=2), close recloser's controlled element and resets recloser to first operation. ''' diff --git a/dss/IReduceCkt.py b/dss/IReduceCkt.py index 25c1e8ec..c84fb6f5 100644 --- a/dss/IReduceCkt.py +++ b/dss/IReduceCkt.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2019-2024 Paulo Meira -# Copyright (c) 2019-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2019-2025 Paulo Meira +# Copyright (c) 2019-2025 DSS-Extensions contributors from ._cffi_api_util import Base from typing import AnyStr diff --git a/dss/IRegControls.py b/dss/IRegControls.py index bfa46994..d548b6b7 100644 --- a/dss/IRegControls.py +++ b/dss/IRegControls.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import AnyStr diff --git a/dss/IRelays.py b/dss/IRelays.py index 7d612225..35688519 100644 --- a/dss/IRelays.py +++ b/dss/IRelays.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import AnyStr @@ -98,7 +98,7 @@ def Reset(self): @property def State(self) -> int: ''' - Get/Set present state of relay. + Present state of relay. If set to open, open relay's controlled element and lock out the relay. If set to close, close relay's controlled element and resets relay to first operation. ''' diff --git a/dss/ISensors.py b/dss/ISensors.py index 7a9082e4..87233f6e 100644 --- a/dss/ISensors.py +++ b/dss/ISensors.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import AnyStr diff --git a/dss/ISettings.py b/dss/ISettings.py index aba56691..d19ba106 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from ._types import Float64Array, Int32Array from typing import AnyStr, Union, List @@ -512,7 +512,7 @@ def AdvancedTypes(self, Value: bool): @property def CompatFlags(self) -> int: ''' - Controls some compatibility flags introduced to toggle some behavior from the official OpenDSS. + Controls some compatibility flags introduced to toggle some behavior from EPRI's OpenDSS. **THE FLAGS ARE GLOBAL, affecting all AltDSS engines in the process.** CompatFlags for Oddie-loaded instances (OpenDSS and OpenDSS-C engines) are handled by the Oddie code itself, @@ -559,8 +559,8 @@ def PreferLists(self, value: bool): @property def COMErrorResults(self) -> bool: ''' - If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with the - official OpenDSS COM interface. + If enabled, in case of errors or empty arrays, the API returns arrays with values compatible with + EPRI's OpenDSS COM interface. For example, consider the property `Loads.ZIPV`. If there is no active circuit or active load element: @@ -568,9 +568,11 @@ def COMErrorResults(self) -> bool: - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should be compatible with the return value of the official COM interface. - Defaults to False/0 (disabled state), starting DSS-Python v0.16. + Defaults to false (disabled state) in AltDSS since the v0.15.x series. - This can also be set through the environment variable `DSS_CAPI_COM_DEFAULTS`. Setting it to 0 disables + This does not affect the results when using EPRI's OpenDSS distribution through Oddie. + + This can also be set through the environment variable `DSS_CAPI_COM_DEFAULTS`. Setting it to 1 enables the legacy/COM behavior. The value can be toggled through the API at any time. **(API Extension)** diff --git a/dss/ISolution.py b/dss/ISolution.py index af4cd408..1f10a44f 100644 --- a/dss/ISolution.py +++ b/dss/ISolution.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from ._types import Int32Array from typing import Union, AnyStr, List diff --git a/dss/IStorages.py b/dss/IStorages.py index c27c92be..ba28fd0e 100644 --- a/dss/IStorages.py +++ b/dss/IStorages.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2023-2024 Paulo Meira -# Copyright (c) 2023-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2023-2025 Paulo Meira +# Copyright (c) 2023-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import List, Union @@ -193,6 +193,8 @@ def kVA(self, Value: float) -> None: def kvar(self) -> float: ''' Get/set the requested kvar value. Final kvar is subjected to the inverter ratings. Sets inverter to operate in constant kvar mode. + + **Note:** reading the DSS property `kvar` returns the adjusted value, while this returns the original input value. ''' return self._lib.Storages_Get_kvar() diff --git a/dss/ISwtControls.py b/dss/ISwtControls.py index 720e74b9..16bb1066 100644 --- a/dss/ISwtControls.py +++ b/dss/ISwtControls.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import AnyStr, Union from .enums import ActionCodes diff --git a/dss/ITSData.py b/dss/ITSData.py index 13a4a8b5..bc50e674 100644 --- a/dss/ITSData.py +++ b/dss/ITSData.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable class ITSData(Iterable): diff --git a/dss/IText.py b/dss/IText.py index 70b73947..518ece8d 100644 --- a/dss/IText.py +++ b/dss/IText.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from typing import AnyStr, List, Union diff --git a/dss/ITopology.py b/dss/ITopology.py index 44d66422..dca1e4a3 100644 --- a/dss/ITopology.py +++ b/dss/ITopology.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base from typing import List, AnyStr diff --git a/dss/ITransformers.py b/dss/ITransformers.py index 90092ea4..46f2f5e1 100644 --- a/dss/ITransformers.py +++ b/dss/ITransformers.py @@ -1,8 +1,8 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable -from ._types import Float64ArrayOrComplexArray +from ._types import ComplexArray, ComplexMatrix from typing import AnyStr, Union from .enums import CoreType as TransformerCoreType @@ -247,7 +247,7 @@ def kVA(self, Value: float): kva = kVA @property - def WdgVoltages(self) -> Float64ArrayOrComplexArray: + def WdgVoltages(self) -> ComplexArray: ''' Complex array of voltages for active winding @@ -259,7 +259,7 @@ def WdgVoltages(self) -> Float64ArrayOrComplexArray: return self._lib.Transformers_Get_WdgVoltages_GR() @property - def WdgCurrents(self) -> Float64ArrayOrComplexArray: + def WdgCurrents(self) -> ComplexArray: ''' All Winding currents (ph1, wdg1, wdg2,... ph2, wdg1, wdg2 ...) @@ -307,7 +307,7 @@ def RdcOhms(self, Value: float): self._lib.Transformers_Set_RdcOhms(Value) @property - def LossesByType(self) -> Float64ArrayOrComplexArray: + def LossesByType(self) -> ComplexArray: ''' Complex array with the losses by type (total losses, load losses, no-load losses), in VA, for the current active transformer @@ -316,7 +316,7 @@ def LossesByType(self) -> Float64ArrayOrComplexArray: return self._lib.Transformers_Get_LossesByType_GR() @property - def AllLossesByType(self) -> Float64ArrayOrComplexArray: + def AllLossesByType(self) -> ComplexMatrix: ''' Complex array with the losses by type (total losses, load losses, no-load losses), in VA, concatenated for ALL transformers diff --git a/dss/IVsources.py b/dss/IVsources.py index b7579b7b..33bf0cd8 100644 --- a/dss/IVsources.py +++ b/dss/IVsources.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable class IVsources(Iterable): diff --git a/dss/IWindGens.py b/dss/IWindGens.py index 4f3d9af1..7a2896ec 100644 --- a/dss/IWindGens.py +++ b/dss/IWindGens.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2024 Paulo Meira -# Copyright (c) 2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2024-2025 Paulo Meira +# Copyright (c) 2024-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array from typing import List, Union, AnyStr diff --git a/dss/IWireData.py b/dss/IWireData.py index 084e0443..4d7d0b9e 100644 --- a/dss/IWireData.py +++ b/dss/IWireData.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import Union from .enums import LineUnits diff --git a/dss/IXYCurves.py b/dss/IXYCurves.py index cd981a8b..eeb21dea 100644 --- a/dss/IXYCurves.py +++ b/dss/IXYCurves.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from ._types import Float64Array @@ -37,7 +37,7 @@ def Npts(self, Value: int): @property def Xarray(self) -> Float64Array: ''' - Get/set X values as a Array of doubles. Set Npts to max number expected if setting + Get/set X values as an array of doubles. When setting, remember to set Npts to max number expected values. Original COM help: https://opendss.epri.com/Xarray.html ''' @@ -77,7 +77,7 @@ def Xshift(self, Value: float): @property def Yarray(self) -> Float64Array: ''' - Get/Set Y values in curve; Set Npts to max number expected if setting + Get/set Y values as an array of doubles. When setting, remember to set Npts to max number expected values. Original COM help: https://opendss.epri.com/Yarray.html ''' diff --git a/dss/IYMatrix.py b/dss/IYMatrix.py index 0c8fbaaa..4b61072d 100644 --- a/dss/IYMatrix.py +++ b/dss/IYMatrix.py @@ -1,6 +1,6 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2016-2024 Paulo Meira -# Copyright (c) 2018-2024 DSS-Extensions contributors +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2016-2025 Paulo Meira +# Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Base import numpy as np from ._types import Int32Array, ComplexArray diff --git a/dss/IZIP.py b/dss/IZIP.py index 1b342d1d..6ad8ba0e 100644 --- a/dss/IZIP.py +++ b/dss/IZIP.py @@ -1,5 +1,5 @@ -# A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface. -# Copyright (c) 2021-2024 Paulo Meira +# A compatibility layer for DSS C-API that mimics EPRI's OpenDSS COM interface. +# Copyright (c) 2021-2025 Paulo Meira from ._cffi_api_util import Base from typing import AnyStr, Optional, List @@ -12,6 +12,8 @@ class IZIP(Base): The implementation provides a specialization which allows more efficient access if the ZIP file is open and reused for many circuits. Doing so reduces the overhead of the initial opening and indexing of the file contents. + *Not available when using EPRI's OpenDSS distribution.* + (**API Extension**) ''' diff --git a/dss/Oddie.py b/dss/Oddie.py index d30808db..afeb1c95 100644 --- a/dss/Oddie.py +++ b/dss/Oddie.py @@ -72,7 +72,7 @@ def _handle_load_lib_error(self): def is_oddie(self) -> bool: """ Returns True if this instance is based on the Oddie compatibility layer for - the official OpenDSS Direct API (a.k.a. DCSL). + EPRI's OpenDSS Direct API (a.k.a. DCSL). Note that the default instance in OpenDSSDirect.py is based on AltDSS since 2018. """ diff --git a/dss/__init__.py b/dss/__init__.py index 4615c601..65124e6c 100644 --- a/dss/__init__.py +++ b/dss/__init__.py @@ -1,4 +1,4 @@ -'''``dss`` is the main package for DSS-Python. DSS-Python is a compatibility layer for the DSS C-API library that mimics the official OpenDSS COM interface, with many extensions and a few limitations. +'''``dss`` is the main package for DSS-Python. DSS-Python is a compatibility layer for the DSS C-API library that mimics EPRI's OpenDSS COM interface, with many extensions and a few limitations. This module used to provide instances for the OpenDSS Version 7 implementation. As of 2022, most of the parallel-machine functions of EPRI's OpenDSS have been reimplemented using a different approach. Therefore the PM functions are available in the instances of this module too. diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index 6f88111b..c9eb0cda 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -3,7 +3,7 @@ from functools import partial, wraps from weakref import ref, WeakKeyDictionary import numpy as np -from ._types import Float64Array, Int32Array, Int8Array, ComplexArray, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex +from ._types import Float64Array, Int32Array, Int8Array, ComplexArray, ComplexArray, Complex from typing import Any, AnyStr, Callable, List, Union, Iterator, Optional, TYPE_CHECKING from .enums import AltDSSEvent from dss_python_backend.events import get_manager_for_ctx @@ -113,7 +113,7 @@ def get_float64_array(self, func, *args) -> Float64Array: return res - def get_complex128_array(self, func, *args) -> Float64ArrayOrComplexArray: + def get_complex128_array(self, func, *args) -> ComplexArray: if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: return self.get_float64_array(func, *args) @@ -149,7 +149,7 @@ def get_fcomplex128_array(self, func, *args) -> Union[ComplexArray, None]: return res - # def get_complex128_array2(self, func, *args) -> Float64ArrayOrComplexArray: + # def get_complex128_array2(self, func, *args) -> ComplexArray: # if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: # return self.get_float64_array2(func, *args) @@ -163,7 +163,7 @@ def get_fcomplex128_array(self, func, *args) -> Union[ComplexArray, None]: # return res - def get_complex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: + def get_complex128_simple(self, func, *args) -> Complex: if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: return self.get_float64_array(func, *args) @@ -177,7 +177,7 @@ def get_complex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: finally: self.DSS_Dispose_PDouble(ptr) - def get_fcomplex128_simple(self, func, *args) -> Float64ArrayOrSimpleComplex: + def get_fcomplex128_simple(self, func, *args) -> Complex: # Currently we use the same as API as get_float64_array, may change later ptr = self._ffi.new('double**') cnt = self._ffi.new('int32_t[4]') @@ -242,7 +242,7 @@ def get_fcomplex128_gr_array(self) -> ComplexArray: return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 8), dtype=complex).copy() - def get_complex128_gr_simple(self) -> Float64ArrayOrSimpleComplex: + def get_complex128_gr_simple(self) -> Complex: if not (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: return self.get_float64_gr_array() @@ -252,7 +252,7 @@ def get_complex128_gr_simple(self) -> Float64ArrayOrSimpleComplex: return ptr[0][0] - def get_fcomplex128_gr_simple(self) -> complex: + def get_fcomplex128_gr_simple(self) -> Complex: # Currently we use the same as API as get_float64_array, may change later ptr, cnt = self.gr_cfloat64_pointers assert cnt[0] == 2, ('Unexpected number of elements returned by API', cnt[0]) @@ -755,7 +755,7 @@ def _check_for_error(self, result=None): If the user disabled exceptions, any error is simply ignored. Note that, in this case, manually calling this function would have no purpose/effects. - Note that, **in the future**, we may try showing a popup form like the official OpenDSS does on Windows + Note that, **in the future**, we may try showing a popup form like EPRI's OpenDSS does on Windows if AllowForms is True. This behavior is not very portable though and not adequate for automated scripts. """ if self._errorPtr[0] and Base.using_exceptions: @@ -921,7 +921,7 @@ def _check_for_error(self, result=None): If the user disabled exceptions, any error is simply ignored. Note that, in this case, manually calling this function would have no purpose/effects. - Note that, **in the future**, we may try showing a popup form like the official OpenDSS does on Windows + Note that, **in the future**, we may try showing a popup form like EPRI's OpenDSS does on Windows if AllowForms is True. This behavior is not very portable though and not adequate for automated scripts. """ if self._errorPtr[0] and Base.using_exceptions: @@ -1130,7 +1130,7 @@ def prepare_complex128_array(self, value): return value, ptr, cnt - def prepare_complex128_simple(self, value: complex): + def prepare_complex128_simple(self, value: Complex): if isinstance(value, (np.complex128, complex)): value = np.asarray([value], dtype=np.complex128).view(dtype=np.float64) elif (isinstance(value, np.array) and value.dtype in (np.complex128, np.complex64)): @@ -1212,7 +1212,7 @@ def get_bus_obj(self, ptr) -> Optional[AltBus]: def _oddie_not_impl(): - raise NotImplementedError("This API requires a function that is not implemented in the official OpenDSS engine.") + raise NotImplementedError("This API requires a function that is not implemented in EPRI's OpenDSS engine.") class Iterable(Base): __slots__ = [ diff --git a/dss/_types.py b/dss/_types.py index c3994c11..613aa80b 100644 --- a/dss/_types.py +++ b/dss/_types.py @@ -1,7 +1,14 @@ import numpy as np -from typing import Union try: import numpy.typing as npt + # TODO: update after these are closed: + # - https://github.com/numpy/numpy/issues/16544 + # - https://github.com/python/typing/issues/516 + + ComplexMatrix = npt.NDArray[np.complex128] + Float64Matrix = npt.NDArray[np.float64] + Float32Matrix = npt.NDArray[np.float32] + Int32Matrix = npt.NDArray[np.int32] ComplexArray = npt.NDArray[np.complex128] Float64Array = npt.NDArray[np.float64] Float32Array = npt.NDArray[np.float32] @@ -10,6 +17,10 @@ BoolArray = npt.NDArray[np.bool_] except (ModuleNotFoundError, ImportError, AttributeError): from typing import List + ComplexMatrix = List[complex] + Float64Matrix = List[np.float64] + Float32Matrix = List[np.float32] + Int32Matrix = List[np.int32] ComplexArray = List[complex] Float64Array = List[np.float64] Float32Array = List[np.float32] @@ -17,5 +28,4 @@ Int8Array = List[np.int8] BoolArray = List[bool] -Float64ArrayOrComplexArray = Union[Float64Array, ComplexArray] -Float64ArrayOrSimpleComplex = Union[Float64Array, complex] +Complex = complex diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 1bbee793..50b06c20 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -433,7 +433,7 @@ def get_archive_fn(live_fn, fn_prefix=None): from _settings import DSS oddd_ver = DSS.Version.split(' ')[1] - print("Using official OpenDSS through ODDIE:", DSS.Version) + print("Using EPRI's OpenDSS (or API-compatible) through Oddie:", DSS.Version) if USE_ODDIE != '1': print("User-provided library path:", USE_ODDIE) @@ -458,7 +458,7 @@ def get_archive_fn(live_fn, fn_prefix=None): DSS = comtypes.client.CreateObject("OpenDSSEngine.DSS") DSS = dss.patch_dss_com(DSS) #DSS.Text.Command = r'set editor=ignore_me_invalid_executable' -- need to let it open for some reports :| - print("Using official OpenDSS COM:", DSS.Version) + print("Using EPRI's OpenDSS COM engine:", DSS.Version) com_ver = DSS.Version.split(' ')[1] suffix = f'-COM-{platform.machine()}-{com_ver}' From 306cc31f7fe0da845cc27362840ccc891914d936 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 7 May 2025 01:19:33 -0300 Subject: [PATCH 55/82] Circuit: Expose `Flatten` --- dss/ICircuit.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dss/ICircuit.py b/dss/ICircuit.py index 4a6fe1fa..1c47e0a1 100644 --- a/dss/ICircuit.py +++ b/dss/ICircuit.py @@ -678,4 +678,30 @@ def Save(self, dirOrFilePath: AnyStr, saveFlags: Union[DSSSaveFlags, List[DSSSav return self._api_util.get_string(self._lib.Circuit_Save(dirOrFilePath, saveFlags)) + def Flatten(self) -> None: + ''' + Flatten the circuit + + Flatten the circuit structures, removing any object of the following types: + + - XfmrCode + - LineCode + - LineSpacing + - LineGeometry + - WireData + - CNData + - TSData + + The general data from those objects is propagated to the referencing Line and Transformer objects, + and the properties on the latter are updated to remove any references to the removed objects. + + This is useful for some converting the DSS circuit to another format, without requiring the user to handle all + the types listed above. This, of course, results in some limitations since a lot of detail is removed. Numerically, + a normal snapshot or daily solution should be the same before and after the flatten operation. + + Available only on AltDSS. + + **(API Extension)** + ''' + self._lib.Circuit_Flatten() From 1463c4e160fdca976ede7cbfad8f7b48f11b11e7 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 22 May 2025 01:20:51 -0300 Subject: [PATCH 56/82] Implement Settings.PreserveCase --- dss/ISettings.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dss/ISettings.py b/dss/ISettings.py index d19ba106..6473c922 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -119,6 +119,7 @@ class ISettings(Base): # 'PreferLists', # 'SkipFileRegExp', # 'CompatFlags', + # 'PreserveCase', ] @@ -639,3 +640,22 @@ def AllowEditor(self) -> bool: @AllowEditor.setter def AllowEditor(self, value: bool): self._lib.DSS_Set_AllowEditor(value) + + @property + def PreserveCase(self) -> bool: + ''' + Gets/sets whether running the engine try to preserve original names + + When enabled, bus and element names in many of the API functions, reports and + exports are kept as provided by the user, without applying lower or upper case + transformations. + + Note that, even when enabled, the engine is still case-insensitive. + + **(API Extension)** + ''' + return self._lib.Settings_Get_Flag(1) + + @PreserveCase.setter + def PreserveCase(self, value: bool): + self._lib.Settings_Set_Flag(1, value) From 89128754a9df4d4580afe8c80894b029aab60141 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:42:15 -0300 Subject: [PATCH 57/82] Plot: toggle some features to ensure compatiblity --- dss/plot.py | 4 +++- dss/plot2.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index 2377263d..3d565141 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -2520,8 +2520,10 @@ def dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): raise NotImplementedError(f'ERROR: not implemented plot type "{ptype}"') return -1 - with ToggleAdvancedTypes(DSS, False), warnings.catch_warnings(): + with DSS.ActiveCircuit.Settings.Context() as settings, warnings.catch_warnings(): warnings.simplefilter("ignore") + settings.AdvancedTypes = False + settings.PreferLists = False func = getattr(plotter, dss_plot_methods.get(ptype)) return 0, (DSS, **kwargs) diff --git a/dss/plot2.py b/dss/plot2.py index c5fdacab..323a10c5 100644 --- a/dss/plot2.py +++ b/dss/plot2.py @@ -251,7 +251,7 @@ def show(text): @register_cell_magic def dss(line, cell): - if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_odd: + if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_oddie: DSSPlotCtx.Text.Commands(cell) else: for line in cell.split('\n'): From 1502971c5a8ae32538886206df01416a2d440013 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:53:23 -0300 Subject: [PATCH 58/82] Plot: ensure the target element is active in `Visualize`; implement `MarkAt` for DSVs. This commit also reorders some ops in `Visualize`, and starts updating the plot examples. --- docs/examples/Plotting.ipynb | 11 ++++++-- dss/plot.py | 51 +++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/docs/examples/Plotting.ipynb b/docs/examples/Plotting.ipynb index 7f96718a..7c4861ef 100644 --- a/docs/examples/Plotting.ipynb +++ b/docs/examples/Plotting.ipynb @@ -604,14 +604,21 @@ "\n", "// the original script already runs a scenario\n", "redirect electricdss-tst/Version8/Distrib/IEEETestCases/123Bus/Run_YearlySim.dss\n", + "Set Year=2 LoadMult=1.05\n", + "solve\n", + "closedi\n", "\n", "// we need a second scenario to compare later\n", "Set CaseName=another\n", "batchedit load..* yearly=default\n", "set mode=yearly number=720\n", - "Set Year=1\n", + "Set Year=1 LoadMult=1\n", + "solve\n", + "Set Year=2 LoadMult=1.05\n", "solve\n", - "closedi" + "closedi\n", + "! Reset LoadMult\n", + "Set LoadMult=1" ] }, { diff --git a/dss/plot.py b/dss/plot.py index 3d565141..90e99f80 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -587,6 +587,15 @@ def Marker(self, param_str: str): self.ax.plot(x, y, ls=None, color=_int_to_color(c), **marker_dict) + def MarkAt(self, param_str: str): + params = param_str.split(',') + x, y = float(params[0]), float(params[1]) + symbol, marker_size = [int(v) for v in params[2:]] + marker_dict = get_marker_dict(symbol) + marker_dict['markersize'] *= max(1, np.sqrt(marker_size) - 1) * marker_dict['markersize'] / 7.0 + self.ax.plot(x, y, ls=None, color=self.color, **marker_dict) + + def Move(self, param_str: str): x, y = [float(v.strip().strip('"')) for v in param_str.split(',')] if self.no_scales: @@ -1792,32 +1801,22 @@ def dss_visualize_plot(self, element = DSS.ActiveCircuit.ActiveCktElement etype, ename = ElementType, ElementName + full_name = f'{ElementType}.{ElementName}'.lower() + + # Check it the target element is currently selected + if element.Name.lower() != full_name: + DSS.ActiveCircuit.SetActiveElement(full_name) + nconds = element.NumConductors # nphases = element.NumPhases buses = element.BusNames[:2] # max 2 terminals vbases = [max(1, 1000 * DSS.ActiveCircuit.Buses[nodot(b)].kVBase) for b in buses] - # assert DSS.ActiveCircuit.ActiveCktElement.Name == ElementType + '.' + ElementName - fig, ax = plt.subplots(1, gridspec_kw=dict(left=0.05, right=0.95, bottom=0.05, top=0.92))#, figsize=(8.6, 7)) - ax.get_xaxis().set_visible(False) - ax.get_yaxis().set_visible(False) - ax.grid(False) - y = 20 + 10 * nconds box_xy0 = np.array([100, 10]) box_xy1 = np.array([XMAX - 100, y]) box_wh = box_xy1 - box_xy0 - middle_box = patches.Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') - ax.text(XMAX / 2, 10 + (y - 10) / 2, f'{etype}.{ename.upper()}', ha='center', va='center', fontweight='bold', rotation='vertical') - ax.add_patch(middle_box) - ax.plot([0, 300], [0, 0], color='gray', lw=7) - - ax.plot([-5] * 2, [5, y - 5], color='k', lw=7) - ax.text(25, y, buses[0].upper(), ha='left') - if len(buses) > 1: - ax.plot([XMAX + 5] * 2, [5, y - 5], color='k', lw=7) - ax.text(XMAX - 25, y, buses[1].upper(), ha='right') - + voltage = (quantity == 'Voltages') if quantity == 'Powers': @@ -1830,7 +1829,6 @@ def dss_visualize_plot(self, values = asarray(element.Currents).view(dtype=complex) unit = 'A' - ax.set_title(f'{etype}.{ename.upper()} {quantity} ({unit})') size = 'x-small' def _get_text(): @@ -1844,6 +1842,23 @@ def _get_text(): return arrow_text + + fig, ax = plt.subplots(1, gridspec_kw=dict(left=0.05, right=0.95, bottom=0.05, top=0.92))#, figsize=(8.6, 7)) + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + ax.grid(False) + middle_box = patches.Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') + ax.text(XMAX / 2, 10 + (y - 10) / 2, f'{etype}.{ename.upper()}', ha='center', va='center', fontweight='bold', rotation='vertical') + ax.add_patch(middle_box) + ax.plot([0, 300], [0, 0], color='gray', lw=7) + + ax.plot([-5] * 2, [5, y - 5], color='k', lw=7) + ax.text(25, y, buses[0].upper(), ha='left') + if len(buses) > 1: + ax.plot([XMAX + 5] * 2, [5, y - 5], color='k', lw=7) + ax.text(XMAX - 25, y, buses[1].upper(), ha='right') + ax.set_title(f'{etype}.{ename.upper()} {quantity} ({unit})') + for bus_idx, vbase in enumerate(vbases): for cond in range(nconds): if cond < (nconds - 1): From 98ef86ee7e9d6ffc54115df529a49a70d7767134 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:17:19 -0300 Subject: [PATCH 59/82] UserModels: default to AltDSS as engine --- docs/examples/UserModels/PyIndMach012/README.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/UserModels/PyIndMach012/README.ipynb b/docs/examples/UserModels/PyIndMach012/README.ipynb index 6acde216..9c73f2f3 100644 --- a/docs/examples/UserModels/PyIndMach012/README.ipynb +++ b/docs/examples/UserModels/PyIndMach012/README.ipynb @@ -176,8 +176,8 @@ "outputs": [], "source": [ "DSS_ENGINE = 'ALT'\n", - "DSS_ENGINE = 'COM'\n", - "DSS_ENGINE = 'ODD.DLL'\n", + "# DSS_ENGINE = 'COM'\n", + "# DSS_ENGINE = 'ODD.DLL'\n", "\n", "original_dir = os.getcwd() # same the original working directory since the COM/ODD.DLL module messes with it\n", "if DSS_ENGINE == 'COM':\n", From d79a403a00e0520d5b02038267c34c83ea31c3f5 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:19:35 -0300 Subject: [PATCH 60/82] Tests/save_outputs: remove trailing spaces from XML filenames Some versions of OpenDSS include those. --- tests/save_outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 50b06c20..5b60612f 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -526,7 +526,7 @@ def get_archive_fn(live_fn, fn_prefix=None): if org_fn in cimxml_test_filenames: DSS.Text.Command = 'export cim100' - xml_live_fns = [DSS.Text.Result] + xml_live_fns = [DSS.Text.Result.strip()] DSS.Text.Command = 'export cim100fragments' xml_live_fns.extend(glob(DSS.Text.Result + '_*.xml')) for xml_live_fn in xml_live_fns: From ee483a18abc881ac3b893ac4ffccc3bfa0df667f Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:48:45 -0300 Subject: [PATCH 61/82] (WIP) pyproject.toml: Use NumPy 2, restricting Python to 3.11+ --- pyproject.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6a38eb75..df1d4b94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,10 +25,10 @@ name = "dss-python" dynamic = ["version"] dependencies = [ "dss_python_backend==0.14.6a1", - "numpy>=1.21.0", + "numpy>=2,<3", "typing_extensions>=4.5,<5", ] -requires-python = ">=3.7" +requires-python = ">=3.11" # Following NumPy's requirements authors = [ {name = "Paulo Meira", email = "pmeira@ieee.org"}, {name = "Dheepak Krishnamurthy", email = "me@kdheepak.com"}, @@ -37,7 +37,7 @@ maintainers = [ {name = "Paulo Meira", email = "pmeira@ieee.org"}, ] description = "Python interface (bindings and tools) for OpenDSS. Based on the AltDSS/DSS C-API project, the alternative OpenDSS implementation from DSS-Extensions.org. Multiplatform, API-compatible/drop-in replacement for the COM version of OpenDSS." -readme = "README.md" +readme = {file = "README.md", content-type = "text/markdown"} license = {file = "LICENSE"} keywords = ["opendss", "altdss", "electric power systems", "opendssdirect", "powerflow", "short-circuit", ] classifiers = [ @@ -50,7 +50,6 @@ classifiers = [ 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', 'Development Status :: 5 - Production/Stable', 'Topic :: Scientific/Engineering', 'License :: OSI Approved :: BSD License' From efa4c6b4a4e68551ee835fd8c8343b3f6824a216 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:39:13 -0300 Subject: [PATCH 62/82] Add shortcut to the Settings interface. --- dss/IDSS.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dss/IDSS.py b/dss/IDSS.py index 04c4c04b..5b97da53 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -14,6 +14,7 @@ from .IDSS_Executive import IDSS_Executive from .IDSSEvents import IDSSEvents from .IParser import IParser +from .ISettings import ISettings from .IYMatrix import IYMatrix from .IZIP import IZIP @@ -33,7 +34,8 @@ class IDSS(Base): Main OpenDSS interface. Organizes the subclasses trying to mimic the `OpenDSSengine.DSS` object as seen from `win32com.client` or `comtypes.client`. - This main class also includes some global settings. See more settings in `ActiveCircuit.Settings`. + This main class also includes some global settings. Most settings at being moved to the dediced + `Settings` interface, exposed in the shortcut `Settings` of this object, or `ActiveCircuit.Settings`. ''' __slots__ = [ 'ActiveCircuit', @@ -603,3 +605,9 @@ def ShareGeneral(self, otherContext: IDSS, skip_cmds: Optional[List[str]] = None if skip_file_regexp is not None: otherContext.ActiveCircuit.Settings.SkipFileRegExp = skip_file_regexp + @property + def Settings(self): + ''' + For convenience, a shortcut to `ActiveCircuit.Settings`. + ''' + return self.ActiveCircuit.Settings From 14965c63569f5f059db57dab4a8736a76483c049 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 5 Dec 2025 23:32:40 -0300 Subject: [PATCH 63/82] Settings: Fix docstring --- dss/ISettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dss/ISettings.py b/dss/ISettings.py index 6473c922..c826ae60 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -644,7 +644,7 @@ def AllowEditor(self, value: bool): @property def PreserveCase(self) -> bool: ''' - Gets/sets whether running the engine try to preserve original names + Gets/sets whether the engine tries to preserve original names When enabled, bus and element names in many of the API functions, reports and exports are kept as provided by the user, without applying lower or upper case From 80874ec79375becd26a0a1bc8936c5be0e968719 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:36:14 -0300 Subject: [PATCH 64/82] Plot/Profile: always check if the meter is enabled Not required by default, but users can toggle iterating through disabled elements, etc. --- dss/plot.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index 90e99f80..77eb7908 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -946,6 +946,8 @@ def _get_branch_data(self, max_currents = {} elem = DSS.ActiveCircuit.ActiveCktElement for _ in DSS.ActiveCircuit.PDElements: + if not elem.Enabled: + continue currents = np.abs(asarray(elem.Currents).view(dtype=complex)) max_current = np.max(currents[:elem.NumConductors]) norm_amps = elem.NormalAmps @@ -958,6 +960,9 @@ def _get_branch_data(self, max_currents = {} elem = DSS.ActiveCircuit.ActiveCktElement for _ in DSS.ActiveCircuit.PDElements: + if not elem.Enabled: + max_currents[elem.Name] = np.nan + continue currents = np.abs(asarray(elem.Currents).view(dtype=complex)) max_current = np.max(currents[:elem.NumConductors]) norm_amps = elem.NormalAmps @@ -1059,7 +1064,14 @@ def _get_branch_data(self, for i, l in enumerate(branch_objects): if i in skip: continue - + + lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style + + if not elem.Enabled: + lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style + offset += 1 + continue + if do_values == pqPower: values[offset] = np.abs(element.TotalPowers[0]) elif do_values == pqLosses: @@ -1083,9 +1095,7 @@ def _get_branch_data(self, elif do_values == pqCurrent: values[offset] = max_currents.get(element.Name, np.NaN) elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.NaN) - - lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style + values[offset] = capacities.get(element.Name, np.NaN) offset += 1 return [lines[:offset], values[:offset], lines_styles[:offset]] + extra @@ -1132,8 +1142,12 @@ def _get_point_data(self, for i, _ in enumerate(point_objects): if i in skip: continue - - values[offset] = np.abs(element.TotalPowers[0]) + + if elem.Enabled: + values[offset] = np.abs(element.TotalPowers[0]) + else: + values[offset] = np.nan + offset += 1 return points[:offset], values[:offset] @@ -1187,6 +1201,9 @@ def dss_profile_plot(self, phases = [phases] for em in DSS.ActiveCircuit.Meters: + if not DSS.ActiveCircuit.ActiveCktElement.Enabled: + continue + branch_names = em.AllBranchesInZone br: str for br in branch_names: @@ -1312,6 +1329,7 @@ def _get_gic_line_data_altdss( # GIC lines are not exposed nicely in the classic API, so we'll use the new Obj API for gic_line in altdss.GICLine: + TODO if not gic_line.enabled: continue From e97c0a9bb287bc9b2bef8fa126fc8724bbbb3bd5 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:50:31 -0300 Subject: [PATCH 65/82] Fuses: update to match OpenDSS v11. --- dss/IFuses.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/dss/IFuses.py b/dss/IFuses.py index 9452074b..279981c4 100644 --- a/dss/IFuses.py +++ b/dss/IFuses.py @@ -3,6 +3,7 @@ # Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import List, AnyStr +import warnings class IFuses(Iterable): __slots__ = [] @@ -109,9 +110,11 @@ def NumPhases(self) -> int: @property def RatedCurrent(self) -> float: ''' - Multiplier or actual amps for the TCCcurve object. Defaults to 1.0. + Fuse continuous rated current in Amps. Defaults to 0. + + Not used internally for either power flow or reporting. - Multiply current values of TCC curve by this to get actual amps. + **NOTE:** *previous* to OpenDSS v11, or AltDSS engine/DSS C-API v0.15, this used to be the multiplier for the TCC curve. Original COM help: https://opendss.epri.com/RatedCurrent.html ''' @@ -119,6 +122,8 @@ def RatedCurrent(self) -> float: @RatedCurrent.setter def RatedCurrent(self, Value: float): + #TODO: suppress warning on older engines? + warnings.warn("RatedCurrent is not used internally by the DSS engine anymore since OpenDSS v11 and AltDSS engine v0.15. Please see `CurveMultiplier`.", UserWarning, stacklevel=2) self._lib.Fuses_Set_RatedCurrent(Value) @property @@ -186,3 +191,34 @@ def NormalState(self) -> List[str]: @NormalState.setter def NormalState(self, Value: List[AnyStr]): self._set_string_array(self._lib.Fuses_Set_NormalState, Value) + + @property + def CurveMultiplier(self) -> float: + ''' + Multiplier or actual amps for the TCCcurve object. Defaults to 1.0. + + Multiply current values of TCC curve by this to get actual amps. + + *New in OpenDSS engine v11, AltDSS engine v0.15.* + ''' + return self._lib.Fuses_Get_CurveMultiplier() + + @CurveMultiplier.setter + def CurveMultiplier(self, Value: float): + self._lib.Fuses_Set_CurveMultiplier(Value) + + @property + def InterruptingRating(self) -> float: + ''' + Fuse rated interrupting current in Amps. Defaults to 0. + + Not used internally for either power flow or reporting. + + *New in OpenDSS engine v11, AltDSS engine v0.15.* + ''' + return self._lib.Fuses_Get_InterruptingRating() + + @InterruptingRating.setter + def InterruptingRating(self, Value: float): + self._lib.Fuses_Set_InterruptingRating(Value) + From 699c528e7915baa1009529ec6afb1c21e9a2d5c8 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:07:57 -0300 Subject: [PATCH 66/82] Docs: Replace "official" with "EPRI's". --- README.md | 4 ++-- docs/changelog.md | 8 ++++---- docs/examples/GettingStarted.ipynb | 6 +++--- docs/examples/JSON.ipynb | 2 +- docs/examples/Plotting.ipynb | 2 +- docs/index.md | 2 +- dss/IDSS.py | 6 +++--- dss/IError.py | 2 +- dss/ISettings.py | 6 +++--- dss/IYMatrix.py | 2 +- dss/Oddie.py | 2 +- tests/compare_outputs.py | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5eab4b68..b273ee79 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # DSS-Python: Extended bindings for an alternative implementation of EPRI's OpenDSS -Python bindings and misc tools for using our to [our customized/alternative implementation](https://github.com/dss-extensions/dss_capi) of [OpenDSS](http://smartgrid.epri.com/SimulationTool.aspx), AltDSS/DSS C-API library. OpenDSS is an open-source electric power distribution system simulator [distributed by EPRI](https://sourceforge.net/p/electricdss/). Based on DSS C-API, CFFI and NumPy, aiming for enhanced performance and full compatibility with the official COM object API on Windows, Linux and macOS. Support includes Intel-based (x86 and x64) processors, as well as ARM processors for Linux (including Raspberry Pi devices) and macOS (including Apple M1 and later). +Python bindings and misc tools for using our to [our customized/alternative implementation](https://github.com/dss-extensions/dss_capi) of [OpenDSS](http://smartgrid.epri.com/SimulationTool.aspx), AltDSS/DSS C-API library. OpenDSS is an open-source electric power distribution system simulator [distributed by EPRI](https://sourceforge.net/p/electricdss/). Based on DSS C-API, CFFI and NumPy, aiming for enhanced performance and full compatibility with EPRI's OpenDSS COM object API on Windows, Linux and macOS. Support includes Intel-based (x86 and x64) processors, as well as ARM processors for Linux (including Raspberry Pi devices) and macOS (including Apple M1 and later). More context about this project and its components (including alternatives in [Julia](https://dss-extensions.org/OpenDSSDirect.jl/latest/), [MATLAB](https://github.com/dss-extensions/dss_matlab/), C++, [C#/.NET](https://github.com/dss-extensions/dss_sharp/), [Go](https://github.com/dss-extensions/AltDSS-Go/), and [Rust](https://github.com/dss-extensions/AltDSS-Rust/)), please check [https://dss-extensions.org/](https://dss-extensions.org/) and our hub repository at [dss-extensions/dss-extensions](https://github.com/dss-extensions/dss-extensions) for more documentation, discussions and the [FAQ](https://dss-extensions.org/faq.html). @@ -136,7 +136,7 @@ Since the DLL is built using the Free Pascal compiler, which is not officially s The validation scripts is `tests/validation.py` and requires the same folder structure as the building process. You need `win32com` to run it on Windows. -As of version 0.11, the full validation suite can be run on the three supported platforms. This is possible by saving the official COM DLL output and loading it on macOS and Linux. We hope to fully automate this validation in the future. +As of version 0.11, the full validation suite can be run on the three supported platforms. This is possible by saving EPRI's OpenDSS COM DLL output and loading it on macOS and Linux. We hope to fully automate this validation in the future. ## Roadmap: docs and interactive features diff --git a/docs/changelog.md b/docs/changelog.md index f14e3c7e..7050adc3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -229,7 +229,7 @@ Released on 2023-03-28. - `Bus_Get_ZSC012Matrix`: check for nulls - `Bus_Get_AllPCEatBus`, `Bus_Get_AllPDEatBus`: faster implementations - `Meters_Get_CountBranches`: reimplemented - - `Monitors_Get_dblHour`: For harmonics solution, return empty array. Previously, it was returning a large array instead of a single element (`[0]`) array. A small issue adjusted for compatibility with the official COM API results. + - `Monitors_Get_dblHour`: For harmonics solution, return empty array. Previously, it was returning a large array instead of a single element (`[0]`) array. A small issue adjusted for compatibility with EPRI's OpenDSS COM API results. - `Reactors_Set_Bus1`: Match the side-effects of the property API for two-terminal reactors. - New `DSS_Set_CompatFlags`/`DSS_Get_CompatFlags` function pair: introduced to address some current and potential future concerns about compatibility of results with EPRI's OpenDSS. See the API docs for more info. - New `DSS_Set_EnableArrayDimensions`/`DSS_Get_EnableArrayDimensions`: for Array results in the API, implement optional matrix sizes; when setting `DSS_Set_EnableArrayDimensions(true)`, the array size pointer will be filled with two extra elements to represent the matrix size (if the data is a matrix instead of a plain vector). For complex number, the dimensions are filled in relation to complex elements instead of double/float64 elements even though we currently reuse the double/float64 array interface. Issue: https://github.com/dss-extensions/dss_capi/issues/113 @@ -295,7 +295,7 @@ This version still maintains basic compatibility with the 0.10.x series of relea For example, consider the function Loads_Get_ZIPV. If there is no active circuit or active load element: - In the disabled state (COMErrorResults=False), the function will return "[]", an array with 0 elements. - - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should be compatible with the return value of the official COM interface. + - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should be compatible with the return value of EPRI's OpenDSS COM interface. Defaults to True/1 (enabled state) in the v0.12.x series. This will change to false in future series. @@ -325,7 +325,7 @@ Released on 2020-12-29. - Maintenance release. - Updated to DSS C-API 0.10.7, which includes most changes up to OpenDSS v9.1.3.4. - Includes an important bug fix related to the `CapRadius` DSS property. If your DSS scripts included the pattern `GMRac=... rad=...` or `GMRac=... diam=...` (in this order and without specifying `CapRadius`), you should upgrade and re-evaluate the results. -- New API properties ported from the official COM interface: `Bus.AllPCEatBus`, `Bus.AllPDEatBus`, `CktElement.TotalPowers`, `Meters.ZonePCE` +- New API properties ported from EPRI's OpenDSS COM interface: `Bus.AllPCEatBus`, `Bus.AllPDEatBus`, `CktElement.TotalPowers`, `Meters.ZonePCE` DSS C-API 0.10.7 changes: @@ -362,7 +362,7 @@ DSS C-API 0.10.6 changes: - The releases now include both the optimized/default binary and a non-optimized/debug version. See the [Debugging](https://github.com/dss-extensions/dss_capi/blob/0.10.x/docs/debug.md) document for more. - Extended API validation and **Extended Errors** mechanism: - The whole API was reviewed to add basic checks for active circuit and element access. - - By default, invalid accesses now result in errors reported through the Error interface. This can be disabled to achieve the previous behavior, more compatible with the official COM implementation — that is, ignore the error, just return a default/invalid value and assume the user has handled it. + - By default, invalid accesses now result in errors reported through the Error interface. This can be disabled to achieve the previous behavior, more compatible with EPRI's OpenDSS COM implementation — that is, ignore the error, just return a default/invalid value and assume the user has handled it. - The mechanism can be toggled by API functions `DSS_Set_ExtendedErrors` and `DSS_Get_ExtendedErrors`, or environment variable `DSS_CAPI_EXTENDED_ERRORS=0` to disable (defaults to enabled state). - New **Legacy Models** mechanism: - OpenDSS 9.0+ dropped the old `PVsystem`, `Storage`, `InvControl`, and `StorageController` models, replacing with the new versions previously known as `PVsystem2`, `Storage2`, `InvControl2` and `StorageController2`. diff --git a/docs/examples/GettingStarted.ipynb b/docs/examples/GettingStarted.ipynb index 60288f8b..a99fbcbe 100644 --- a/docs/examples/GettingStarted.ipynb +++ b/docs/examples/GettingStarted.ipynb @@ -82,7 +82,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For a comparison of the general Python-level API, including a list of our extra functions, please check [DSS-Extensions — OpenDSS: Overview of Python APIs](https://github.com/dss-extensions/dss-extensions/blob/main/docs/python_apis.md). That documents introduces and compares DSS-Python, OpenDSSDirect.py, and the official COM implementation." + "For a comparison of the general Python-level API, including a list of our extra functions, please check [DSS-Extensions — OpenDSS: Overview of Python APIs](https://github.com/dss-extensions/dss-extensions/blob/main/docs/python_apis.md). That documents introduces and compares DSS-Python, OpenDSSDirect.py, and EPRI's OpenDSS COM implementation." ] }, { @@ -108,7 +108,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The package exposes the lower level API functions from AltDSS/DSS C-API mimicking the organization and behavior of the official COM implementation, as used in Python. This allows an easier migration, or even toggling which interface is used if the user avoids using API Extensions (which are marked as such in the documentation)." + "The package exposes the lower level API functions from AltDSS/DSS C-API mimicking the organization and behavior of EPRI's OpenDSS COM implementation, as used in Python. This allows an easier migration, or even toggling which interface is used if the user avoids using API Extensions (which are marked as such in the documentation)." ] }, { @@ -149,7 +149,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "After a DSS circuit is loaded, the interaction can be done as with the official COM module:" + "After a DSS circuit is loaded, the interaction can be done as with EPRI's OpenDSS COM module:" ] }, { diff --git a/docs/examples/JSON.ipynb b/docs/examples/JSON.ipynb index e5f05ccf..64feed16 100644 --- a/docs/examples/JSON.ipynb +++ b/docs/examples/JSON.ipynb @@ -84,7 +84,7 @@ "\n", "Our alternative OpenDSS engine used in DSS-Extensions, implemented in the DSS C-API library, extends the API to provide some JSON export functions since version 0.12.0. Provided some constraints, most classes and properties could be exported as JSON, but a few properties didn't work correctly even with these contraints. **Warning: This feature is an API Extension and is not available in the official OpenDSS.**\n", "\n", - "Note that manually creating JSON through the classic DSS API (as implemented in the official COM DLL API) is possible but cumbersome, requiring manually tracking and converting strings to the types and several other details. We expect the DSS C-API implementation, being shared across all DSS-Extensions, can achieve better performance and remove this extra processing work.\n", + "Note that manually creating JSON through the classic DSS API (as implemented in EPRI's OpenDSS COM DLL API) is possible but cumbersome, requiring manually tracking and converting strings to the types and several other details. We expect the DSS C-API implementation, being shared across all DSS-Extensions, can achieve better performance and remove this extra processing work.\n", "\n", "We will use Pandas to show tables more easily in this document, but it is not required for exporting. In the near future, we plan to release integrated dataframe support for using with Pandas and similar workloads in Python and other languages.\n", "\n", diff --git a/docs/examples/Plotting.ipynb b/docs/examples/Plotting.ipynb index 7c4861ef..004e26d5 100644 --- a/docs/examples/Plotting.ipynb +++ b/docs/examples/Plotting.ipynb @@ -328,7 +328,7 @@ "\n", "Besides a few missing plots, some of the implementation ones can be polished in a future release, either by default or by toggling an option. Colormaps and colorbars are one example we should use instead of the manual colormapping in general data plots, etc.\n", "\n", - "Reading a plot from `.dsv` files created by the official OpenDSS could also be added. Using our full plotting backend with the official OpenDSS is not possible, but we could expose an user-friendly API to reuse our code with the official COM implementation." + "Reading a plot from `.dsv` files created by the official OpenDSS could also be added. Using our full plotting backend with the official OpenDSS is not possible, but we could expose an user-friendly API to reuse our code with EPRI's implementation." ] }, { diff --git a/docs/index.md b/docs/index.md index fb69bd48..c239ebc8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,7 +26,7 @@ flowchart TD -DSS-Python is one of three Python projects under DSS-Extensions. See [DSS-Extensions — OpenDSS: Overview of Python APIs](https://dss-extensions.org/python_apis.html) for a brief comparison between these and the official COM API. Both OpenDSSDirect.py and DSS-Python expose the classic OpenDSS API (closer to the COM implementation). For an alternative API which exposes all OpenDSS objects, batch operations, and a more intuitive API, check [AltDSS-Python](https://dss-extensions.org/AltDSS-Python/). If required, users can mix all three packages in the same project to access some of their unique features. +DSS-Python is one of three Python projects under DSS-Extensions. See [DSS-Extensions — OpenDSS: Overview of Python APIs](https://dss-extensions.org/python_apis.html) for a brief comparison between these and EPRI's OpenDSS COM API. Both OpenDSSDirect.py and DSS-Python expose the classic OpenDSS API (closer to the COM implementation). For an alternative API which exposes all OpenDSS objects, batch operations, and a more intuitive API, check [AltDSS-Python](https://dss-extensions.org/AltDSS-Python/). If required, users can mix all three packages in the same project to access some of their unique features. ## Brief introduction diff --git a/dss/IDSS.py b/dss/IDSS.py index 5b97da53..8a21488d 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -432,9 +432,9 @@ def COMErrorResults(self) -> bool: For example, consider the function `Loads_Get_ZIPV`. If there is no active circuit or active load element: - - In the disabled state (COMErrorResults=False), the function will return "[]", an array with 0 elements. - - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should - be compatible with the return value of the official COM interface. + - In the disabled state (`COMErrorResults`=False), the function will return "[]", an array with 0 elements. + - In the enabled state (`COMErrorResults`=True), the function will return "[0.0]" instead. This should + be compatible with the return value of EPRI's OpenDSS COM interface. Defaults to false (disabled state) in AltDSS since the v0.15.x series. diff --git a/dss/IError.py b/dss/IError.py index a47a3844..49a4d5f5 100644 --- a/dss/IError.py +++ b/dss/IError.py @@ -50,7 +50,7 @@ def ExtendedErrors(self) -> bool: Extended errors are errors derived from checks across the API to ensure a valid state. Although many of these checks are already present in the - original/official COM interface, the checks do not produce any error + original/EPRI's COM interface, the checks do not produce any error message. An error value can be returned by a function but this value can, for many of the functions, be a valid value. As such, the user has no means to detect an invalid API call. diff --git a/dss/ISettings.py b/dss/ISettings.py index c826ae60..1766ef7f 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -565,9 +565,9 @@ def COMErrorResults(self) -> bool: For example, consider the property `Loads.ZIPV`. If there is no active circuit or active load element: - - In the disabled state (COMErrorResults=False), the function will return "[]", an array with 0 elements. - - In the enabled state (COMErrorResults=True), the function will return "[0.0]" instead. This should - be compatible with the return value of the official COM interface. + - In the disabled state (`COMErrorResults`=False), the function will return "[]", an array with 0 elements. + - In the enabled state (`COMErrorResults`=True), the function will return "[0.0]" instead. This should + be compatible with the return value of EPRI's OpenDSS COM interface. Defaults to false (disabled state) in AltDSS since the v0.15.x series. diff --git a/dss/IYMatrix.py b/dss/IYMatrix.py index 4b61072d..4babc70e 100644 --- a/dss/IYMatrix.py +++ b/dss/IYMatrix.py @@ -11,7 +11,7 @@ class IYMatrix(Base): YMatrix provides access to some lower-level solution aspects. Part of this class is ported from the original OpenDSSDirect.DLL back in 2017, but - part is new. Since this is not exposed in the official COM API, it is marked as an extension. + part is new. Since this is not exposed in EPRI's OpenDSS COM API, it is marked as an extension. (**API Extension**) ''' diff --git a/dss/Oddie.py b/dss/Oddie.py index afeb1c95..311da700 100644 --- a/dss/Oddie.py +++ b/dss/Oddie.py @@ -13,7 +13,7 @@ class IOddieDSS(IDSS): r''' The OddieDSS class exposes the official OpenDSSDirect.DLL binary, as distributed by EPRI, with the same API as the DSS-Python and - the official COM interface object on Windows. It uses AltDSS Oddie + EPRI's OpenDSS COM interface object on Windows. It uses AltDSS Oddie to achieve this. **Note:** This class requires the backend for Oddie to be included in diff --git a/tests/compare_outputs.py b/tests/compare_outputs.py index f4f822cd..87ea3556 100644 --- a/tests/compare_outputs.py +++ b/tests/compare_outputs.py @@ -30,7 +30,7 @@ class MISSING: ENABLE_JSON = True KNOWN_COM_DIFF = set([ - # On official COM, uninitialized values for CalcCurrent, AllocFactors + # On EPRI's OpenDSS COM, uninitialized values for CalcCurrent, AllocFactors # Note that this could be a bug on the upstream version, but debugging without Delphi gets tricky *[('Version8/Distrib/Examples/DOCTechNote/1_2.dss.json', 'Meters', 'records', x, 'CalcCurrent') for x in range(77)], *[('Version8/Distrib/Examples/DOCTechNote/2_1.dss.json', 'Meters', 'records', x, 'CalcCurrent') for x in range(77)], From 0cbefa475e74c60e0619d47cd9bb9fbd6489ec22 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:49:34 -0300 Subject: [PATCH 67/82] Tests: add `test_share_general` ; update some other tests. --- tests/test_ctrlqueue.py | 53 ++++++++++++++++++++++++++------------- tests/test_general.py | 27 ++++++++++++++++++-- tests/test_past_issues.py | 9 ++++--- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/tests/test_ctrlqueue.py b/tests/test_ctrlqueue.py index 544f3a5b..451640cc 100644 --- a/tests/test_ctrlqueue.py +++ b/tests/test_ctrlqueue.py @@ -81,16 +81,25 @@ def test_ctrlqueue(): i = 0 - # Values updated for OpenDSS 10.1, which include the change to reset YPrimInvalid to false v_step_up = [ - 119.26520933058164, - 118.53391703558978, - 119.00162278912609, - 118.22495566279100, - 118.66307559565404, - 119.11533205526253, - 118.26023037859353, + 119.26520933058164, + 118.53391703558978, + 119.00110473442648, + 118.22480242913166, + 118.66765922692467, + 119.1133497058388, + 118.25980207026679, ] + # # Values below were updated for OpenDSS 10.1, which included the change to reset YPrimInvalid to false + # v_step_up = [ + # 119.26520933058164, + # 118.53391703558978, + # 119.00162278912609, + # 118.22495566279100, + # 118.66307559565404, + # 119.11533205526253, + # 118.26023037859353, + # ] while DSSCapacitors.AvailableSteps > 0: print('DSSCapacitors.AvailableSteps', DSSCapacitors.AvailableSteps) i = i + 1 @@ -130,17 +139,27 @@ def test_ctrlqueue(): print("Capacitor", DSSCapacitors.Name, "States =", tuple(DSSCapacitors.States)) - # Values updated for OpenDSS 10.1, which include the change to reset YPrimInvalid to false v_step_down = [ - 121.87640973217214, - 121.19194698692986, - 121.72771816928677, - 121.00166364015094, - 121.51952384526496, - 120.74621507897345, - 121.24285846717471, - 120.41741114839155, + 121.8764097324052, + 121.1919475267437, + 121.72822436752874, + 121.00188980140766, + 121.51826847553448, + 120.74704094567356, + 121.24330745091815, + 120.41556666716592, ] + # # Values below were updated for OpenDSS 10.1, which included the change to reset YPrimInvalid to false + # v_step_down = [ + # 121.87640973217214, + # 121.19194698692986, + # 121.72771816928677, + # 121.00166364015094, + # 121.51952384526496, + # 120.74621507897345, + # 121.24285846717471, + # 120.41741114839155, + # ] # Now let's reverse Direction and start removing steps while DSSCapacitors.AvailableSteps < DSSCapacitors.NumSteps: diff --git a/tests/test_general.py b/tests/test_general.py index b448d683..8a0d04dd 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -303,8 +303,11 @@ def test_pm_threads(): DSS.Text.Command = 'set hour=216' DSS.ActiveCircuit.Solution.SolveAll() - DSS.Text.Command = 'wait' - + Parallel.Wait() + + # DSS.Text.Command = 'solve all' + # DSS.Text.Command = 'wait' + assert tuple(Parallel.ActorStatus) == (1, 1, 1, 1) assert tuple(Parallel.ActorProgress) == (100, 100, 100, 100) t1 = perf_counter() @@ -388,6 +391,9 @@ def _run(ctx, i): def test_threading2(): DSS.AllowChangeDir = False + + # EPRITestCircuits/epri_dpv/M1 has loads with zero power + DSS.ActiveCircuit.Settings.CompatFlags |= DSSCompatFlags.PermissiveProperties fns = [ f"{BASE_DIR}/Version8/Distrib/EPRITestCircuits/epri_dpv/M1/Master_NoPV.dss", @@ -1093,6 +1099,23 @@ def test_settings_context(): assert isinstance(DSS.ActiveCircuit.AllBusVmag, np.ndarray) +def test_share_general(): + DSS2 = DSS.NewContext() + DSS('new loadshape.sharedloadshape npts=4 pmult=[1, 2, 3, 4]') + DSS.ShareGeneral(DSS2) + DSS.NewCircuit('test1') + DSS2.NewCircuit('test2') + + assert 'sharedloadshape' in DSS.ActiveCircuit.LoadShapes.AllNames + assert 'sharedloadshape' in DSS2.ActiveCircuit.LoadShapes.AllNames + + LS = DSS.ActiveCircuit.LoadShapes + LS.Name = 'sharedloadshape' + + LS2 = DSS2.ActiveCircuit.LoadShapes + LS2.Name = 'sharedloadshape' + assert list(LS2.Pmult) == list(LS.Pmult) + assert list(LS2.Pmult) == [1., 2., 3., 4.] if __name__ == '__main__': diff --git a/tests/test_past_issues.py b/tests/test_past_issues.py index 928f10a8..cf1b9851 100644 --- a/tests/test_past_issues.py +++ b/tests/test_past_issues.py @@ -74,7 +74,7 @@ def test_create_with_circuit(): for cls in DSS.Classes: DSS.ClearAll() DSS.NewCircuit(f'test_{cls}') - if cls in ('CapControl', 'RegControl', 'GenDispatcher', 'StorageController', 'Relay', 'Fuse', 'SwtControl', 'ESPVLControl', 'GICsource', 'FMonitor'): + if cls in ('CapControl', 'RegControl', 'GenDispatcher', 'StorageController', 'Relay', 'Fuse', 'SwtControl', 'ESPVLControl', 'GICsource', 'FMonitor', 'Generic5'): with pytest.raises(DSSException): DSS.Text.Command = f'new {cls}.test{cls}' @@ -86,8 +86,10 @@ def test_create_with_circuit(): DSS.Text.Command = f'new {cls}.test{cls}2 element=transformer.testtr capacitor=testcap' elif cls == 'GenDispatcher': DSS.Text.Command = f'new {cls}.test{cls}2 element=transformer.testtr' - elif cls == 'FMonitor': - DSS.Text.Command = f'new {cls}.test{cls}2 element=transformer.testtr' + elif cls in ('FMonitor', 'Generic5'): + # Skip these for now... they are disabled by default + # DSS.Text.Command = f'new {cls}.test{cls}2 element=transformer.testtr' + pass else: DSS.Text.Command = f'new {cls}.test{cls}' @@ -101,3 +103,4 @@ def test_ymatrix_csc(): DSS.ActiveCircuit.Solution.Solve() DSS.ActiveCircuit.Settings.AdvancedTypes = True assert np.all(DSS.ActiveCircuit.SystemY == sp.csc_matrix(DSS.YMatrix.GetCompressedYMatrix())) + From 0198c6f1ec67f792fb442b4d78162431051b39f3 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:51:58 -0300 Subject: [PATCH 68/82] Some docstrings and misc updates to reexpose functions to AltDSS-Python Fully rename CffiApiUtil to AltDSSAPIUtil --- dss/IDSS.py | 10 ++--- dss/ISettings.py | 4 +- dss/Oddie.py | 4 +- dss/__init__.py | 6 +-- dss/_cffi_api_util.py | 98 +++++++++++++++++++++---------------------- dss/plot.py | 2 +- dss/plot2.py | 2 +- 7 files changed, 63 insertions(+), 63 deletions(-) diff --git a/dss/IDSS.py b/dss/IDSS.py index 8a21488d..524dfa5a 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -5,7 +5,7 @@ import warnings from weakref import WeakKeyDictionary from typing import Any, List, Union, AnyStr, TYPE_CHECKING -from ._cffi_api_util import Base, CffiApiUtil, DSSException +from ._cffi_api_util import Base, AltDSSAPIUtil, DSSException from .ICircuit import ICircuit from .IError import IError from .IText import IText @@ -79,7 +79,7 @@ class IDSS(Base): ZIP: IZIP @classmethod - def _get_instance(cls: IDSS, api_util: CffiApiUtil = None, ctx=None) -> IDSS: + def _get_instance(cls: IDSS, api_util: AltDSSAPIUtil = None, ctx=None) -> IDSS: ''' If there is an existing instance for a DSSContext, returns it. Otherwise, tries to wrap the context into a new DSS-Python API instance. @@ -87,7 +87,7 @@ def _get_instance(cls: IDSS, api_util: CffiApiUtil = None, ctx=None) -> IDSS: if api_util is None: # If none exists, something is probably wrong elsewhere, # so let's allow the IndexError to propagate - api_util = CffiApiUtil._ctx_to_util[ctx] + api_util = AltDSSAPIUtil._ctx_to_util[ctx] dss = cls._ctx_to_dss.get(api_util.ctx) if dss is None: @@ -410,7 +410,7 @@ def AllowDOScmd(self) -> bool: Defaults to False/0 (disabled state). Users should consider DOScmd deprecated on DSS-Extensions. - This can also be set through the environment variable DSS_CAPI_ALLOW_DOSCMD. Setting it to 1 enables + This can also be set through the environment variable `DSS_CAPI_ALLOW_DOSCMD`. Setting it to 1 enables the command. **Deprecated:** Use `Settings.AllowDOScmd` instead (same behavior, the setting was just moved there for better organization). @@ -472,7 +472,7 @@ def NewContext(self) -> IDSS: ffi = self._api_util.ffi lib = self._api_util.lib_unpatched new_ctx = ffi.gc(lib.ctx_New(), lib.ctx_Dispose) - new_api_util = CffiApiUtil(ffi, lib, new_ctx, parent=self._api_util) + new_api_util = AltDSSAPIUtil(ffi, lib, new_ctx, parent=self._api_util) return IDSS(new_api_util) def __call__(self, cmds: Union[AnyStr, List[AnyStr]]): diff --git a/dss/ISettings.py b/dss/ISettings.py index 1766ef7f..a613af4d 100644 --- a/dss/ISettings.py +++ b/dss/ISettings.py @@ -591,7 +591,7 @@ def AllowDOScmd(self) -> bool: Defaults to False/0 (disabled state). Users should consider DOScmd deprecated on DSS-Extensions. - This can also be set through the environment variable DSS_CAPI_ALLOW_DOSCMD. Setting it to 1 enables + This can also be set through the environment variable `DSS_CAPI_ALLOW_DOSCMD`. Setting it to 1 enables the command. **(API Extension)** @@ -613,7 +613,7 @@ def AllowChangeDir(self) -> bool: Defaults to True (allow changes, backwards compatible) in the 0.10.x versions of DSS C-API. This might change to False in future versions. - This can also be set through the environment variable DSS_CAPI_ALLOW_CHANGE_DIR. Set it to 0 to + This can also be set through the environment variable `DSS_CAPI_ALLOW_CHANGE_DIR`. Set it to 0 to disallow changing the active working directory. **(API Extension)** diff --git a/dss/Oddie.py b/dss/Oddie.py index 311da700..fc3a0b60 100644 --- a/dss/Oddie.py +++ b/dss/Oddie.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys, platform, ctypes, os from typing import Optional -from ._cffi_api_util import CffiApiUtil +from ._cffi_api_util import AltDSSAPIUtil from .IDSS import IDSS from enum import Flag @@ -137,7 +137,7 @@ def __init__(self, library_path: str = '', load_flags: Optional[int] = None, odd lib.Oddie_SetOptions(oddie_options) ctx = ffi.gc(ctx, lib.ctx_Dispose) - api_util = CffiApiUtil(ffi, lib, ctx, is_oddie=True) + api_util = AltDSSAPIUtil(ffi, lib, ctx, is_oddie=True) api_util._library_path = library_path IDSS.__init__(self, api_util) diff --git a/dss/__init__.py b/dss/__init__.py index 65124e6c..e14f51d8 100644 --- a/dss/__init__.py +++ b/dss/__init__.py @@ -16,7 +16,7 @@ if os.path.exists(_properties_mo): lib.DSS_SetPropertiesMO(_properties_mo.encode()) -from ._cffi_api_util import CffiApiUtil, AltDSSAPIUtil, DSSException, set_case_insensitive_attributes +from ._cffi_api_util import AltDSSAPIUtil, DSSException, set_case_insensitive_attributes from .IDSS import IDSS from .Oddie import IOddieDSS, OddieOptions from .enums import * @@ -25,11 +25,11 @@ if not hasattr(lib, 'ctx_New'): # Module was built without the context API - api_util: CffiApiUtil = CffiApiUtil(ffi, lib) #: API utility functions and low-level access to the classic API + api_util: AltDSSAPIUtil = AltDSSAPIUtil(ffi, lib) #: API utility functions and low-level access to the classic API prime_api_util = None DSS_GR: IDSS = IDSS(api_util) #: GR (Global Result) interface else: - api_util = prime_api_util = CffiApiUtil(ffi, lib, lib.ctx_Get_Prime()) #: API utility functions and low-level access for DSSContext API + api_util = prime_api_util = AltDSSAPIUtil(ffi, lib, lib.ctx_Get_Prime()) #: API utility functions and low-level access for DSSContext API DSS_GR: IDSS = IDSS(prime_api_util) #: GR (Global Result) interface using the new DSSContext API DSS_IR: IDSS = DSS_GR #: IR was removed in DSS-Python v0.13.x, we'll keep mapping it to DSS_GR for this version diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index c9eb0cda..a4046c95 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -285,19 +285,19 @@ def get_int32_gr_array(self) -> Int32Array: return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 4), dtype=np.int32).copy() - # def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: - # ptr = self._ffi.new('int8_t**') - # cnt = self._ffi.new('int32_t[4]') - # func(ptr, cnt, *args) - # res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() - # self.DSS_Dispose_PByte(ptr) + def get_int8_array(self, func: Callable, *args: Any) -> Int8Array: + ptr = self._ffi.new('int8_t**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + res = np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() + self.DSS_Dispose_PByte(ptr) - # if cnt[3] and (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: - # # If the last element is filled, we have a matrix. Otherwise, the - # # matrix feature is disabled or the result is indeed a vector - # return res.reshape((cnt[2], cnt[3])) + if cnt[3] and (self.settings_ptr[0] & (1 << 1)): # self.advanced_types: + # If the last element is filled, we have a matrix. Otherwise, the + # matrix feature is disabled or the result is indeed a vector + return res.reshape((cnt[2], cnt[3])) - # return res + return res def get_int8_gr_array(self) -> Int8Array: @@ -311,24 +311,24 @@ def get_int8_gr_array(self) -> Int8Array: return np.frombuffer(self._ffi.buffer(ptr[0], cnt[0] * 1), dtype=np.int8).copy() - # def get_string_array(self, func: Callable, *args: Any) -> List[str]: - # ptr = self._ffi.new('char***') - # cnt = self._ffi.new('int32_t[4]') - # func(ptr, cnt, *args) - # if not cnt[0]: - # res = [] - # else: - # actual_ptr = ptr[0] - # if actual_ptr == self._ffi.NULL: - # res = [] - # else: - # codec = self.codec - # str_ptrs = self._unpack(actual_ptr, cnt[0]) - # #res = [(str(self._ffi.string(str_ptr).decode(codec)) if (str_ptr != self._ffi.NULL) else None) for str_ptr in str_ptrs] - # res = [(self._ffi.string(str_ptr).decode(codec) if (str_ptr != self._ffi.NULL) else u'') for str_ptr in str_ptrs] + def get_string_array(self, func: Callable, *args: Any) -> List[str]: + ptr = self._ffi.new('char***') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if not cnt[0]: + res = [] + else: + actual_ptr = ptr[0] + if actual_ptr == self._ffi.NULL: + res = [] + else: + codec = self._api_util.codec + str_ptrs = self._unpack(actual_ptr, cnt[0]) + #res = [(str(self._ffi.string(str_ptr).decode(codec)) if (str_ptr != self._ffi.NULL) else None) for str_ptr in str_ptrs] + res = [(self._ffi.string(str_ptr).decode(codec) if (str_ptr != self._ffi.NULL) else u'') for str_ptr in str_ptrs] - # self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) - # return res + self.DSS_Dispose_PPAnsiChar(ptr, cnt[1]) + return res # def get_string_array2(self, func, *args): # for compatibility with OpenDSSDirect.py @@ -343,7 +343,7 @@ def get_int8_gr_array(self) -> Int8Array: # if actual_ptr == self._ffi.NULL: # res = [] # else: - # codec = self.codec + # codec = self._api_util.codec # res = [(str(self._ffi.string(actual_ptr[i]).decode(codec)) if (actual_ptr[i] != self._ffi.NULL) else '') for i in range(cnt[0])] # if res == [u'']: # # most COM methods return an empty array as an @@ -357,29 +357,29 @@ def get_int8_gr_array(self) -> Int8Array: # return res - # def get_float64_array2(self, func, *args): - # ptr = self._ffi.new('double**') - # cnt = self._ffi.new('int32_t[4]') - # func(ptr, cnt, *args) - # if not cnt[0]: - # res = [] - # else: - # res = self._unpack(ptr[0], cnt[0]) + def get_float64_array2(self, func, *args): + ptr = self._ffi.new('double**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if not cnt[0]: + res = [] + else: + res = self._unpack(ptr[0], cnt[0]) - # self.DSS_Dispose_PDouble(ptr) - # return res + self.DSS_Dispose_PDouble(ptr) + return res - # def get_int32_array2(self, func, *args): - # ptr = self._ffi.new('int32_t**') - # cnt = self._ffi.new('int32_t[4]') - # func(ptr, cnt, *args) - # if not cnt[0]: - # res = None - # else: - # res = self._unpack(ptr[0], cnt[0]) + def get_int32_array2(self, func, *args): + ptr = self._ffi.new('int32_t**') + cnt = self._ffi.new('int32_t[4]') + func(ptr, cnt, *args) + if not cnt[0]: + res = None + else: + res = self._unpack(ptr[0], cnt[0]) - # self.DSS_Dispose_PInteger(ptr) - # return res + self.DSS_Dispose_PInteger(ptr) + return res # def get_int8_array2(self, func, *args): # ptr = self._ffi.new('int8_t**') diff --git a/dss/plot.py b/dss/plot.py index 77eb7908..487bb4f9 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -11,7 +11,7 @@ from typing_extensions import TypedDict, Unpack from . import api_util from . import DSS as DSSPlotCtx -from ._cffi_api_util import CffiApiUtil +from ._cffi_api_util import AltDSSAPIUtil from .IDSS import IDSS from .IBus import IBus from ._cffi_api_util import Iterable as DSSIterable diff --git a/dss/plot2.py b/dss/plot2.py index 323a10c5..4591eeb6 100644 --- a/dss/plot2.py +++ b/dss/plot2.py @@ -11,7 +11,7 @@ from typing_extensions import TypedDict, Unpack from . import api_util from . import DSS as DSSPlotCtx -from ._cffi_api_util import CffiApiUtil +from ._cffi_api_util import AltDSSAPIUtil from .IDSS import IDSS from .IBus import IBus from ._cffi_api_util import Iterable as DSSIterable From 1d6a83b1fa46e27f39621950b5f8bfafa842fc32 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:31:29 -0300 Subject: [PATCH 69/82] Replace remaining `np.NaN` with `np.nan`. --- dss/plot.py | 10 +++++----- dss/plot2.py | 10 +++++----- tests/compare_outputs.py | 4 ++-- tests/test_general.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dss/plot.py b/dss/plot.py index 487bb4f9..4a0a6ce7 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -1020,9 +1020,9 @@ def _get_branch_data(self, values[offset] = value elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.NaN) + values[offset] = max_currents.get(element.Name, np.nan) elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.NaN) + values[offset] = capacities.get(element.Name, np.nan) offset += 1 @@ -1093,9 +1093,9 @@ def _get_branch_data(self, values[offset] = value elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.NaN) + values[offset] = max_currents.get(element.Name, np.nan) elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.NaN) + values[offset] = capacities.get(element.Name, np.nan) offset += 1 return [lines[:offset], values[:offset], lines_styles[:offset]] + extra @@ -2434,7 +2434,7 @@ def _add_line(element, color): except: pass elif quantity in (pqCurrent, pqCapacity): - lw = capacities.get(element.Name, np.NaN) + lw = capacities.get(element.Name, np.nan) if (element.NumPhases == 1): lines1.append([c1, c2]) diff --git a/dss/plot2.py b/dss/plot2.py index 4591eeb6..4a3a8f25 100644 --- a/dss/plot2.py +++ b/dss/plot2.py @@ -1049,9 +1049,9 @@ def _get_branch_data(DSS: IDSS, values[offset] = value elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.NaN) + values[offset] = max_currents.get(element.Name, np.nan) elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.NaN) + values[offset] = capacities.get(element.Name, np.nan) offset += 1 @@ -1115,9 +1115,9 @@ def _get_branch_data(DSS: IDSS, values[offset] = value elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.NaN) + values[offset] = max_currents.get(element.Name, np.nan) elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.NaN) + values[offset] = capacities.get(element.Name, np.nan) lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style offset += 1 @@ -2422,7 +2422,7 @@ def _add_line(element, color): except: pass elif quantity in (pqCurrent, pqCapacity): - lw = capacities.get(element.Name, np.NaN) + lw = capacities.get(element.Name, np.nan) if (element.NumPhases == 1): lines1.append([c1, c2]) diff --git a/tests/compare_outputs.py b/tests/compare_outputs.py index 87ea3556..da008fc5 100644 --- a/tests/compare_outputs.py +++ b/tests/compare_outputs.py @@ -310,10 +310,10 @@ def compare(self, a, b, org_path=None): if isinstance(va[0], float) or va[0] is None: if None in va: - va = [x if x is not None else np.NaN for x in va] + va = [x if x is not None else np.nan for x in va] if None in vb: - vb = [x if x is not None else np.NaN for x in vb] + vb = [x if x is not None else np.nan for x in vb] atol = tol rtol = tol diff --git a/tests/test_general.py b/tests/test_general.py index 8a0d04dd..a08845e8 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -385,7 +385,7 @@ def _run(ctx, i): np.testing.assert_allclose(v_ctx[2], v_seq[2]) np.testing.assert_allclose(v_ctx[3], v_seq[3]) else: - dt_ctx = np.NaN + dt_ctx = np.nan print(f"PM: {dt_pm:.3g} s; Python threads: {dt_ctx:.3g} s; Sequential: {dt_seq:.3g} s") From d9bc5266e84fb2cd029aba3fccdaf864560bc47c Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:46:58 -0300 Subject: [PATCH 70/82] Plot: finish general refactoring; notebook still needs to be updated. --- dss/notebook.py | 42 +- dss/plot.py | 380 ++++--- dss/plot2.py | 2698 ----------------------------------------------- 3 files changed, 238 insertions(+), 2882 deletions(-) delete mode 100644 dss/plot2.py diff --git a/dss/notebook.py b/dss/notebook.py index a9214872..eb299f99 100644 --- a/dss/notebook.py +++ b/dss/notebook.py @@ -1,4 +1,24 @@ +import os +from enum import IntEnum from .IDSS import IDSS +from . import api_util +from . import plot + +class DSSMessageType(IntEnum): + Error = -1 + General = 0 + Info = 1 + Help = 2 + Progress = 3 + ProgressCaption = 4 + ProgressFormCaption = 5 + ProgressPercent = 6 + FireOffEditor = 7 + ProgressSummary = 8 + ReportOutput = 9 + ShowOutput = 10 + ShowTreeView = 11 + try: from IPython import get_ipython @@ -24,17 +44,19 @@ def show(text): @register_cell_magic def dss(line, cell): - if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_oddie: - DSSPlotCtx.Text.Commands(cell) + if isinstance(plot.DSSPlotCtx, IDSS) and not plot.DSSPlotCtx._api_util._is_oddie: + plot.DSSPlotCtx.Text.Commands(cell) else: for line in cell.split('\n'): - DSSPlotCtx(line) - res = DSSPlotCtx.Text.Result + plot.DSSPlotCtx(line) + res = plot.DSSPlotCtx.Text.Result if res.endswith('.DSV'): if _enabled and FilePath(res).exists(): plot_dsv(res) - DSSPlotCtx.AllowChangeDir = False + if isinstance(plot.DSSPlotCtx, IDSS) and not plot.DSSPlotCtx._api_util._is_oddie: + #TODO: save original state? + plot.DSSPlotCtx.AllowChangeDir = False except: def link_file(fn): print(f'Output file: "{fn}"') @@ -64,17 +86,17 @@ def dss_python_cb_write(ctx, message_str, message_type: int, message_size: int, # DSS = _ctx2dss(ctx) message_str = api_util.ffi.string(message_str).decode(api_util.codec) - if message_type == api_util.lib.DSSMessageType_Error: + if message_type == DSSMessageType.Error: #print('DSS Error:', message_str, file=sys.stderr) pass - elif message_type in (api_util.lib.DSSMessageType_ProgressCaption, api_util.lib.DSSMessageType_ProgressFormCaption): + elif message_type in (DSSMessageType.ProgressCaption, DSSMessageType.ProgressFormCaption): #dss_progress_desc = message_str # print('Progress Caption:', message_str, file=sys.stderr) pass - elif message_type == api_util.lib.DSSMessageType_Progress: + elif message_type == DSSMessageType.Progress: #print('DSS Progress:', message_str, file=sys.stderr) pass - elif message_type == api_util.lib.DSSMessageType_FireOffEditor: + elif message_type == DSSMessageType.FireOffEditor: link_file(message_str) # try: # # print('DSSMessageType_FireOffEditor') @@ -86,7 +108,7 @@ def dss_python_cb_write(ctx, message_str, message_type: int, message_size: int, # print(f'Could not display file "{message_str}"') # return 1 - elif message_type == api_util.lib.DSSMessageType_ProgressPercent: + elif message_type == DSSMessageType.ProgressPercent: try: pass # n = int(message_str) diff --git a/dss/plot.py b/dss/plot.py index 4a0a6ce7..2f757caa 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -4,22 +4,29 @@ This is not a complete implementation and there are known limitations, but should suffice for many use-cases. We'd like to add another backend later. + +For DSS-Python v0.16, this module was refactored and improved to also allow using EPRI's +OpenDSS distributions (original Delphi OpenDSS, and OpenDSS-C), although more limited +since they do not provide the same API as the AltDSS engine. """ from __future__ import annotations import os, re, json, sys, warnings +from weakref import WeakKeyDictionary from typing import List, TYPE_CHECKING, Optional, Tuple, Dict, Union, Iterable +from enum import Enum, IntEnum +from pathlib import Path as FilePath from typing_extensions import TypedDict, Unpack + +import numpy as np +from numpy import asarray +from numpy.testing import suppress_warnings + +from dss_python_backend import loader_lib from . import api_util from . import DSS as DSSPlotCtx -from ._cffi_api_util import AltDSSAPIUtil +from ._cffi_api_util import AltDSSAPIUtil, Iterable as DSSIterable from .IDSS import IDSS from .IBus import IBus -from ._cffi_api_util import Iterable as DSSIterable -from enum import Enum, IntEnum -import numpy as np -from numpy import asarray -from numpy.testing import suppress_warnings -from pathlib import Path as FilePath try: from matplotlib import pyplot as plt from matplotlib.path import Path @@ -229,8 +236,6 @@ class PlotParams(TypedDict): MaxScale=None, ) -include_3d = '2d' # '2d' (default), '3d' (prefer 3d), 'both' - str_to_pq = { 'Voltages': pqVoltage, 'Currents': pqCurrent, @@ -410,25 +415,10 @@ def remove_nodes(bus): def _int_to_color(v: int): return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) -class ToggleAdvancedTypes: - def __init__(self, dss: IDSS, value: bool): - self._value = value - self._dss = dss - self._previous = self._dss.AdvancedTypes - - def __enter__(self): - if self._value != self._previous: - self._dss.AdvancedTypes = self._value - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if self._value != self._previous: - self._dss.AdvancedTypes = self._previous - class DSVHandler: - def __init__(self, fn: Union[str, FilePath]): + + def __init__(self, fn: Union[str, FilePath], show=True): self.fn = fn self.fig, self.ax = plt.subplots() self.ax.get_xaxis().get_major_formatter().set_scientific(False) @@ -441,7 +431,7 @@ def __init__(self, fn: Union[str, FilePath]): self.no_scales = False self.bold = True self.txt_align = 'left' - + self._do_show = show def BoldLabel(self, param_str: str): self.bold = int(param_str.strip()) != 0 @@ -664,7 +654,7 @@ def Xlabel(self, param_str: str): def Ylabel(self, param_str: str): self.ax.set_ylabel(param_str.strip().strip('"')) - def parse(self): + def parse_and_plot(self): with open(self.fn, 'r') as f: for l in f: l = l.strip() @@ -679,23 +669,32 @@ def parse(self): # print(item, repr(rest)[:100]) getattr(self, item_name)(rest[0] if rest else '') # let the exception propagate on error - if _do_show: + if self._do_show: self.fig.show() else: return self.fig, self.ax class DSSMPLPlotter: + _ctx_to_plotter = WeakKeyDictionary() + def __init__(self, dss: IDSS): + if dss._api_util.ctx not in DSSMPLPlotter._ctx_to_plotter: + DSSMPLPlotter._ctx_to_plotter[dss._api_util.ctx] = self + self.dss = dss + self._original_allow_forms = None + self._do_show = True + self._enabled = False - def dss_monitor_plot(DSS: IDSS, + def monitor(self, *, ObjectName: str = None, Channels: List[int] = None, # TODO: allow channel names too Bases: List[float] = None, **kwargs: Unpack[PlotParams] ): + DSS = self.dss monitor = DSS.ActiveCircuit.Monitors monitor.Name = ObjectName data = monitor.AsMatrix() @@ -753,7 +752,7 @@ def dss_monitor_plot(DSS: IDSS, ax.set_xlabel(xlabel) - def dss_tshape_plot(self, + def tshape(self, *, ObjectName: str = None, Color1: str = None, @@ -761,7 +760,7 @@ def dss_tshape_plot(self, ): # There is no dedicated API yet but we can move to the Obj API name = ObjectName - DSS = self.DSS + DSS = self.dss DSS.Text.Command = f'? tshape.{name}.temp' p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') try: @@ -797,7 +796,7 @@ def dss_tshape_plot(self, - def dss_priceshape_plot(self, + def priceshape(self, *, ObjectName: str = None, Color1: str = None, @@ -805,7 +804,7 @@ def dss_priceshape_plot(self, ): # There is no dedicated API yet but we can move to the Obj API name = ObjectName - DSS = self.DSS + DSS = self.dss DSS.Text.Command = f'? priceshape.{name}.price' p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') @@ -842,7 +841,7 @@ def dss_priceshape_plot(self, fig.set_layout_engine(layout='tight') - def dss_loadshape_plot(self, + def loadshape(self, *, ObjectName: str = None, Color1: str = None, @@ -850,7 +849,7 @@ def dss_loadshape_plot(self, **kwargs: Unpack[PlotParams] ): # pprint(kwargs) - DSS = self.DSS + DSS = self.dss ls = DSS.ActiveCircuit.LoadShapes ls.Name = ObjectName @@ -900,7 +899,7 @@ def _get_branch_data(self, single_ph_line_style: int = 1, three_ph_line_style: int = 1 ): - DSS = self.DSS + DSS = self.dss line_count = branch_objects.Count if not idxs else len(idxs) lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) @@ -944,29 +943,29 @@ def _get_branch_data(self, max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) except: max_currents = {} - elem = DSS.ActiveCircuit.ActiveCktElement + element = DSS.ActiveCircuit.ActiveCktElement for _ in DSS.ActiveCircuit.PDElements: - if not elem.Enabled: + if not element.Enabled: continue - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - norm_amps = elem.NormalAmps - max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + currents = np.abs(asarray(element.Currents).view(dtype=complex)) + max_current = np.max(currents[:element.NumConductors]) + norm_amps = element.NormalAmps + max_currents[element.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 elif do_values == pqCapacity: try: capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) except: max_currents = {} - elem = DSS.ActiveCircuit.ActiveCktElement + element = DSS.ActiveCircuit.ActiveCktElement for _ in DSS.ActiveCircuit.PDElements: - if not elem.Enabled: - max_currents[elem.Name] = np.nan + if not element.Enabled: + max_currents[element.Name] = np.nan continue - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - norm_amps = elem.NormalAmps - max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 + currents = np.abs(asarray(element.Currents).view(dtype=complex)) + max_current = np.max(currents[:element.NumConductors]) + norm_amps = element.NormalAmps + max_currents[element.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 elif do_values == pqVoltage: node_volts = dict(zip(DSS.ActiveCircuit.AllNodeNames, asarray(DSS.ActiveCircuit.AllBusVmag) * 1e-3)) @@ -1067,7 +1066,7 @@ def _get_branch_data(self, lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style - if not elem.Enabled: + if not element.Enabled: lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style offset += 1 continue @@ -1106,7 +1105,7 @@ def _get_point_data(self, bus_coords: Dict[str, Tuple[float, float, float]], do_values: bool = False ): - DSS = self.DSS + DSS = self.dss if isinstance(point_objects, str): cls = point_objects DSS.SetActiveClass(cls) @@ -1143,7 +1142,7 @@ def _get_point_data(self, if i in skip: continue - if elem.Enabled: + if element.Enabled: values[offset] = np.abs(element.TotalPowers[0]) else: values[offset] = np.nan @@ -1153,13 +1152,13 @@ def _get_point_data(self, return points[:offset], values[:offset] - def dss_profile_plot(self, + def profile(self, *, PhasesToPlot: int = None, ProfileScale: float = None, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss if len(DSS.ActiveCircuit.Meters) == 0: raise RuntimeError(f"An EnergyMeter is required to use 'plot profile'") @@ -1242,7 +1241,7 @@ def dss_profile_plot(self, linewidths.append(lw) #TODO: NodeMarkerCode, NodeMarkerWidth - if include_3d in ('both', '2d'): + if self._include_3d in ('both', '2d'): fig = plt.figure()#figsize=(9, 5)) ax = fig.add_subplot(1, 1, 1) ax.set_xlabel(xlabel) @@ -1265,7 +1264,7 @@ def dss_profile_plot(self, ax.grid(ls='--') fig.set_layout_engine(layout='tight') - if include_3d in ('both', '3d'): + if self._include_3d in ('both', '3d'): fig2 = plt.figure()#figsize=(7, 7)) ax2 = fig2.add_subplot(1, 1, 1, projection='3d') ax2.set_xlabel(xlabel) @@ -1329,7 +1328,6 @@ def _get_gic_line_data_altdss( # GIC lines are not exposed nicely in the classic API, so we'll use the new Obj API for gic_line in altdss.GICLine: - TODO if not gic_line.enabled: continue @@ -1357,7 +1355,7 @@ def _get_gic_line_data(self, single_ph_line_style: int = 1, three_ph_line_style: int = 1 ): - DSS = self.DSS + DSS = self.dss try: return self._get_gic_line_data_altdss( DSS.to_altdss(), @@ -1381,10 +1379,10 @@ def _get_gic_line_data(self, # skip = set() # GIC lines are not exposed nicely in the classic API - elem = DSS.ActiveCircuit.ActiveCktElement + element = DSS.ActiveCircuit.ActiveCktElement idx = aclass.First while idx != 0: - buses = elem.BusNames + buses = element.BusNames b1 = remove_nodes(buses[0]) b2 = remove_nodes(buses[1]) fr = bus_coords.get(b1) @@ -1398,15 +1396,15 @@ def _get_gic_line_data(self, lines[offset, 1] = to lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) + currents = np.abs(asarray(element.Currents).view(dtype=complex)) + max_current = np.max(currents[:element.NumConductors]) values[offset] = max_current offset += 1 return lines[:offset], values[:offset], lines_styles[:offset] - def dss_circuit_plot(self, + def circuit(self, *, fig=None, ax=None, @@ -1426,7 +1424,7 @@ def dss_circuit_plot(self, MaxScaleIsSpecified: bool = None, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss if not MaxScaleIsSpecified: MaxScale = None @@ -1463,7 +1461,6 @@ def dss_circuit_plot(self, ax.set_aspect('equal', 'datalim') lines_lines, lines_values, lines_styles, switch_idxs, isolated_idxs, *extra = self._get_branch_data( - DSS, DSS.ActiveCircuit.Lines, bus_coords, do_values=quantity, @@ -1594,7 +1591,7 @@ def dss_circuit_plot(self, # ax.set_xlim(np.min(lines_lines[:, :, 0]), np.max(lines_lines[:, :, 0])) # ax.set_ylim(np.min(lines_lines[:, :, 1]), np.max(lines_lines[:, :, 1])) - transformers_lines, *_ = self._get_branch_data(DSS, DSS.ActiveCircuit.Transformers, bus_coords) + transformers_lines, *_ = self._get_branch_data(DSS.ActiveCircuit.Transformers, bus_coords) if not is3d: lc_transformers = LineCollection(transformers_lines, linewidth=3, linestyle='solid', color='gray') @@ -1654,7 +1651,7 @@ def dss_circuit_plot(self, ax.plot(*coords, color='red', **marker_dict) else: - #TODO? branch_lines = self._get_branch_data(DSS, objs, bus_coords, idxs=idxs) + #TODO? branch_lines = self._get_branch_data(objs, bus_coords, idxs=idxs) pass @@ -1665,7 +1662,7 @@ def dss_circuit_plot(self, marker_code = pmarkers[code_opt] marker_size = pmarkers[size_opt] - points = self._get_point_data(DSS, objs, bus_coords) + points = self._get_point_data(objs, bus_coords) # if marker_code not in MARKER_MAP: #marker_code = 25 @@ -1717,10 +1714,10 @@ def dss_circuit_plot(self, ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - def dss_scatter_plot(self, + def scatter(self, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss x = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) y = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) vcomplex = np.empty(shape=(DSS.ActiveCircuit.NumBuses, 3), dtype=complex) @@ -1743,16 +1740,16 @@ def dss_scatter_plot(self, vmean = np.mean(vabs, axis=1, where=np.isfinite(vabs)) title = '{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude') - if include_3d in ('both', '2d'): + if self._include_3d in ('both', '2d'): fig, ax = plt.subplots(1, 1, constrained_layout=True)#, figsize=(8, 7)) - dss_circuit_plot(DSS, fig=fig, ax=ax, Color1='k') + self.circuit(fig=fig, ax=ax, Color1='k') ax.get_xaxis().get_major_formatter().set_scientific(False) ax.get_yaxis().get_major_formatter().set_scientific(False) sc = ax.scatter(x, y, c=vmean) fig.colorbar(sc, label='V1 (pu)') ax.set_title(title) - if include_3d in ('both', '3d'): + if self._include_3d in ('both', '3d'): bus_coords = {} for idx, b in enumerate(DSS.ActiveCircuit.Buses): if b.Coorddefined: @@ -1760,7 +1757,7 @@ def dss_scatter_plot(self, fig = plt.figure()#figsize=(7, 7)) ax = fig.add_subplot(projection='3d') - dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True, Color1='k') + self.circuit(fig=fig, ax=ax, is3d=True, Color1='k') ax.get_xaxis().get_major_formatter().set_scientific(False) ax.get_yaxis().get_major_formatter().set_scientific(False) @@ -1797,14 +1794,14 @@ def dss_scatter_plot(self, ax.set_title(title) - def dss_visualize_plot(self, + def visualize(self, *, Quantity: str = None, ElementType: str = None, ElementName: str = None, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss XMAX = 300 #pprint(kwargs) @@ -1932,7 +1929,7 @@ def _get_text(): ax.set_ylim(-15, y + 5) - def dss_general_data_plot(self, + def general_data(self, *, PlotType: str = None, ObjectName: str = None, @@ -1947,7 +1944,7 @@ def dss_general_data_plot(self, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss if not MaxScaleIsSpecified: MaxScale = None @@ -2030,7 +2027,7 @@ def dss_general_data_plot(self, data = np.asarray(data) - dss_circuit_plot(DSS, **kwargs) + self.circuit(**kwargs) #fig = plt.figure(figsize=(8, 7)) plt.title(f'{field}, Max={max_val:.3g}') @@ -2058,13 +2055,13 @@ def dss_general_data_plot(self, #MarkSpecialClasses - def dss_matrix_plot(self, + def matrix(self, *, MatrixType: str = None, Color1: str = None, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss # plot_id = kwargs.get('PlotId', None) if MatrixType == 'IncMatrix': @@ -2078,7 +2075,7 @@ def dss_matrix_plot(self, m = coo.coo_matrix((v, (x, y))) #fig, [ax, ax2] = plt.subplots(1, 2, figsize=(8.6 * 2, 8.6), constrained_layout=True, num=title) - if include_3d in ('both', '2d'): + if self._include_3d in ('both', '2d'): fig = plt.figure(constrained_layout=True)#, num=plot_id) #, figsize=(8.6, 8.6)) ax = fig.add_subplot(1, 1, 1) ax.grid(True) @@ -2087,7 +2084,7 @@ def dss_matrix_plot(self, ax.set_ylabel('Row') ax.set_title(title) - if include_3d in ('both', '3d'): + if self._include_3d in ('both', '3d'): fig = plt.figure()#figsize=(8.6, 8.6), num=plot_id + '_3D') ax2 = fig.add_subplot(1, 1, 1, projection='3d') ax2.scatter(x, y, v, c=v, marker='s') @@ -2095,7 +2092,7 @@ def dss_matrix_plot(self, ax2.set_ylabel('Row') ax2.set_zlabel('Value') - def dss_daisy_plot(self, + def daisy(self, *, DaisyBusList: List[str] = None, Quantity: str = None, @@ -2103,9 +2100,9 @@ def dss_daisy_plot(self, DaisySize: float = None, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss - dss_circuit_plot(DSS, **kwargs) + self.circuit(**kwargs) # print(params['DaisySize']) @@ -2164,7 +2161,7 @@ def dss_daisy_plot(self, ax.text(bus.x, bus.y, bus.Name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - def dss_di_plot(self, + def di(self, *, CaseName: str = None, MeterName: str = None, @@ -2173,7 +2170,7 @@ def dss_di_plot(self, PeakDay: bool = None, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss caseYear, caseName, meterName = CaseYear, CaseName, MeterName plotRegisters, peakDay = Registers, PeakDay @@ -2237,7 +2234,7 @@ def dss_di_plot(self, def _plot_yearly_case(self, caseName: str, meterName: str, plotRegisters: List[int], icolor: int, ax, registerNames: List[str]): - DSS = self.DSS + DSS = self.dss anyData = True xvalues = [] all_yvalues = [[] for _ in plotRegisters] @@ -2298,20 +2295,20 @@ def _plot_yearly_case(self, caseName: str, meterName: str, plotRegisters: List[i return icolor - def dss_yearly_curve_plot(self, *, + def yearly_curve(self, *, MeterName: str = None, CaseNames: List[str] = None, Registers: List[str] = None, **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss caseNames, meterName, plotRegisters = CaseNames, MeterName, Registers fig, ax = plt.subplots(1) icolor = 0 registerNames = [] for caseName in caseNames: - icolor = _plot_yearly_case(DSS, caseName, MeterName, plotRegisters, icolor, ax, registerNames) + icolor = self._plot_yearly_case(caseName, MeterName, plotRegisters, icolor, ax, registerNames) if icolor == 0: plt.close(fig) @@ -2325,12 +2322,12 @@ def dss_yearly_curve_plot(self, *, ax.grid() - def dss_comparecases_plot(self, **kwargs: Unpack[PlotParams]): - DSS = self.DSS - print('TODO: dss_comparecases_plot', kwargs) + def compare_cases(self, **kwargs: Unpack[PlotParams]): + DSS = self.dss + print('TODO: compare_cases', kwargs) - def dss_zone_plot(self, + def zone(self, *, ObjectName: str, Quantity: DSSPlotQuantity = DEFAULT_PLOT_PARAMS['Quantity'], @@ -2345,7 +2342,7 @@ def dss_zone_plot(self, MaxScale: float = DEFAULT_PLOT_PARAMS['MaxScale'], **kwargs: Unpack[PlotParams] ): - DSS = self.DSS + DSS = self.dss obj_name = ObjectName show_loops = ShowLoops color1 = Color1 @@ -2371,7 +2368,7 @@ def dss_zone_plot(self, else: meters = ActiveCircuit.Meters - elem = ActiveCircuit.ActiveCktElement + element = ActiveCircuit.ActiveCktElement line = ActiveCircuit.Lines topo = ActiveCircuit.Topology @@ -2394,6 +2391,8 @@ def dss_zone_plot(self, coords_to_names = {} + element = DSS.ActiveCircuit.ActiveCktElement + def _name_coords(c, name): prev = coords_to_names.get(c) if prev is None: @@ -2452,7 +2451,7 @@ def _add_line(element, color): fig, ax = plt.subplots(1) for meter in meters: - if not elem.Enabled: + if not element.Enabled: continue feeder_name = meter.Name @@ -2462,7 +2461,7 @@ def _add_line(element, color): # Meter marker _ = topo.First - coords = bus_coords.get(elem.BusNames[meter.MeteredTerminal - 1]) + coords = bus_coords.get(element.BusNames[meter.MeteredTerminal - 1]) if coords: plt.plot(*coords, color='red', **meter_marker_dict) @@ -2471,15 +2470,15 @@ def _add_line(element, color): br_idx = topo.First while br_idx != 0: - if not elem.Enabled: + if not element.Enabled: continue - lcs, lidx = _add_line(elem, feeder_color) + lcs, lidx = _add_line(element, feeder_color) if show_loops: looped = (topo.LoopedBranch != 0) if looped: # The looped PDE is set as active by LoopedBranch - _add_line(elem, color3) + _add_line(element, color3) # Adjust the original to color3 if lidx is not None: lcs[lidx] = color3 @@ -2526,118 +2525,151 @@ def _add_line(element, color): ax.set_aspect('equal', 'datalim') ax.autoscale() + def enable(self, plot3d: bool = False, plot2d: bool = True, show: bool = True, dss: Optional[IDSS] = None): + """ + Enables the plotting subsystem from DSS-Extensions. + + Set plot3d to `True` to try to reproduce some of the plots from the + alternative OpenDSS Visualization Tool / OpenDSS Viewer addition + to OpenDSS. + + Use `show` to control whether this backend should call `pyplot.show()` + or leave that to the system or the user. If the user plans to customize + the figure, it is better to set `show=False` in order to preserve the + figures, since `pyplot.show()` discards them. + """ + + if dss is not None: + get_plotter(dss).enable(plot3d=plot3d, plot2d=plot2d, show=show) + return + + self._do_show = show + was_enabled = self._enabled + self._enabled = True + + if plot3d and plot2d: + self._include_3d = 'both' + elif plot3d and not plot2d: + self._include_3d = '3d' + elif plot2d and not plot3d: + self._include_3d = '2d' + + dss = self.dss + if not was_enabled: + api_util.lib_unpatched.DSS_RegisterPlotCallback(dss._api_util.ctx, loader_lib.dss_python_cb_plot) + api_util.lib_unpatched.DSS_RegisterMessageCallback(dss._api_util.ctx, loader_lib.dss_python_cb_write) + self._original_allow_forms = dss.AllowForms + + dss.AllowForms = True + + def disable(self, dss: Optional[IDSS] = None): + if dss is not None: + get_plotter(dss).enable(plot3d=plot3d, plot2d=plot2d, show=show) + return + + dss = self.dss + self._enabled = False + api_util.lib_unpatched.DSS_RegisterPlotCallback(dss._api_util.ctx, dss._api_util.ffi.NULL) + api_util.lib_unpatched.DSS_RegisterMessageCallback(dss._api_util.ctx, dss._api_util.ffi.NULL) + if self._original_allow_forms is not None: + self.dss.AllowForms = self._original_allow_forms +DSSPlotter = DSSMPLPlotter dss_plot_methods = { - 'Scatter': 'dss_scatter_plot', - 'Daisy': 'dss_daisy_plot', - 'TShape': 'dss_tshape_plot', - 'PriceShape': 'dss_priceshape_plot', - 'LoadShape': 'dss_loadshape_plot', - 'Monitor': 'dss_monitor_plot', - 'Circuit': 'dss_circuit_plot', - 'Profile': 'dss_profile_plot', - 'Visualize': 'dss_visualize_plot', - 'YearlyCurve': 'dss_yearly_curve_plot', - 'Matrix': 'dss_matrix_plot', - 'GeneralData': 'dss_general_data_plot', - 'DI': 'dss_di_plot', -# 'CompareCases': 'dss_comparecases_plot', - 'MeterZones': 'dss_zone_plot' + 'Scatter': 'scatter', + 'Daisy': 'daisy', + 'TShape': 'tshape', + 'PriceShape': 'priceshape', + 'LoadShape': 'loadshape', + 'Monitor': 'monitor', + 'Circuit': 'circuit', + 'Profile': 'profile', + 'Visualize': 'visualize', + 'YearlyCurve': 'yearly_curve', + 'Matrix': 'matrix', + 'GeneralData': 'general_data', + 'DI': 'di', +# 'CompareCases': 'compare_cases', + 'MeterZones': 'zone' } -def dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): +def _dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): try: ptype = kwargs['PlotType'] if ptype not in dss_plot_methods: raise NotImplementedError(f'ERROR: not implemented plot type "{ptype}"') return -1 + plotter = get_plotter(DSS._api_util.ctx, create=False) + if plotter is None: + # plotter = DSSPlotter(DSS) + return 0 + with DSS.ActiveCircuit.Settings.Context() as settings, warnings.catch_warnings(): warnings.simplefilter("ignore") settings.AdvancedTypes = False settings.PreferLists = False func = getattr(plotter, dss_plot_methods.get(ptype)) - return 0, (DSS, **kwargs) + fig = func(**kwargs) + if plotter._do_show and fig is not None: + fig.show() + + return 0 except Exception as ex: from traceback import format_exc # print('DSS: Error while plotting. Parameters:', kwargs, file=sys.stderr) DSS._errorPtr[0] = 777 DSS._lib.Error_Set_Description(f"Error in the plot backend: {ex}\n{format_exc()}".encode()) - return 777, None + return 777 - return 0, None + return 0 -@api_util.ffi.def_extern() -def dss_python_cb_plot(ctx, paramsStr): +@api_util.ffi.def_extern(name="dss_python_cb_plot") +def _dss_python_cb_plot(ctx, paramsStr): params = json.loads(api_util.ffi.string(paramsStr)) result = 0 try: DSS = IDSS._get_instance(ctx=ctx) - result, fig = dss_plot(DSS, **params) - if _do_show: - fig.show() + result = _dss_plot(DSS, **params) + except: from traceback import print_exc print('DSS: Error while plotting. Parameters:', params, file=sys.stderr) print_exc() return 0 if result is None else result -_original_allow_forms = None -_do_show = True -_enabled = False -def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True, ctx: IDSS = None): +def plot_dsv(fn: Union[str, FilePath], show=True): """ - Enables the plotting subsystem from DSS-Extensions. - - Set plot3d to `True` to try to reproduce some of the plots from the - alternative OpenDSS Visualization Tool / OpenDSS Viewer addition - to OpenDSS. + Plot an OpenDSS DSV file. - Use `show` to control whether this backend should call `pyplot.show()` - or leave that to the system or the user. If the user plans to customize - the figure, it is better to set `show=False` in order to preserve the - figures, since `pyplot.show()` discards them. + When passing `show=False`, the user can modify the figure before showing it, + using Matplotlib's API. In that case, the function returns a tuple `(figure, ax)`. """ + return DSVHandler(fn, show=show).parse_and_plot() - global include_3d - global _original_allow_forms - global _do_show - global _enabled - global DSSPlotCtx - - if ctx is not None: - DSSPlotCtx = ctx - - _do_show = show - _enabled = True - - if plot3d and plot2d: - include_3d = 'both' - elif plot3d and not plot2d: - include_3d = '3d' - elif plot2d and not plot3d: - include_3d = '2d' - - api_util.lib.DSS_RegisterPlotCallback(api_util.lib.dss_python_cb_plot) - api_util.lib.DSS_RegisterMessageCallback(api_util.lib.dss_python_cb_write) - _original_allow_forms = DSSPlotCtx.AllowForms - DSSPlotCtx.AllowForms = True +def get_plotter(ctx: IDSS, create=True): + """ + Returns the DSS plotter associated with the context `ctx`, if any. + If none exists and `create=True` (default), as new plotter is created + and returned. + """ + if hasattr(ctx, '_api_util'): + ctx = ctx._api_util.ctx -def disable(): - global _enabled - _enabled = False - api_util.lib.DSS_RegisterPlotCallback(api_util.ffi.NULL) - api_util.lib.DSS_RegisterMessageCallback(api_util.ffi.NULL) - if _original_allow_forms is not None: - DSSPlotCtx.AllowForms = _original_allow_forms + plotter = DSSPlotter._ctx_to_plotter.get(ctx) + if create and plotter is None: + DSS = IDSS._get_instance(ctx=ctx) + plotter = DSSPlotter(DSS) + return plotter -def plot_dsv(fn: Union[str, FilePath]): - return DSVHandler(fn).parse() +plot = DSSPlotter(DSSPlotCtx) # Main plotter instance (default DSS context) +enable = plot.enable +disable = plot.disable -__all__ = ['enable', 'disable', 'plot_dsv', ] +__all__ = ['enable', 'disable', 'plot_dsv', 'plot', 'get_plotter'] diff --git a/dss/plot2.py b/dss/plot2.py deleted file mode 100644 index 4a3a8f25..00000000 --- a/dss/plot2.py +++ /dev/null @@ -1,2698 +0,0 @@ -""" -This module provides a **work-in-progress** implementation of the original OpenDSS plots -using the new features from DSS C-API v0.12+ and common Python modules such as matplotlib. - -This is not a complete implementation and there are known limitations, but should suffice -for many use-cases. We'd like to add another backend later. -""" -from __future__ import annotations -import os, re, json, sys, warnings -from typing import List, TYPE_CHECKING, Optional, Tuple, Dict -from typing_extensions import TypedDict, Unpack -from . import api_util -from . import DSS as DSSPlotCtx -from ._cffi_api_util import AltDSSAPIUtil -from .IDSS import IDSS -from .IBus import IBus -from ._cffi_api_util import Iterable as DSSIterable -from enum import Enum, IntEnum -import numpy as np -from numpy import asarray -from numpy.testing import suppress_warnings -from pathlib import Path as FilePath -try: - from matplotlib import pyplot as plt - from matplotlib.path import Path - from matplotlib.collections import LineCollection - from mpl_toolkits.mplot3d.art3d import Line3DCollection - import matplotlib.patches as patches - import matplotlib.colors - import scipy.sparse.coo as coo -except: - raise ImportError("SciPy and matplotlib are required to use this module.") - -if TYPE_CHECKING: - from altdss.AltDSS import IAltDSS - -class DSSPlotType(Enum): - AutoAddLog = 'AutoAddLog' - Circuit = 'Circuit' - Daisy = 'Daisy' - Energy = 'Energy' - Evolution = 'Evolution' - GeneralData = 'GeneralData' - LoadShape = 'LoadShape' - Matrix = 'Matrix' - MeterZones = 'MeterZones' - Monitor = 'Monitor' - PhaseVoltage = 'PhaseVoltage' - PriceShape = 'PriceShape' - Profile = 'Profile' - Scatter = 'Scatter' - TShape = 'TShape' - - -(pqVoltage, pqCurrent, pqPower, pqLosses, pqCapacity, pqNone) = range(6) - -class DSSPlotQuantity(Enum): - Capacities = 'Capacities' - Currents = 'Currents' - Losses = 'Losses' - Powers = 'Powers' - Voltages = 'Voltages' - none = 'None' - - -class ProfileScale(Enum): - pukm = 'pukm' - kft120 = '120kft' - - -class ObjMarkers(TypedDict): - NodeMarkerCode: Optional[int] - NodeMarkerWidth: Optional[float] - - MarkTransformers: Optional[bool] - TransMarkerCode: Optional[int] - TransMarkerSize: Optional[float] - - MarkCapacitors: Optional[bool] - CapMarkerCode: Optional[int] - CapMarkerSize: Optional[float] - - MarkPVSystems: Optional[bool] - PVMarkerCode: Optional[int] - PVMarkerSize: Optional[float] - - MarkFuses: Optional[bool] - FuseMarkerCode: Optional[int] - FuseMarkerSize: Optional[float] - - MarkReclosers: Optional[bool] - RecloserMarkerCode: Optional[int] - RecloserMarkerSize: Optional[float] - - MarkRegulators: Optional[bool] - RegMarkerCode: Optional[int] - RegMarkerSize: Optional[float] - - MarkRelays: Optional[bool] - RelayMarkerCode: Optional[int] - RelayMarkerSize: Optional[float] - - MarkStorage: Optional[bool] - StoreMarkerCode: Optional[int] - StoreMarkerSize: Optional[float] - - MarkSwitches: Optional[bool] - SwitchMarkerCode: Optional[int] - - -class BusMarker(TypedDict): - Name: str - Color: str - Code: int - Size: float - - -class DSSPlotPhases(IntEnum): - PROFILE3PH = -1 # Default - PROFILEALL = -2 # All - PROFILEALLPRI = -3 # Primary - PROFILELL3PH = -4 # LL3Ph - PROFILELLALL = -5 # LLAll - PROFILELLPRI = -6 # LLPrimary - - -class PlotParams(TypedDict): - PlotType: DSSPlotType - MatrixType: str - MaxScale: float - MinScale: float - Dots: bool - Labels: bool - ShowLoops: bool - ShowSubs: bool - Quantity: str - ObjectName: str - PlotId: str #TODO - ValueIndex: int - PhasesToPlot: DSSPlotPhases - ProfileScale: str - Channels: List[int] - Bases: Optional[List[float]] - SinglePhLineStyle: int - ThreePhLineStyle: int - Color1: str - Color2: str - Color3: str - TriColorMax: float - TriColorMid: float - MaxScaleIsSpecified: bool - MinScaleIsSpecified: bool - DaisyBusList: List[str] - DaisySize: float - MaxLineThickness: float - MarkerParams: Optional[ObjMarkers] - BusMarkers: Optional[List[BusMarker]] - - Registers: List[int] - PeakDay: bool - MeterName: str - CaseName: str - CaseYear: int - - -DEFAULT_MARKER_PARAMS = ObjMarkers( - MarkTransformers=False, - TransMarkerCode=None, - TransMarkerSize=None, - - MarkCapacitors=False, - CapMarkerCode=None, - CapMarkerSize=None, - - MarkPVSystems=False, - PVMarkerCode=None, - PVMarkerSize=None, - - MarkStorage=False, - StoreMarkerCode=None, - StoreMarkerSize=None, - - MarkSwitches=False, - SwitchMarkerCode=None, - - MarkFuses=False, - FuseMarkerCode=None, - FuseMarkerSize=None, - - MarkRegulators=False, - RegMarkerCode=None, - RegMarkerSize=None, - - MarkRelays=False, - RelayMarkerCode=None, - RelayMarkerSize=None, - - MarkReclosers=False, - RecloserMarkerCode=None, - RecloserMarkerSize=None, -) - -DEFAULT_PLOT_PARAMS = PlotParams( - PlotType=DSSPlotType.Circuit, - Quantity=DSSPlotQuantity.Powers, - Channels=[1, 3, 5], - MarkerParams=DEFAULT_MARKER_PARAMS, - BusMarkers=[], - Color1='#0000FF', - Color2='#008000', - Color3='#FF0000', - TriColorMax=0.85, - TriColorMid=0.50, - ThreePhLineStyle=1, - SinglePhLineStyle=1, - ProfileScale=ProfileScale.pukm, - PhasesToPlot=DSSPlotPhases.PROFILE3PH, - DaisyBusList=[], - MaxLineThickness=10, - Dots=False, - Labels=False, - ShowLoops=False, - ShowSubs=False, - MinScaleIsSpecified=False, - MaxScaleIsSpecified=False, - MinScale=0.0, - MaxScale=None, -) - -try: - from IPython import get_ipython - from IPython.display import FileLink, display, display_html, HTML - from IPython.core.magic import register_cell_magic - ipython = get_ipython() - if ipython is None: - raise ImportError - - import html - - def link_file(fn): - relfn = os.path.relpath(fn, os.getcwd()) - if relfn.startswith('..'): - # cannot show in the notebook :( - display(HTML(f'

File output ("{html.escape(relfn)}") outside current workspace.

')) - else: - display(FileLink(relfn, result_html_prefix=f'File output ("{html.escape(fn)}"): ')) - - def show(text): - display(text) - - - @register_cell_magic - def dss(line, cell): - if isinstance(DSSPlotCtx, IDSS) and not DSSPlotCtx._api_util._is_oddie: - DSSPlotCtx.Text.Commands(cell) - else: - for line in cell.split('\n'): - DSSPlotCtx(line) - res = DSSPlotCtx.Text.Result - if res.endswith('.DSV'): - if _enabled and FilePath(res).exists(): - plot_dsv(res) - - DSSPlotCtx.AllowChangeDir = False -except: - def link_file(fn): - print(f'Output file: "{fn}"') - - def show(text): - print(text) - - - #FileLink('path_to_file/filename.extension') - -# import os -# import html -# import tqdm -# from tqdm.notebook import tqdm -# import IPython.display - -include_3d = '2d' # '2d' (default), '3d' (prefer 3d), 'both' - -str_to_pq = { - 'Voltages': pqVoltage, - 'Currents': pqCurrent, - 'Powers': pqPower, - 'Losses': pqLosses, - 'Capacities': pqCapacity, -} - -quantity_str = {v: k for k, v in str_to_pq.items()} -quantity_str[pqLosses] = 'Loss Density' - -str_to_pq.update({ - 'Voltage': pqVoltage, - 'Current': pqCurrent, - 'Power': pqPower, - 'Loss': pqLosses, - 'Capacity': pqCapacity -}) - -# Markers -DSS_MARKER_37 = Path([(0.0, -0.5), (0.0, 0.196), (-0.36, -0.5), (0.361, -0.5)], [1, 2, 1, 2]) -DSS_MARKER_38 = Path([(0.0, -0.23), (0.0, 0.196), (-0.36, 0.196), (0.361, 0.196), (-0.36, -0.23), (0.361, -0.23)], [1, 2, 1, 2, 1, 2]) -DSS_MARKER_20 = Path([(-0.23, -0.147), (0.0, 0.13), (0.23, -0.147)], [1, 2, 2]) -DSS_MARKER_21 = Path([(-0.28, -0.147), (0.0, 0.13), (0.28, -0.147)], [1, 2, 2]) -DSS_MARKER_22 = Path([(-0.23, 0.147), (0.0, -0.13), (0.23, 0.147)], [1, 2, 2]) -DSS_MARKER_23 = Path([(-0.28, 0.147), (0.0, -0.13), (0.28, 0.147)], [1, 2, 2]) - -MARKER_MAP = { - # marker, size multiplier (1=normal, 2=small, 3=tiny), fill - 0: (',', 1, 1), - 1: ('+', 3, 1), - 2: ('+', 2, 1), - 3: ('+', 1, 1), - 4: ('x', 3, 1), - 5: ('x', 2, 1), - 6: ('x', 1, 1), - 7: ('s', 3, 1), - 8: ('s', 2, 1), - 9: ('s', 1, 1), - 10: ('s', 3, 0), - 11: ('s', 2, 0), - 12: ('s', 1, 0), - 13: ('D', 3, 1), - 14: ('D', 2, 1), - 15: ('D', 1, 1), - 16: ('o', 2, 0), - 17: ('o', 1, 0), - 18: ('s', 1, 0.5), - 19: ('D', 1, 0), - 20: (DSS_MARKER_20, 2, 0), - 21: (DSS_MARKER_21, 1, 0), - 22: (DSS_MARKER_22, 2, 0), - 23: (DSS_MARKER_23, 1, 0), - 24: ('o', 1, 1), - 25: ('X', 1, 1), - 26: ('o', 2, 1), - 27: ('o', 3, 0), - 28: ('o', 3, 1), - 29: (DSS_MARKER_22, 3, 1), - 30: (DSS_MARKER_23, 2, 1), - 31: ('v', 3, 0), - 32: ('v', 2, 0), - 33: (7, 1, 0), - 34: (7, 2, 1), - 35: ('^', 1, 0), - 36: (6, 1, 1), - 37: (DSS_MARKER_37, 1, 0), - 38: (DSS_MARKER_38, 1, 0), - 39: ('$⊕$', 1, 1), # normal (circled plus) - 40: (8, 2, 0), # small - 41: (8, 2, 1), # small - 42: (8, 1, 0), # normal - 43: (8, 1, 1), # normal - 44: (9, 2, 0), # small - 45: (9, 2, 1), # small - 46: (9, 1, 0), # normal - 47: (9, 1, 1), # normal -} - -LINES_STYLE_CODE = {1: '-', 2: '--', 3: ':', 4: '-.', 5: (0, (5, 1, 1, 1, 1, 1)), 6: (0, (0, 1))} - -Colors = [ - '#000000', - '#FF0000', - '#0000FF', - '#FF00FF', - '#008000', - '#80FF00', - '#FF8040', - '#DADE21', - '#B56AFF', - '#804000', - '#808000', - '#0000A0', - '#FF8080', - '#000080', - '#7F7F7F', - '#8E0F7B', - '#07968E' -] - -sizes = np.array([0, 9, 6, 4], dtype=float) * 0.7 - -MARKER_SEQ = (5, 15, 2, 8, 26, 36, 39, 19, 18) - - -DSV_LINE_STYLES = { - 0: 'solid', - 1: 'dashed', - 2: 'dotted', - 3: 'dashdot', - 4: (0, (3, 5, 1, 5, 1, 5)), -} - -DSS_ITEMS = { - 'BoldLabel', - 'Caption', - 'Center', - 'ChartCaption', - 'Circle', - 'ClickOn', - 'Curve', - 'DataColor', - 'Draw', - 'FStyle', - 'KeepAspect', - 'KeyClass', - 'Label', - 'Line', - 'Marker', - 'Move', - 'NoScales', - 'PctRim', - 'Range', - 'Rect', - 'SetProp', - 'Text', - 'TxtAlign', - 'Width', - 'Xlabel', - 'Ylabel', -} - -def get_marker_dict(dss_code): - marker, size, fill = MARKER_MAP[dss_code] - res = dict( - marker=marker, - markersize=sizes[size], - markerfacecolor=None if fill else 'none', - # fillstyle='full' if fill else 'none', - alpha=0.5 if fill not in (0, 1) else 1, - markeredgecolor='none' if dss_code == 39 else None, - markeredgewidth=1 - ) - for k, v in list(res.items()): - if v is None: - del res[k] - - return res - -def nodot(b): - return b.split('.', 1)[0] - -def unquote(field: str): - field = field.strip() - if field[0] == '"' and field[-1] == '"': - return field[1:-1] - - return field - -node_re = re.compile(r'(.*?)(\.[0-9])*$') - -def remove_nodes(bus): - match = node_re.match(bus) - return match.group(1) - -def _int_to_color(v: int): - return ((v & 255) / 255.0, (v >> 8 & 255) / 255.0, (v >> 16) / 255.0) - -class ToggleAdvancedTypes: - def __init__(self, dss: IDSS, value: bool): - self._value = value - self._dss = dss - self._previous = self._dss.AdvancedTypes - - def __enter__(self): - if self._value != self._previous: - self._dss.AdvancedTypes = self._value - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if self._value != self._previous: - self._dss.AdvancedTypes = self._previous - - -class DSVHandler: - def __init__(self, fn: Union[str, FilePath]): - self.fn = fn - self.fig, self.ax = plt.subplots() - self.ax.get_xaxis().get_major_formatter().set_scientific(False) - self.ax.get_yaxis().get_major_formatter().set_scientific(False) - self.xy = [0.0, 0.0] - self.line_width = 1 - self.fig_caption = None - self.color = 'k' - self.key_class = None - self.no_scales = False - self.bold = True - self.txt_align = 'left' - - - def BoldLabel(self, param_str: str): - self.bold = int(param_str.strip()) != 0 - - - def Caption(self, param_str: str): - self.fig_caption = param_str.strip().strip('"') - self.fig.canvas.manager.set_window_title(self.fig_caption) - - - def ChartCaption(self, param_str: str): - self.ax.set_title(param_str.strip().strip('"')) - - - def Center(self, param_str: str): - *int_params, text = param_str.split(',') - x, y, s = [int(v.strip()) for v in int_params] - text = text.strip().strip('"') - if '/_' in text: - text = text.replace('/_', '∠') + '°' - - if '->' in text: - text = text.replace('->', '→') - s = s * 1.5 - elif '<-' in text: - text = text.replace('<-', '←') - s = s * 1.5 - elif '^' in text: - text = text.replace('^', '↑') - s = s * 1.5 - - self.ax.text(x, y, text, horizontalalignment='center', fontsize=s * 8 / 13.) - - - def Circle(self, param_str: str): - params = param_str.split(',') - x, y = float(params[0]), float(params[1]) - fc = _int_to_color(int(params[4])) - ec = _int_to_color(int(params[3])) - self.ax.scatter(x, y, marker='o', color=fc, edgecolors=ec, s=50, zorder=10, linewidths=0.5) - - - def ClickOn(self, param_str: str): - #TODO - pass - - - def Curve(self, param_str: str): - *int_params, curve_name, rest = param_str.split(',', 7) - npts, color, width, style, curve_markers, curve_marker = [int(v.strip()) for v in int_params] - if curve_markers: - marker_dict = get_marker_dict(curve_marker) - else: - marker_dict = {} - - data = np.fromstring(rest, dtype=float, sep=',') - self.ax.plot(data[:npts], data[npts:], lw=width/2.0, label=curve_name.strip().strip('"'), color=_int_to_color(color), ls=DSV_LINE_STYLES[style], **marker_dict) - # self.ax.minorticks_on() - - - def DataColor(self, param_str: str): - self.color = _int_to_color(int(param_str)) - - - def Draw(self, param_str: str): - if not self.no_scales: - # Currently not used since Move/Draw is emulated with axhline - return - - x0, y0 = self.xy - x1, y1 = [float(v.strip().strip('"')) for v in param_str.split(',')] - self.ax.plot([x0, x1], [y0, y1], color=self.color, lw=self.line_width/2.0) - - - def FStyle(self, param_str: str): - fstyle = int(param_str.strip().strip('"')) - # if fstyle != 0: - # print('Unhandled font style:', fstyle) - - - def KeepAspect(self, param_str: str): - try: - v = int(param_str.strip().strip('"')) - except: - v = 1 - - if v: - self.ax.set_aspect('equal', 'datalim') - else: - self.ax.set_aspect('auto') - - - def KeyClass(self, param_str: str): - self.key_class = int(param_str.strip()) - - - def Label(self, param_str: str): - *int_params, text, _ = param_str.split(',') - x, y, color_int = [int(v.strip()) for v in int_params] - color = _int_to_color(color_int) - text = text.strip().strip('"') - self.ax.text(x, y, text, - horizontalalignment='center', - fontsize=10 * 8 / 13., - color=color, - backgroundcolor='white', - weight='bold' if self.bold else 'normal' - ) - - - def Line(self, param_str: str): - #TODO: use LineCollection - - *str_params, rest = param_str.split(',', 3) - line_name, bus1, bus2 = [v.strip().strip('"') for v in str_params] - *int_params, rest = rest.split(',', 4) - offset, data_count, num_cust, total_cust = [int(v) for v in int_params] - *dbl_params, rest = rest.split(',', 6) - kv, dist, x1, y1, x2, y2 = [float(v) for v in dbl_params] - int_params = rest.split(',') - #TODO: markers - color, width, style, dots, mark_center, center_marker_code, node_marker_code, node_marker_size = [int(v) for v in int_params] - - if dots: - node_marker_dict = get_marker_dict(node_marker_code) - node_marker_dict['markersize'] *= max(1, np.sqrt(node_marker_size) - 1) * node_marker_dict['markersize'] / 7.0 - else: - node_marker_dict = {} - - self.ax.plot([x1, x2], [y1, y2], color=_int_to_color(color), lw=width / 2.0, ls=DSV_LINE_STYLES[style], solid_capstyle='round', **node_marker_dict) - - if mark_center: - center_marker_dict = get_marker_dict(center_marker_code) - self.ax.scatter((x1 + x2) / 2, (y1 + y2) / 2, color=_int_to_color(color), **center_marker_dict) - - - def Marker(self, param_str: str): - params = param_str.split(',') - x, y = float(params[0]), float(params[1]) - c, symbol, marker_size = [int(v) for v in params[2:]] - marker_dict = get_marker_dict(symbol) - marker_dict['markersize'] *= max(1, np.sqrt(marker_size) - 1) * marker_dict['markersize'] / 7.0 - self.ax.plot(x, y, ls=None, color=_int_to_color(c), **marker_dict) - - - def Move(self, param_str: str): - x, y = [float(v.strip().strip('"')) for v in param_str.split(',')] - if self.no_scales: - self.xy = [x, y] - else: - self.ax.axhline(y, color=self.color, lw=self.line_width / 2.0) - - - def NoScales(self, param_str: str): - self.no_scales = True - self.ax.get_xaxis().set_visible(False) - self.ax.get_yaxis().set_visible(False) - - - def PctRim(self, param_str: str): - self.ax.margins(float(param_str) / 100.0) - - - def Range(self, param_str: str): - pass - - - def Rect(self, param_str: str): - left, bottom, right, top = [int(v) for v in param_str.split(',')] - r = patches.Rectangle((left, bottom), right - left, top - bottom, fill=True, ec='k', fc='#c0c0c0') - self.ax.add_patch(r) - - - def SetProp(self, param_str: str): - if int(param_str.rsplit(',', 1)[-1]) != 0: - self.ax.grid(which='both', ls='--') - else: - self.ax.grid(False) - - - def Text(self, param_str: str): - *int_params, text = param_str.split(',') - x, y, c, s = [int(v.strip()) for v in int_params] - text = text.strip().strip('"') - self.ax.text(x, y, text, ha=self.txt_align, va='center', fontsize=s * 10 / 13.) - - - def TxtAlign(self, param_str: str): - v = int(param_str) - if v == 1: - self.txt_align = 'left' - return - - if v == 2: - self.txt_align = 'center' - return - - if v == 3: - self.txt_align = 'right' - return - - - def Width(self, param_str: str): - self.line_width = int(param_str.strip().strip('"')) - - - def Xlabel(self, param_str: str): - self.ax.set_xlabel(param_str.strip().strip('"')) - - - def Ylabel(self, param_str: str): - self.ax.set_ylabel(param_str.strip().strip('"')) - - def parse(self): - with open(self.fn, 'r') as f: - for l in f: - l = l.strip() - if not l: - continue - - item_name, *rest = l.split(',', 1) - item_name = item_name.strip() - if item_name not in DSS_ITEMS: - raise NotImplemented(f'"{item_name}" DSV item is not implemented') - - # print(item, repr(rest)[:100]) - getattr(self, item_name)(rest[0] if rest else '') # let the exception propagate on error - - if _do_show: - self.fig.show() - else: - return self.fig, self.ax - - -class DSSMPLPlotter: - def __init__(self, dss: IDSS): - self.dss = dss - - def dss_monitor_plot(DSS: IDSS, - *, - ObjectName: str = None, - Channels: List[int] = None, # TODO: allow channel names too - Bases: List[float] = None, - **kwargs: Unpack[PlotParams] - ): - monitor = DSS.ActiveCircuit.Monitors - monitor.Name = ObjectName - data = monitor.AsMatrix() - if data is None or len(data) == 0: - raise ValueError("There is not data to plot in the monitor. Hint: check the solution mode, solve the circuit and retry.") - - channels = Channels - num_ch = monitor.NumChannels - channels = [ch for ch in channels if ch >= 1 and ch <= num_ch] - if len(channels) == 0: - raise IndexError("No valid channel numbers were specified.") - - bases = Bases - header = list(monitor.Header) - if len(monitor.dblHour) < len(monitor.dblFreq): - header.insert(0, 'Frequency') - header.insert(1, 'Harmonic') - xlabel = 'Frequency (Hz)' - h = data[:, 0] - else: - header.insert(0, 'Hour') - header.insert(1, 'Seconds') - h = data[:, 0] * 3600 + data[:, 1] - total_seconds = max(h) - min(h) - if total_seconds < 7200: - xlabel = 'Time (s)' - else: - xlabel = 'Time (h)' - h /= 3600 - - separate = False - if separate: - fig, axs = plt.subplots(len(channels), sharex=True)#, figsize=(8, 9)) - icolor = -1 - for ax, base, ch in zip(axs, bases, channels): - ch += 1 - icolor += 1 - ax.plot(h, data[:, ch] / base, color=Colors[icolor % len(Colors)]) - ax.grid() - ax.set_ylabel(header[ch]) - - else: - fig, ax = plt.subplots(1) - icolor = -1 - for base, ch in zip(bases, channels): - ch += 1 - icolor += 1 - ax.plot(h, data[:, ch] / base, label=header[ch], color=Colors[icolor % len(Colors)]) - - ax.grid() - ax.legend() - ax.set_ylabel('Mag') # Where "Mag" comes from? - - ax.set_title(ObjectName) - ax.set_xlabel(xlabel) - - - def dss_tshape_plot(DSS: IDSS, - *, - ObjectName: str = None, - Color1: str = None, - **kwargs: Unpack[PlotParams] - ): - # There is no dedicated API yet but we can move to the Obj API - name = ObjectName - DSS.Text.Command = f'? tshape.{name}.temp' - p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - try: - DSS.Text.Command = f'? tshape.{name}.hour' - h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - except: - h = np.array([]) - - try: - interval = f'? tshape.{name}.interval' # hours - interval = float(DSS.Text.Result) - except: - interval = 1 - - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"TShape.{ObjectName}") - - if not h.size: - h = interval * np.array(range(len(p))) - - x_unit = 'h' - if h[-1] < 1: - h *= 3600 - x_unit = 's' - - color1 = Color1 - ax.plot(h, p, color=color1, label="Price") - ax.set_title(f"TShape = {ObjectName}") - ax.set_xlabel(f'Time ({x_unit})') - ax.set_ylabel('Temperature') - - ax.grid(ls='--') - plt.tight_layout() - - - - def dss_priceshape_plot(DSS: IDSS, - *, - ObjectName: str = None, - Color1: str = None, - **kwargs: Unpack[PlotParams] - ): - # There is no dedicated API yet but we can move to the Obj API - name = ObjectName - DSS.Text.Command = f'? priceshape.{name}.price' - p = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - try: - DSS.Text.Command = f'? priceshape.{name}.hour' - h = np.fromstring(DSS.Text.Result[1:-1].strip(), dtype=float, sep=' ') - except: - h = np.array([]) - - try: - interval = f'? priceshape.{name}.interval' # hours - interval = float(DSS.Text.Result) - except: - interval = 1 - - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"PriceShape.{ObjectName}") - - if not h.size: - h = interval * np.array(range(len(p))) - - x_unit = 'h' - if h[-1] < 1: - h *= 3600 - x_unit = 's' - - color1 = Color1 - - ax.plot(h, p, color=color1, label="Price") - ax.set_title(f"PriceShape = {ObjectName}") - ax.set_xlabel(f'Time ({x_unit})') - ax.set_ylabel('Price') - - ax.grid(ls='--') - plt.tight_layout() - - - def dss_loadshape_plot(DSS: IDSS, - *, - ObjectName: str = None, - Color1: str = None, - Color2: str = None, - **kwargs: Unpack[PlotParams] - ): - # pprint(kwargs) - - ls = DSS.ActiveCircuit.LoadShapes - ls.Name = ObjectName - h = asarray(ls.TimeArray) - p = asarray(ls.Pmult) - q = asarray(ls.Qmult) - - fig, ax = plt.subplots(1)#, figsize=(8.5, 6))#, num=f"LoadShape.{ObjectName}") - - if not h.size or h is None or len(h) != len(p): - h = ls.HrInterval * np.array(range(len(p))) - - x_unit = 'h' - if h[-1] < 1: - h *= 3600 - x_unit = 's' - - color1 = Color1 - color2 = Color2 - - ax.plot(h, p, color=color1, label="Pmult") - if q.size == p.size: - ax.plot(h, q, color=color2, label="Qmult") - - ax.set_title(f"LoadShape = {ObjectName}") - ax.set_xlabel(f'Time ({x_unit})') - if ls.UseActual: - if q.size == p.size: - ax.set_ylabel('kW, kvar') - else: - ax.set_ylabel('kW') - else: - ax.set_ylabel('p.u.') - - ax.grid(ls='--') - if q.size == p.size: - ax.legend() - plt.tight_layout() - - - def _get_branch_data(DSS: IDSS, - branch_objects: DSSIterable, - bus_coords: Dict[str, Tuple[float, float, float]], - do_values=pqNone, - do_switches=False, - idxs=None, - single_ph_line_style: int = 1, - three_ph_line_style: int = 1 - ): - line_count = branch_objects.Count if not idxs else len(idxs) - lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) - lines.fill(np.nan) - values = np.empty(shape=(line_count, ), dtype=np.float64) - values.fill(np.nan) - lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) - - element = DSS.ActiveCircuit.ActiveCktElement - - if do_switches: - switch_idxs = [] - isolated_idxs = [] - try: - element.IsIsolated - has_is_isolated = True - except: - has_is_isolated = False - isolated_names = set(name.lower() for name in DSS.ActiveCircuit.Topology.AllIsolatedBranches if name) - - extra = [switch_idxs, isolated_idxs] - else: - extra = [] - # def get_buses_line(l): - # b1 = remove_nodes(l.Bus1) - # b2 = remove_nodes(l.Bus2) - - offset = 0 - skip = set() - - # norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu - # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu - # emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu - # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu - - vbs = None - if do_values == pqCurrent: - # Currently the same as pqCapacity to match the OpenDSS impl.; the correct would be: - #max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllMaxCurrents(True))) - try: - max_currents = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) - except: - max_currents = {} - elem = DSS.ActiveCircuit.ActiveCktElement - for _ in DSS.ActiveCircuit.PDElements: - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - norm_amps = elem.NormalAmps - max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 - - elif do_values == pqCapacity: - try: - capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) - except: - max_currents = {} - elem = DSS.ActiveCircuit.ActiveCktElement - for _ in DSS.ActiveCircuit.PDElements: - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - norm_amps = elem.NormalAmps - max_currents[elem.Name] = (100 * max_current / norm_amps) if norm_amps else 0.0 - - elif do_values == pqVoltage: - node_volts = dict(zip(DSS.ActiveCircuit.AllNodeNames, asarray(DSS.ActiveCircuit.AllBusVmag) * 1e-3)) - vbs = np.empty(shape=(line_count, ), dtype=np.float64) - vbs.fill(0) - extra.append(vbs) - - if idxs: - l = branch_objects - for idx in idxs: - l.idx = idx - buses = element.BusNames - b1 = remove_nodes(buses[0]) - b2 = remove_nodes(buses[1]) - - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None: - skip.add(idx) - continue - - lines[offset, 0] = fr - lines[offset, 1] = to - offset += 1 - - if do_values == pqNone: - return lines[:offset] - - offset = 0 - for idx in idxs: - if idx in skip: - continue - - l.idx = idx - - if do_values == pqPower: - values[offset] = np.abs(element.TotalPowers[0]) - elif do_values == pqLosses: - values[offset] = abs(element.Losses[0]) / l.Length - elif do_values == pqVoltage: - b2name = nodot(l.Bus2) - b = DSS.ActiveCircuit.Buses[b2name] - vb = b.kVBase - vbs[offset] = vb - value = 1e30 - if vb > 0: - for n in b.Nodes: - if n > 0 and n <= 3: - value = min(value, node_volts[f'{b2name}.{n}'] / vb) - - values[offset] = value - elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.nan) - elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.nan) - - offset += 1 - - return lines[:offset], values[:offset] - - else: - for i, l in enumerate(branch_objects): - buses = element.BusNames - b1 = remove_nodes(buses[0]) - b2 = remove_nodes(buses[1]) - - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None or not element.Enabled: - skip.add(i) - continue - - if do_switches: - if ((has_is_isolated and element.IsIsolated) or - ((not has_is_isolated) and (element.Name.lower() in isolated_names))): - isolated_idxs.append(offset) - - if l.IsSwitch: - #skip.add(i) - switch_idxs.append(offset) - #continue - - lines[offset, 0] = fr - lines[offset, 1] = to - - offset += 1 - - if do_values == pqNone: - return [lines[:offset], None, None] + extra - - offset = 0 - - for i, l in enumerate(branch_objects): - if i in skip: - continue - - if do_values == pqPower: - values[offset] = np.abs(element.TotalPowers[0]) - elif do_values == pqLosses: - values[offset] = abs(element.Losses[0]) / l.Length - elif do_values == pqVoltage: - b2name = nodot(l.Bus2) - b = DSS.ActiveCircuit.Buses[b2name] - vb = b.kVBase - vbs[offset] = vb - value = 1e30 - - if l.Phases < 3: - lines_styles[offset] = 1 - - if vb > 0: - for n in b.Nodes: - if n > 0 and n <= 3: - value = min(value, node_volts[f'{b2name}.{n}'] / vb) - - values[offset] = value - elif do_values == pqCurrent: - values[offset] = max_currents.get(element.Name, np.nan) - elif do_values == pqCapacity: - values[offset] = capacities.get(element.Name, np.nan) - - lines_styles[offset] = single_ph_line_style if l.Phases == 1 else three_ph_line_style - offset += 1 - - return [lines[:offset], values[:offset], lines_styles[:offset]] + extra - - - def _get_point_data(DSS: IDSS, - point_objects: Union[str, Iterable], - bus_coords: Dict[str, Tuple[float, float, float]], - do_values: bool = False - ): - if isinstance(point_objects, str): - cls = point_objects - DSS.SetActiveClass(cls) - point_objects = DSS.ActiveClass - - point_count = point_objects.Count - - points = np.empty(shape=(point_count, 2), dtype=np.float64) - values = np.empty(shape=(point_count, ), dtype=np.float64) - - offset = 0 - skip = set() - element = DSS.ActiveCircuit.ActiveCktElement - for i, _ in enumerate(point_objects): - buses = element.BusNames - all_coords = [] - buses = [remove_nodes(b) for b in buses] - all_coords = [c for c in (bus_coords.get(b) for b in buses) if c] - - if not all_coords: - skip.add(i) - continue - - coords = tuple(sum(c) / len(all_coords) for c in zip(*all_coords)) - - points[offset] = coords - offset += 1 - - if not do_values: - return points[:offset] - - offset = 0 - for i, _ in enumerate(point_objects): - if i in skip: - continue - - values[offset] = np.abs(element.TotalPowers[0]) - offset += 1 - - return points[:offset], values[:offset] - - - def dss_profile_plot(DSS: IDSS, - *, - PhasesToPlot: int = None, - ProfileScale: float = None, - **kwargs: Unpack[PlotParams] - ): - if len(DSS.ActiveCircuit.Meters) == 0: - raise RuntimeError(f"An EnergyMeter is required to use 'plot profile'") - - vmin = DSS.ActiveCircuit.Settings.NormVminpu - vmax = DSS.ActiveCircuit.Settings.NormVmaxpu - if ProfileScale == '120kft': - xlabel = 'Distance (kft)' - ylabel = '120 Base Voltage' - DenomLN = 1.0 / 120.0 - # DenomLL = 1.732 / 120.0 - LenScale = 3.2809 - # RangeScale = 120.0 - else: - xlabel = 'Distance (km)' - ylabel = 'p.u. Voltage' - DenomLN = 1.0 - # DenomLL = 1.732 - LenScale = 1.0 - # RangeScale = 1.0 - - busnode_to_index = {(bn.rsplit('.', 1)[0], int(bn.rsplit('.', 1)[1])): num for (num, bn) in enumerate(DSS.ActiveCircuit.AllNodeNames)} - bus_to_kvbase = {b.Name: b.kVBase for b in DSS.ActiveCircuit.Buses} - puV = asarray(DSS.ActiveCircuit.AllBusVmagPu) / DenomLN - distances = {name: d for (name, d) in zip(DSS.ActiveCircuit.AllBusNames, asarray(DSS.ActiveCircuit.AllBusDistances) * LenScale)} - linewidths = [] - segments = [] - colors = [] - linestyles = [] - seg_phases = [] - pri_only = (PhasesToPlot == DSSPlotPhases.PROFILEALLPRI) - if PhasesToPlot in [DSSPlotPhases.PROFILEALL, DSSPlotPhases.PROFILEALLPRI, DSSPlotPhases.PROFILE3PH]: - phases = (1, 2, 3) - else: - phases = PhasesToPlot - try: - _ = iter(phases) - except: - phases = [phases] - - for em in DSS.ActiveCircuit.Meters: - branch_names = em.AllBranchesInZone - br: str - for br in branch_names: - if not br.startswith('Line.'): - continue - - ls = '-' - lw = 2 - - DSS.ActiveCircuit.Lines.Name = br[len('Line.'):] - - if DSSPlotPhases.PROFILE3PH == PhasesToPlot and DSS.ActiveCircuit.Lines.Phases < 3: - continue - - bus1 = nodot(DSS.ActiveCircuit.Lines.Bus1) - bus2 = nodot(DSS.ActiveCircuit.Lines.Bus2) - - # Plot all phases present (between 1 and 3) - for iphs in phases: - try: - b1n_idx = busnode_to_index[(bus1, iphs)] - b2n_idx = busnode_to_index[(bus2, iphs)] - except: - continue - - if bus_to_kvbase[bus1] < 1.0: - if pri_only: - continue - ls = ':' - lw = 1 - - segments.append(((distances[bus1], puV[b1n_idx]), (distances[bus2], puV[b2n_idx]))) - colors.append(Colors[iphs - 1]) - seg_phases.append(iphs) - linestyles.append(ls) - linewidths.append(lw) - #TODO: NodeMarkerCode, NodeMarkerWidth - - if include_3d in ('both', '2d'): - fig = plt.figure()#figsize=(9, 5)) - ax = fig.add_subplot(1, 1, 1) - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): - ax.set_title('L-L Voltage Profile') - else: - ax.set_title('L-N Voltage Profile') - - - lc = LineCollection(segments, linewidth=linewidths, colors=colors, linestyles=linestyles) - - # ax.set_title('{}:{}, max: {:3g}'.format(DSS.ActiveCircuit.Name, quantity, quantity_max_value)) - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - ax.add_collection(lc) - ax.autoscale_view() - ax.axhline(vmin, color='darkred', ls='-', lw=3) - ax.axhline(vmax, color='darkred', ls='-', lw=3) - ax.grid(ls='--') - plt.tight_layout() - - if include_3d in ('both', '3d'): - fig2 = plt.figure()#figsize=(7, 7)) - ax2 = fig2.add_subplot(1, 1, 1, projection='3d') - ax2.set_xlabel(xlabel) - ax2.set_ylabel(ylabel) - if PhasesToPlot in (DSSPlotPhases.PROFILELL3PH, DSSPlotPhases.PROFILELLALL, DSSPlotPhases.PROFILELLPRI): - ax2.set_title('L-L Voltage Profile') - else: - ax2.set_title('L-N Voltage Profile') - - segments_3d = [ - [(*p, ph) for p in seg] for seg, ph in zip(segments, seg_phases) - ] - rseg = np.ravel(segments) - max_x = np.max(rseg[::2]) - max_y = np.max(rseg[1::2]) - min_y = np.min(rseg[1::2]) - lc3d = Line3DCollection(segments_3d, colors=colors, linestyles=linestyles) - ax2.add_collection(lc3d) - ax2.set_xlabel(xlabel) - ax2.set_ylabel(ylabel) - ax2.set_zlabel('Phase') - xl = [0, max_x] - yl = [min(min_y, vmin) - 0.05, min(max_y, vmax) + 0.05] - maxph = np.max(seg_phases) + 1 - ax2.set_xlim(xl) - ax2.set_ylim(yl) - ax2.set_zlim(0, maxph) - ax2.plot_surface( - np.array([xl, xl]), - np.array([[vmax, vmax]] * 2), - np.array([[0, 0], [maxph, maxph]]), - color='k', - alpha=0.5 - ) - ax2.plot_surface( - np.array([xl, xl]), - np.array([[vmin, vmin]] * 2), - np.array([[0, 0], [maxph, maxph]]), - color='k', - alpha=0.5 - ) - ax2.autoscale_view() - - - def _get_gic_line_data_altdss( - altdss: IAltDSS, - bus_coords: Dict[str, Tuple[float, float, float]], - single_ph_line_style: int = 1, - three_ph_line_style: int = 1 - ): - branch_objects = altdss.GICLine - line_count = len(branch_objects)# if not idxs else len(idxs) - lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) - lines.fill(np.nan) - values = np.empty(shape=(line_count, ), dtype=np.float64) - values.fill(np.nan) - lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) - offset = 0 - # skip = set() - - # GIC lines are not exposed nicely in the classic API, so we'll use the new Obj API - for gic_line in altdss.GICLine: - if not gic_line.enabled: - continue - - b1 = remove_nodes(gic_line.bus1) - b2 = remove_nodes(gic_line.bus2) - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None: - # skip.add(idx) - continue - - lines[offset, 0] = fr - lines[offset, 1] = to - - lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style - values[offset] = gic_line.MaxCurrent(1) - offset += 1 - - return lines[:offset], values[:offset], lines_styles[:offset] - - - def _get_gic_line_data(DSS: IDSS, - bus_coords: Dict[str, Tuple[float, float]], - single_ph_line_style: int = 1, - three_ph_line_style: int = 1 - ): - try: - return _get_gic_line_data_altdss( - DSS.to_altdss(), - bus_coords, - single_ph_line_style=single_ph_line_style, - three_ph_line_style=three_ph_line_style - ) - except: - pass - - # Fallback for Oddie and COM - DSS.ActiveCircuit.SetActiveClass('GICLine') - aclass = DSS.ActiveCircuit.ActiveClass - line_count = aclass.Count# if not idxs else len(idxs) - lines = np.empty(shape=(line_count, 2, 2), dtype=np.float64) - lines.fill(np.nan) - values = np.empty(shape=(line_count, ), dtype=np.float64) - values.fill(np.nan) - lines_styles = np.zeros(shape=(line_count,), dtype=np.int8) - offset = 0 - # skip = set() - - # GIC lines are not exposed nicely in the classic API - elem = DSS.ActiveCircuit.ActiveCktElement - idx = aclass.First - while idx != 0: - buses = elem.BusNames - b1 = remove_nodes(buses[0]) - b2 = remove_nodes(buses[1]) - fr = bus_coords.get(b1) - to = bus_coords.get(b2) - - if fr is None or to is None: - # skip.add(idx) - continue - - lines[offset, 0] = fr - lines[offset, 1] = to - - lines_styles[offset] = single_ph_line_style if gic_line.phases == 1 else three_ph_line_style - currents = np.abs(asarray(elem.Currents).view(dtype=complex)) - max_current = np.max(currents[:elem.NumConductors]) - values[offset] = max_current - offset += 1 - - return lines[:offset], values[:offset], lines_styles[:offset] - - - def dss_circuit_plot(DSS: IDSS, - *, - fig=None, - ax=None, - is3d=False, - Quantity: str = None, - Dots: bool = False, - Color1: str = None, - Color2: str = None, - Color3: str = None, - SinglePhLineStyle: int = None, - ThreePhLineStyle: int = None, - MaxLineThickness: float = None, - BusMarkers: List[BusMarker] = None, - Labels: bool = None, - Markers: ObjMarkers = None, - MaxScale: float = None, - MaxScaleIsSpecified: bool = None, - **kwargs: Unpack[PlotParams] - ): - if not MaxScaleIsSpecified: - MaxScale = None - - quantity = str_to_pq.get(Quantity, pqNone) - dots = Dots - color1 = Color1 - color2 = Color2 - color3 = Color3 - single_ph_line_style = SinglePhLineStyle - three_ph_line_style = ThreePhLineStyle - max_lw = MaxLineThickness - bus_markers = BusMarkers or [] - do_labels = Labels - - norm_min_volts = DSS.ActiveCircuit.Settings.NormVminpu - # norm_max_volts = DSS.ActiveCircuit.Settings.NormVmaxpu - emerg_min_volts = DSS.ActiveCircuit.Settings.EmergVminpu - # emerg_max_volts = DSS.ActiveCircuit.Settings.EmergVmaxpu - - # bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if (b.x, b.y) != (0.0, 0.0)) - bus_coords = dict((b.Name, (b.x, b.y)) for b in DSS.ActiveCircuit.Buses if b.Coorddefined) - - if fig is None: - fig = plt.figure()#figsize=(8, 7)) - - given_ax = ax is not None - if not given_ax: - ax = plt.gca() - else: - plt.sca(ax) - - if not is3d: - ax.set_aspect('equal', 'datalim') - - lines_lines, lines_values, lines_styles, switch_idxs, isolated_idxs, *extra = self._get_branch_data( - DSS, - DSS.ActiveCircuit.Lines, - bus_coords, - do_values=quantity, - do_switches=True, - single_ph_line_style=single_ph_line_style, - three_ph_line_style=three_ph_line_style - ) - - if isolated_idxs: - line_idx = isolated_idxs - if not is3d: - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#ff00ff', capstyle='round')) - - if switch_idxs: - line_idx = switch_idxs - if not is3d: - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle='-', color='#000000', capstyle='round')) - - switch_idxs = set(switch_idxs) - isolated_idxs = set(isolated_idxs) - #lc_lines = LineCollection(lines_lines, linewidths=0.5, color=color1)# + 3 * lines_values / np.max(lines_values), linestyle='solid', color=color1) - quantity_max_value = MaxScale if MaxScale is not None else 0.0 - - quantity_suffix = '' - - if lines_lines is not None and len(lines_lines) > 0: - if quantity in (pqVoltage,): - colors = [] - for v in lines_values: - if v > norm_min_volts or np.isnan(v): - colors.append(color1) - elif v > emerg_min_volts: - colors.append(color2) - else: - colors.append(color3) - - - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] - if not is3d: - edgecolors = [colors[i] for i in line_idx] - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=1, linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=edgecolors, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=edgecolors, s=9, lw=1) - - # if is3d: - # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) - # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) - # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) - - quantity_max_value = 0 - elif quantity in (pqLosses,): - - if quantity_max_value == 0: - # quantity_max_value = max(lines_values) * 1e-3 - # For compatibility with the official version, loop through all lines instead - # of the actual plotted lines - element = DSS.ActiveCircuit.ActiveCktElement - quantity_max_value = max( - abs(element.Losses[0] / line.Length) - for line in DSS.ActiveCircuit.Lines - if element.Enabled - ) * 0.001 - - lines_values = np.clip(3 * 1e-3 * lines_values / quantity_max_value, 0.5, max_lw) - if not is3d: - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] - # edgecolors = [colors[i] for i in line_idx] - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - - elif quantity in (pqCurrent, pqCapacity): - line_idx = [i for i in range(lines_lines.shape[0]) if i not in isolated_idxs and i not in switch_idxs] - colors = [color3 if v > 100 and not np.isnan(v) else color1 for v in lines_values[line_idx]] - - if quantity_max_value == 0: - quantity_max_value = max(lines_values) - - lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) - if not is3d: - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle='-', color=colors, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=colors, s=9, lw=1) - - elif quantity != pqNone: - if quantity == pqPower: - quantity_suffix = ' kW' - if quantity_max_value == 0: - #lines_values *= 1e-3 - - # For compatibility with the official version, loop through all lines instead - # of the actual plotted lines - element = DSS.ActiveCircuit.ActiveCktElement - - quantity_max_value = max( - element.TotalPowers[0] - for _ in DSS.ActiveCircuit.Lines - if element.Enabled - ) #* 0.001 - else: - #TODO:may need workaround about GeneralPlotQuantity - quantity_max_value = max(lines_values) - - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls and i not in isolated_idxs and i not in switch_idxs] - if not is3d: - ax.add_collection(LineCollection( - lines_lines[line_idx, :], - linewidths=np.clip(0.5 + 3 * lines_values[line_idx] / quantity_max_value, 0.5, max_lw), - linestyle=LINES_STYLE_CODE.get(ls, 'solid'), - color=color1, - capstyle='round' - )) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - else: - #TODO: handle 1 and 3 phase, etc.? - if not is3d: - ax.add_collection(LineCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) - # else: - # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=color1, capstyle='round')) - # ax.set_xlim(np.min(lines_lines[:, :, 0]), np.max(lines_lines[:, :, 0])) - # ax.set_ylim(np.min(lines_lines[:, :, 1]), np.max(lines_lines[:, :, 1])) - - transformers_lines, *_ = self._get_branch_data(DSS, DSS.ActiveCircuit.Transformers, bus_coords) - - if not is3d: - lc_transformers = LineCollection(transformers_lines, linewidth=3, linestyle='solid', color='gray') - ax.add_collection(lc_transformers) - - lines_lines, lines_values, lines_styles, *_ = self._get_gic_line_data(DSS, bus_coords, single_ph_line_style=single_ph_line_style, three_ph_line_style=three_ph_line_style) - if len(lines_lines) != 0: - if quantity_max_value == 0: - quantity_max_value = max(lines_values) - - lines_values = np.clip(3 * lines_values / quantity_max_value, 0.5, max_lw) - for ls in set(lines_styles): - line_idx = [i for i, c in enumerate(lines_styles) if c == ls] - ax.add_collection(LineCollection(lines_lines[line_idx, :], linewidths=lines_values[line_idx], linestyle=LINES_STYLE_CODE.get(ls, 'solid'), color=color1, capstyle='round')) - if dots: - ax.scatter(lines_lines[line_idx, 0, 0].ravel(), lines_lines[line_idx, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - ax.scatter(lines_lines[line_idx, 1, 0].ravel(), lines_lines[line_idx, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=color1, s=9, lw=1) - - - - # 'Daisysize' - # 'Markercode', 'Nodewidth' # NodeMarkerCode - - branch_marker_options = [ - ('MarkSwitches', 'SwitchMarkerCode', None, DSS.ActiveCircuit.Lines, switch_idxs), - ('MarkFuses', 'FuseMarkerCode', 'FuseMarkerSize', DSS.ActiveCircuit.Fuses, None), - ('MarkRegulators', 'RegMarkerCode', 'RegMarkerSize', DSS.ActiveCircuit.RegControls, None), - ('MarkRelays', 'RelayMarkerCode', 'RelayMarkerSize', DSS.ActiveCircuit.Relays, None), - ('MarkReclosers', 'RecloserMarkerCode', 'RecloserMarkerSize', DSS.ActiveCircuit.Reclosers, None) - ] - - point_marker_options = [ - ('MarkTransformers', 'TransMarkerCode', 'TransMarkerSize', DSS.ActiveCircuit.Transformers, None), - ('MarkCapacitors', 'CapMarkerCode', 'CapMarkerSize', DSS.ActiveCircuit.Capacitors, None), - ('MarkPVSystems', 'PVMarkerCode', 'PVMarkerSize', DSS.ActiveCircuit.PVSystems, None), - ('MarkStorage', 'StoreMarkerCode', 'StoreMarkerSize', 'Storage', None), - ] - - pmarkers = Markers - if pmarkers is not None: - for (mark_opt, code_opt, size_opt, objs, idxs) in branch_marker_options: - # print(mark_opt, pmarkers[mark_opt]) - if not pmarkers[mark_opt]: - continue - - marker_code = pmarkers[code_opt] - marker_size = pmarkers[size_opt] - #TODO: use marker_size? - marker_dict = get_marker_dict(marker_code) - if mark_opt == 'MarkRegulators': - for obj in objs: - DSS.ActiveCircuit.Transformers.Name = obj.Transformer - bus = remove_nodes(DSS.ActiveCircuit.ActiveCktElement.BusNames[obj.Winding - 1]) - coords = bus_coords.get(bus) - if coords is None: - continue - ax.plot(*coords, color='red', **marker_dict) - - else: - #TODO? branch_lines = self._get_branch_data(DSS, objs, bus_coords, idxs=idxs) - pass - - - for (mark_opt, code_opt, size_opt, objs, idxs) in point_marker_options: - if not pmarkers[mark_opt]: - continue - - marker_code = pmarkers[code_opt] - marker_size = pmarkers[size_opt] - - points = self._get_point_data(DSS, objs, bus_coords) - - # if marker_code not in MARKER_MAP: - #marker_code = 25 - - marker_dict = get_marker_dict(marker_code) - #marker_dict['markersize'] *= (marker_size / 2.0)**2 - marker_dict['markersize'] *= (marker_size / 1.2)**2 - - #marker_dict['marker'] = marker_dict['marker'].vertices - #marker_dict.pop('markersize') - #marker_dict.pop('markerfacecolor') - # print(mark_opt, marker_dict['marker']) - # pprint(marker_dict) - ax.plot(points[:, 0], points[:, 1], ls='', color='red', **marker_dict) - #ax.plot(points[:, 0], points[:, 1], color='red', ls='', marker=6, alpha=1) - - for bus_marker in bus_markers: - name = bus_marker['Name'] - bus = DSS.ActiveCircuit.Buses[name] - if not bus.Coorddefined: - raise RuntimeError('Bus markers: coordinates are not defined for bus "{name}"') - - marker_dict = get_marker_dict(bus_marker['Code']) - marker_size = bus_marker['Size'] - marker_dict['markersize'] *= (marker_size / 6) - ax.plot(bus.x, bus.y, ls='', color=bus_marker['Color'], **marker_dict) - - - ax.set_xlabel('X') - ax.set_ylabel('Y') - if not given_ax: - if quantity != pqNone: - ax.set_title('{}:{}, max={:g}{}'.format(DSS.ActiveCircuit.Name.upper(), quantity_str[quantity], quantity_max_value, quantity_suffix)) - ax.autoscale_view() - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - plt.tight_layout() - - if do_labels: - coords_to_names = {} - for name, coords in bus_coords.items(): - prev = coords_to_names.get(coords) - if prev: - coords_to_names[coords] = prev + ',' + name - else: - coords_to_names[coords] = name - - for coords, name in coords_to_names.items(): - ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - - - def dss_scatter_plot(DSS: IDSS, - **kwargs: Unpack[PlotParams] - ): - x = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) - y = np.empty(shape=(DSS.ActiveCircuit.NumBuses, )) - vcomplex = np.empty(shape=(DSS.ActiveCircuit.NumBuses, 3), dtype=complex) - x.fill(np.nan) - y.fill(np.nan) - vcomplex.fill(np.nan) - for idx, b in enumerate(DSS.ActiveCircuit.Buses): - if not b.Coorddefined: - continue - - x[idx] = b.x - y[idx] = b.y - vnodes = asarray(b.puVoltages).view(dtype=complex) - nnodes = min(3, len(vnodes)) - vcomplex[idx, :nnodes] = vnodes[:nnodes] - - vabs = np.abs(vcomplex) - del vcomplex - with suppress_warnings(): - vmean = np.mean(vabs, axis=1, where=np.isfinite(vabs)) - - if include_3d in ('both', '2d'): - fig, ax = plt.subplots(1, 1, constrained_layout=True)#, figsize=(8, 7)) - dss_circuit_plot(DSS, fig=fig, ax=ax, Color1='k') - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - sc = ax.scatter(x, y, c=vmean) - fig.colorbar(sc, label='V1 (pu)') - ax.set_title('{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude')) - - if include_3d in ('both', '3d'): - bus_coords = {} - for idx, b in enumerate(DSS.ActiveCircuit.Buses): - if b.Coorddefined: - bus_coords[b.Name] = (b.x, b.y, vmean[idx]) - - fig = plt.figure()#figsize=(7, 7)) - ax = fig.add_subplot(projection='3d') - dss_circuit_plot(DSS, fig=fig, ax=ax, is3d=True, Color1='k') - ax.get_xaxis().get_major_formatter().set_scientific(False) - ax.get_yaxis().get_major_formatter().set_scientific(False) - - # if is3d: - # ax.add_collection(Line3DCollection(lines_lines, linewidths=1, linestyle='-', color=[colors[i] for i in line_idx], capstyle='round')) - # ax.set_xlim(np.min(lines_lines_3d[:, :, 0]), np.max(lines_lines_3d[:, :, 0])) - # ax.set_ylim(np.min(lines_lines_3d[:, :, 1]), np.max(lines_lines_3d[:, :, 1])) - - sc = ax.scatter(x, y, vmean, c='k', s=2) - - segs = [] - el = DSS.ActiveCircuit.ActiveCktElement - for pd in DSS.ActiveCircuit.PDElements: - buses = el.BusNames - if len(buses) != 2: - continue - - seg = [] - for b in buses: - c = bus_coords.get(nodot(b), None) - if c is not None: - seg.append(c) - - if len(seg) == 2: - segs.append(seg) - - segs = np.array(segs, dtype=float) - seg_v = (segs[:, 0, 2] + segs[:, 1, 2]) / 2 - lc3d = Line3DCollection(segs) - ax.add_collection(lc3d) - lc3d.set_array(seg_v) - #fig.colorbar(sc, label='V1 (pu)') - ax.set_title('{}:{}'.format(DSS.ActiveCircuit.Name.upper(), 'Voltage magnitude')) - - - def dss_visualize_plot(DSS: IDSS, - *, - Quantity: str = None, - ElementType: str = None, - ElementName: str = None, - **kwargs: Unpack[PlotParams] - ): - XMAX = 300 - #pprint(kwargs) - quantity = Quantity - - # Fix for backend v0.13.1 - quantity = { - 'Power': 'Powers', - 'Current': 'Currents', - 'Voltage': 'Voltages', - }.get(quantity, quantity) - - element = DSS.ActiveCircuit.ActiveCktElement - etype, ename = ElementType, ElementName - nconds = element.NumConductors - # nphases = element.NumPhases - buses = element.BusNames[:2] # max 2 terminals - vbases = [max(1, 1000 * DSS.ActiveCircuit.Buses[nodot(b)].kVBase) for b in buses] - - # assert DSS.ActiveCircuit.ActiveCktElement.Name == ElementType + '.' + ElementName - fig, ax = plt.subplots(1, gridspec_kw=dict(left=0.05, right=0.95, bottom=0.05, top=0.92))#, figsize=(8.6, 7)) - ax.get_xaxis().set_visible(False) - ax.get_yaxis().set_visible(False) - ax.grid(False) - - y = 20 + 10 * nconds - box_xy0 = np.array([100, 10]) - box_xy1 = np.array([XMAX - 100, y]) - box_wh = box_xy1 - box_xy0 - middle_box = patches.Rectangle(box_xy0, *box_wh, facecolor='lightgray', edgecolor='k') - ax.text(XMAX / 2, 10 + (y - 10) / 2, f'{etype}.{ename.upper()}', ha='center', va='center', fontweight='bold', rotation='vertical') - ax.add_patch(middle_box) - ax.plot([0, 300], [0, 0], color='gray', lw=7) - - ax.plot([-5] * 2, [5, y - 5], color='k', lw=7) - ax.text(25, y, buses[0].upper(), ha='left') - if len(buses) > 1: - ax.plot([XMAX + 5] * 2, [5, y - 5], color='k', lw=7) - ax.text(XMAX - 25, y, buses[1].upper(), ha='right') - - voltage = (quantity == 'Voltages') - - if quantity == 'Powers': - values = 1e-3 * (asarray(element.Voltages).view(dtype=complex) * np.conj(asarray(element.Currents).view(dtype=complex))) - unit = 'kVA' - elif voltage: - values = asarray(element.Voltages).view(dtype=complex) - unit = 'pu' - elif quantity == 'Currents': - values = asarray(element.Currents).view(dtype=complex) - unit = 'A' - - ax.set_title(f'{etype}.{ename.upper()} {quantity} ({unit})') - size = 'x-small' - - def _get_text(): - v = values[bus_idx * nconds + cond] - if quantity == 'Powers': - arrow_text = f"{v.real:-.6g} {'-' if v.imag < 0 else '+'} j{abs(v.imag):g}" - else: - if quantity == 'Voltages': - v /= vbase - arrow_text = f"{np.abs(v):-.6g} {unit} ∠ {np.angle(v, deg=True):.2f}°" - - return arrow_text - - for bus_idx, vbase in enumerate(vbases): - for cond in range(nconds): - if cond < (nconds - 1): - weight = 'bold' - lw = 2 - else: - weight = 'normal' - lw = 0.6667 - - if bus_idx: - arrow_x = XMAX + 5 - arrow_y = y - (cond + 1) * 10.0 - dx = box_xy1[0] - arrow_x - ax.text(arrow_x - 20, arrow_y + 2, _get_text(), ha='right', fontweight=weight, size=size) - if voltage: - plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) - x = XMAX - 4 * (cond) - 1 - ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) - else: - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) - - else: - arrow_x = -5 - arrow_y = y - (cond + 1) * 10.0 - dx = box_xy0[0] + 5 - ax.text(arrow_x + 20, arrow_y + 2, _get_text(), ha='left', fontweight=weight, size=size) - if voltage: - plt.plot([arrow_x, dx + arrow_x], [arrow_y, arrow_y], color='k', lw=lw*1.5) - x = 4 * (cond) + 1 - ax.annotate('', xy=(x, arrow_y), xytext=(x, 0), arrowprops=dict(width=0.2, color='lightgray')) - else: - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=lw, color='k')) - - if quantity == 'Currents': - # Residual - v = -np.sum(values[(nconds * bus_idx):(nconds * (bus_idx + 1))]) - txt = f"{np.abs(v):-.6g} A ∠ {np.angle(v, deg=True):.2f}°" - - if bus_idx: - arrow_x = XMAX + 5 - arrow_y = -10 - dx = box_xy1[0] - arrow_x - ax.text(arrow_x - 5, arrow_y + 2, txt, ha='right', fontweight='normal', size=size) - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) - else: - arrow_x = -5 - arrow_y = -10 - dx = box_xy0[0] + 5 - ax.text(arrow_x + 5, arrow_y + 2, txt, ha='left', fontweight='normal', size=size) - ax.annotate('', xytext=(arrow_x, arrow_y), xy=(dx + arrow_x, arrow_y), arrowprops=dict(width=1, color='k')) - - ax.set_xlim(-20, XMAX + 20) - ax.set_ylim(-15, y + 5) - - - def dss_general_data_plot(DSS: IDSS, - *, - PlotType: str = None, - ObjectName: str = None, - ValueIndex: int = None, - Color1: str = None, - Color2: str = None, - Labels: bool = None, - MinScaleIsSpecified: bool = None, - MaxScaleIsSpecified: bool = None, - MinScale: float = None, - MaxScale: float = None, - - **kwargs: Unpack[PlotParams] - ): - if not MaxScaleIsSpecified: - MaxScale = None - - if not MinScaleIsSpecified: - MinScale = None - - is_general = PlotType == 'GeneralData' - ValueIndex = max(1, ValueIndex - 1) - fn = ObjectName - do_labels = Labels - color1 = Color1 - color2 = Color2 - - # Whenever we add Pandas as a dependency, this could be - # rewritten to avoid all the extra/slow work - exp = re.compile('[,=\t]') - with open(fn, 'r') as f: - line = f.readline().rstrip() - field = exp.split(line)[ValueIndex].strip() #TODO: Is this right?! - f.seek(0) - # Find min and max - names, vals = [], [] - for line in f: - if not line: - continue - - data = exp.split(line) - name, val = data[0], data[ValueIndex] - if len(val): - names.append(name) - vals.append(float(val)) - - vals = np.asarray(vals) - min_val = np.min(vals) - max_val = np.max(vals) - - # Do some sanity checking on the numbers. Don't want to include negative numbers in autoadd plot - if not is_general: - if min_val < 0.0: - min_val = 0.0 - if max_val < 0.0: - max_val = 0.0 - - if MaxScaleIsSpecified: - max_val = MaxScale # Override with user specified value - if MinScaleIsSpecified: - min_val = MinScale # Override with user specified value - - diff = max_val - min_val - if diff == 0.0: - diff = max_val - if diff == 0.0: - diff = 1.0 # Everything is zero - - sidxs = np.argsort(vals) - bus: IBus = DSS.ActiveCircuit.ActiveBus - data = [] - labels = [] - colors = [] - c1 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color1)) - c2 = np.asarray(matplotlib.colors.colorConverter.to_rgb(color2)) - for i in sidxs: - name, val = names[i], vals[i] - if DSS.ActiveCircuit.SetActiveBus(name) <= 0 or not bus.Coorddefined: - continue - - if is_general: - data.append((bus.x, bus.y, val)) - s = ((val - min_val) / diff) - colors.append(c2*s + c1*(1-s)) - # InterpolateGradientColor(Color1, Color2, (GenPlotItem.Value - MinValue) / Diff), - else: # ptAutoAddLogPlot - data.append((bus.x, bus.y, val)) - # GetAutoColor((GenPlotItem.Value - MinValue) / Diff), - - if do_labels: - labels.append(bus.Name) - - data = np.asarray(data) - - - dss_circuit_plot(DSS, **kwargs) - - #fig = plt.figure(figsize=(8, 7)) - plt.title(f'{field}, Max={max_val:.3g}') - ax = plt.gca() - #if not is3d: - #ax.set_aspect('equal', 'datalim') - - ax.scatter(data[:, 0], data[:, 1], c=colors, zorder=10) - # ax.colorbar() - - #ax.autoscale_view() - #ax.get_xaxis().get_major_formatter().set_scientific(False) - #ax.get_yaxis().get_major_formatter().set_scientific(False) - #plt.tight_layout() - - # marker_code = MarkerIdx - - # NodeMarkerWidth: int - # MarkerIdx = NodeMarkerCode - - # marker_code = pmarkers[code_opt] - # marker_size = pmarkers[size_opt] - #marker_dict = get_marker_dict(marker_code) - # ax.plot(*coords, color='red', **marker_dict) - #MarkSpecialClasses - - - def dss_matrix_plot(DSS: IDSS, - *, - MatrixType: str = None, - Color1: str = None, - **kwargs: Unpack[PlotParams] - ): - # plot_id = kwargs.get('PlotId', None) - if MatrixType == 'IncMatrix': - title = 'Incidence matrix' - data = DSS.ActiveCircuit.Solution.IncMatrix[:-1] - else: - title = 'Laplacian matrix' - data = DSS.ActiveCircuit.Solution.Laplacian[:-1] - - x, y, v = data[0::3], data[1::3], data[2::3] - m = coo.coo_matrix((v, (x, y))) - #fig, [ax, ax2] = plt.subplots(1, 2, figsize=(8.6 * 2, 8.6), constrained_layout=True, num=title) - - if include_3d in ('both', '2d'): - fig = plt.figure(constrained_layout=True)#, num=plot_id) #, figsize=(8.6, 8.6)) - ax = fig.add_subplot(1, 1, 1) - ax.grid(True) - ax.spy(m, marker='s', markersize=1, color=Color1) - ax.set_xlabel('Column') - ax.set_ylabel('Row') - ax.set_title(title) - - if include_3d in ('both', '3d'): - fig = plt.figure()#figsize=(8.6, 8.6), num=plot_id + '_3D') - ax2 = fig.add_subplot(1, 1, 1, projection='3d') - ax2.scatter(x, y, v, c=v, marker='s') - ax2.set_xlabel('Column') - ax2.set_ylabel('Row') - ax2.set_zlabel('Value') - - - def dss_daisy_plot(DSS: IDSS, - *, - DaisyBusList: List[str] = None, - Quantity: str = None, - Labels: bool = None, - DaisySize: float = None, - **kwargs: Unpack[PlotParams] - ): - dss_circuit_plot(DSS, **kwargs) - - # print(params['DaisySize']) - - ax = plt.gca() - XMIN, XMAX = ax.get_xlim() - quantity = str_to_pq.get(Quantity, pqNone) - daisy_bus_list = DaisyBusList - do_labels = Labels - daisy_size = DaisySize - - ax.set_title(f'Device Locations / {quantity_str[quantity]}') - element = DSS.ActiveCircuit.ActiveCktElement - - if len(daisy_bus_list) == 0: - for g in DSS.ActiveCircuit.Generators: - if element.Enabled: - daisy_bus_list.append(element.BusNames[0]) - - counts = np.zeros(shape=(DSS.ActiveCircuit.NumBuses + 1,), dtype=np.int32) - for b in daisy_bus_list: - idx = DSS.ActiveCircuit.SetActiveBus(b) - if idx > 0: - counts[idx] += 1 - - radius = 0.005 * daisy_size * (XMAX - XMIN) - lines = [] - pointx, pointy = [], [] - for bidx in np.nonzero(counts)[0]: - bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] - if not bus.Coorddefined: - continue - - cnt = counts[bidx] - angle0 = 0 - angle = np.pi * 2.0 / cnt - for j in range(cnt): - Xc = bus.x + 2 * radius * np.cos(angle * j + angle0) - Yc = bus.y + 2 * radius * np.sin(angle * j + angle0) - lines.append([(bus.x, bus.y), (Xc, Yc)]) - pointx.append(Xc) - pointy.append(Yc) - - - lc = LineCollection(lines, linewidth=1, colors='r') - ax.add_collection(lc) - ax.scatter(pointx, pointy, marker='o', color='yellow', edgecolors='red', s=100, zorder=10) - - if not do_labels: - return - - for bidx in np.nonzero(counts)[0]: - bus: IBus = DSS.ActiveCircuit.Buses[int(bidx)] - if not bus.Coorddefined: - continue - - ax.text(bus.x, bus.y, bus.Name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - - -def dss_di_plot(DSS: IDSS, - *, - CaseName: str = None, - MeterName: str = None, - Registers: List[int] = None, - CaseYear: str = None, - PeakDay: bool = None, - **kwargs: Unpack[PlotParams] -): - caseYear, caseName, meterName = CaseYear, CaseName, MeterName - plotRegisters, peakDay = Registers, PeakDay - - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', meterName + '.csv') - - if len(plotRegisters) == 0: - raise RuntimeError("No register indices were provided for DI_Plot") - - if not os.path.exists(fn): - fn = fn[:-4] + '_1.csv' - - # Whenever we add Pandas as a dependency, this could be - # rewritten to avoid all the extra/slow work - selected_data = [] - day_data = [] - mult = 1 if peakDay else 0.001 - - # If the file doesn't exist, let the exception raise - with open(fn, 'r') as f: - header = f.readline().rstrip() - allRegisterNames = [unquote(field) for field in header.strip().strip(' \t,').split(',')] - registerNames = [allRegisterNames[i] for i in plotRegisters] - - if not len(registerNames): - raise RuntimeError("Could not find any register name in the file") - - for line in f: - if not line: - continue - - rawValues = line.split(',') - selValues = [float(rawValues[0]), *(float(rawValues[i]) for i in plotRegisters)] - if not peakDay: - selected_data.append(selValues) - else: - day_data.append(selValues) - if len(day_data) == 24: - max_vals = [max(x) for x in zip(*day_data)] - max_vals[0] = day_data[0][0] - day_data = [] - selected_data.append(max_vals) - - if day_data: - max_vals = [max(x) for x in zip(*day_data)] - max_vals[0] = day_data[0][0] - day_data = [] - selected_data.append(max_vals) - - vals = np.asarray(selected_data, dtype=float) - fig, ax = plt.subplots(1) - icolor = -1 - for idx, name in enumerate(registerNames, start=1): - icolor += 1 - ax.plot(vals[:, 0], vals[:, idx] * mult, label=name, color=Colors[icolor % len(Colors)]) - - ax.set_title(f'{caseName}, Yr={caseYear}') - ax.set_xlabel('Hour') - ax.set_ylabel('MW, MWh or MVA') - ax.legend() - ax.grid() - - -def _plot_yearly_case(DSS: IDSS, caseName: str, meterName: str, plotRegisters: List[int], icolor: int, ax, registerNames: List[str]): - anyData = True - xvalues = [] - all_yvalues = [[] for _ in plotRegisters] - for caseYear in range(0, 21): - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'Totals_1.csv') - if not os.path.exists(fn): - continue - - with open(fn, 'r') as f: - f.readline() # Skip the header - # Get started - initialize Registers 1 - registerVals = [float(x) * 0.001 for x in f.readline().split(',')] - if len(registerVals): - xvalues.append(registerVals[7]) - - if len(xvalues) == 0: - raise RuntimeError('No data to plot') - - for caseYear in range(0, 21): - if meterName.lower() in ('totals', 'systemmeter', 'totals_1', 'systemmeter_1'): - suffix = '' if meterName.endswith('_1') else '_1' - meterName = meterName.lower().replace('totals', 'Totals').replace('systemmeter', 'SystemMeter') - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', f'{meterName}{suffix}.csv') - searchForMeterLine = False - else: - fn = os.path.join(DSS.DataPath, caseName, f'DI_yr_{caseYear}', 'EnergyMeterTotals_1.csv') - searchForMeterLine = True - - if not os.path.exists(fn): - continue - - with open(fn, 'r') as f: - header = f.readline() - if len(registerNames) == 0: - allRegisterNames = [unquote(field) for field in header.strip(' \t,').split(',')] - registerNames.extend(allRegisterNames[i] for i in plotRegisters) - - if not searchForMeterLine: - line = f.readline() - else: - for line in f: - label, rest = line.split(',', 1) - if label.strip().lower() == meterName.lower(): - line = f'{caseYear},{rest}' - else: - raise RuntimeError("Meter not found") - - registerVals = [float(x) * 0.001 for x in line.strip(' \t,').split(',')] - if len(registerVals): - for yvalues, idx in zip(all_yvalues, plotRegisters): - yvalues.append(registerVals[idx]) - - for yvalues, idx, regName in zip(all_yvalues, plotRegisters, registerNames): - marker_code = MARKER_SEQ[icolor % len(MARKER_SEQ)] - ax.plot(xvalues, yvalues, label=f'{caseName}:{meterName}:{regName}', color=Colors[icolor % len(Colors)], **get_marker_dict(marker_code)) - icolor += 1 - - return icolor - - -def dss_yearly_curve_plot(DSS: IDSS, *, - MeterName: str = None, - CaseNames: List[str] = None, - Registers: List[str] = None, - **kwargs: Unpack[PlotParams] -): - caseNames, meterName, plotRegisters = CaseNames, MeterName, Registers - - fig, ax = plt.subplots(1) - icolor = 0 - registerNames = [] - for caseName in caseNames: - icolor = _plot_yearly_case(DSS, caseName, MeterName, plotRegisters, icolor, ax, registerNames) - - if icolor == 0: - plt.close(fig) - raise RuntimeError('No files found') - - fig.suptitle(f"Yearly Curves for case(s): {', '.join(caseNames)}") - ax.set_title(f"Meter: {meterName}; Registers: {', '.join(registerNames)}", fontsize='small') - ax.set_xlabel('Total Area MW') - ax.set_ylabel('MW, MWh or MVA') - ax.legend() - ax.grid() - - -def dss_comparecases_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): - print('TODO: dss_comparecases_plot', kwargs) - - -def dss_zone_plot(DSS: IDSS, - *, - ObjectName: str, - Quantity: DSSPlotQuantity = DEFAULT_PLOT_PARAMS['Quantity'], - ShowLoops: bool = DEFAULT_PLOT_PARAMS['ShowLoops'], - Dots: bool = DEFAULT_PLOT_PARAMS['Dots'], - Labels: bool = DEFAULT_PLOT_PARAMS['Labels'], - Color1: str = DEFAULT_PLOT_PARAMS['Color1'], - Color3: str = DEFAULT_PLOT_PARAMS['Color3'], - SinglePhLineStyle: int = DEFAULT_PLOT_PARAMS['SinglePhLineStyle'], - ThreePhLineStyle: int = DEFAULT_PLOT_PARAMS['ThreePhLineStyle'], - MaxLineThickness: float = DEFAULT_PLOT_PARAMS['MaxLineThickness'], - MaxScale: float = DEFAULT_PLOT_PARAMS['MaxScale'], - **kwargs: Unpack[PlotParams] -): - obj_name = ObjectName - show_loops = ShowLoops - color1 = Color1 - color3 = Color3 - single_ph_line_style = LINES_STYLE_CODE.get(SinglePhLineStyle) - three_ph_line_style = LINES_STYLE_CODE.get(ThreePhLineStyle) - dots = Dots - do_labels = Labels - quantity = str_to_pq.get(Quantity, pqNone) - max_lw = MaxLineThickness - - if MaxScale is not None: - quantity_max_value = MaxScale - else: - quantity_max_value = 0 - - - ActiveCircuit = DSS.ActiveCircuit - - if obj_name: - ActiveCircuit.Meters.Name = obj_name - meters = [ActiveCircuit.Meters] - else: - meters = ActiveCircuit.Meters - - elem = ActiveCircuit.ActiveCktElement - line = ActiveCircuit.Lines - topo = ActiveCircuit.Topology - - icolor = 0 - - #TODO: check if/where we need to transform to lowercase. - bus_coords = dict((b.Name.lower(), (b.x, b.y)) for b in ActiveCircuit.Buses if b.Coorddefined) - - meter_marker_dict = get_marker_dict(24) - meter_marker_dict['markersize'] *= (3 / 3.5)**2 - - lines1, lines1_colors, labels1 = [], [], [] - lines3, lines3_colors, labels3 = [], [], [] - - # lw1, lw3 will initially hold the values, later transformed to actual widths - lw1, lw3 = [], [] - - if quantity in (pqCurrent, pqCapacity): - capacities = dict(zip(DSS.ActiveCircuit.PDElements.AllNames, DSS.ActiveCircuit.PDElements.AllPctNorm(True))) - - coords_to_names = {} - - def _name_coords(c, name): - prev = coords_to_names.get(c) - if prev is None: - coords_to_names[c] = name - return - elif prev == name: - return - - if prev.endswith(',' + name) or prev.startswith(name + ',') or (',' + name + ',') in prev: - return - - coords_to_names[c] = prev + ',' + name - - - def _add_line(element, color): - br_name = element.Name - bus1, bus2 = element.BusNames[:2] - bus1, bus2 = nodot(bus1).lower(), nodot(bus2).lower() - c1 = bus_coords.get(bus1) - c2 = bus_coords.get(bus2) - lw = 1 - if not c1 or not c2: - return None, None - - if do_labels: - _name_coords(c1, f'{bus1}({feeder_name})') - _name_coords(c2, f'{bus2}({feeder_name})') - - if quantity == pqPower: - lw = element.TotalPowers[0] - elif quantity == pqVoltage: - lw = 1 - elif quantity == pqLosses: - lw = 0 - try: - if element.Name.startswith('Line.'): - lw = 1e-3 * abs(element.Losses[0] / line.Length) - except: - pass - elif quantity in (pqCurrent, pqCapacity): - lw = capacities.get(element.Name, np.nan) - - if (element.NumPhases == 1): - lines1.append([c1, c2]) - lines1_colors.append(color) - labels1.append(br_name) - lw1.append(lw) - return lines1_colors, len(lines1_colors) - 1 - else: - lines3.append([c1, c2]) - lines3_colors.append(color) - labels3.append(br_name) - lw3.append(lw) - return lines3_colors, len(lines3_colors) - 1 - - - fig, ax = plt.subplots(1) - for meter in meters: - if not elem.Enabled: - continue - - feeder_name = meter.Name - branches = meter.AllBranchesInZone - if not branches: - continue - - # Meter marker - _ = topo.First - coords = bus_coords.get(elem.BusNames[meter.MeteredTerminal - 1]) - if coords: - plt.plot(*coords, color='red', **meter_marker_dict) - - feeder_color = color1 if show_loops else Colors[icolor % len(Colors)] - icolor += 1 - - br_idx = topo.First - while br_idx != 0: - if not elem.Enabled: - continue - - lcs, lidx = _add_line(elem, feeder_color) - if show_loops: - looped = (topo.LoopedBranch != 0) - if looped: - # The looped PDE is set as active by LoopedBranch - _add_line(elem, color3) - # Adjust the original to color3 - if lidx is not None: - lcs[lidx] = color3 - - br_idx = topo.Next - - - lw1 = np.asarray(lw1) - lw3 = np.asarray(lw3) - - if quantity_max_value == 0: - lw1_max_value = 0 - lw3_max_value = 0 - if len(lw1): - lw1_max_value = np.nanmax(lw1) - if np.isfinite(lw1_max_value): - quantity_max_value = max(quantity_max_value, lw1_max_value) - if len(lw3): - lw3_max_value = np.nanmax(lw3) - if np.isfinite(lw3_max_value): - quantity_max_value = max(quantity_max_value, lw3_max_value) - - if quantity_max_value == 0: - quantity_max_value = 1 - - lw1 = np.clip(3 * lw1 / quantity_max_value, 0.5, max_lw) - lw3 = np.clip(3 * lw3 / quantity_max_value, 0.5, max_lw) - lines1 = np.asarray(lines1) - lines3 = np.asarray(lines3) - lc1 = LineCollection(lines1, linewidth=lw1, colors=lines1_colors, linestyle=single_ph_line_style) - lc3 = LineCollection(lines3, linewidth=lw3, colors=lines3_colors, linestyle=three_ph_line_style) - ax.add_collection(lc1) - ax.add_collection(lc3) - if dots: - for lines, lc in ((lines1, lc1), (lines3, lc3)): - ax.scatter(lines[:, 0, 0].ravel(), lines[:, 0, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) - ax.scatter(lines[:, 1, 0].ravel(), lines[:, 1, 1].ravel(), marker='o', facecolors='none', edgecolors=lc, s=9, lw=1) - - ax.set_title(f'Meter Zone: {obj_name}' if obj_name else 'All Meter Zones') - - for coords, name in coords_to_names.items(): - ax.text(*coords, name, zorder=11, fontsize='xx-small', va='center', clip_on=True) - - ax.set_aspect('equal', 'datalim') - ax.autoscale() - - - -dss_plot_funcs = { - 'Scatter': dss_scatter_plot, - 'Daisy': dss_daisy_plot, - 'TShape': dss_tshape_plot, - 'PriceShape': dss_priceshape_plot, - 'LoadShape': dss_loadshape_plot, - 'Monitor': dss_monitor_plot, - 'Circuit': dss_circuit_plot, - 'Profile': dss_profile_plot, - 'Visualize': dss_visualize_plot, - 'YearlyCurve': dss_yearly_curve_plot, - 'Matrix': dss_matrix_plot, - 'GeneralData': dss_general_data_plot, - 'DI': dss_di_plot, -# 'CompareCases': dss_comparecases_plot, - 'MeterZones': dss_zone_plot -} - -def dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): - try: - ptype = kwargs['PlotType'] - if ptype not in dss_plot_funcs: - raise NotImplementedError(f'ERROR: not implemented plot type "{ptype}"') - return -1 - - with ToggleAdvancedTypes(DSS, False), warnings.catch_warnings(): - warnings.simplefilter("ignore") - return 0, dss_plot_funcs.get(ptype)(DSS, **kwargs) - - except Exception as ex: - from traceback import format_exc - # print('DSS: Error while plotting. Parameters:', kwargs, file=sys.stderr) - DSS._errorPtr[0] = 777 - DSS._lib.Error_Set_Description(f"Error in the plot backend: {ex}\n{format_exc()}".encode()) - return 777, None - - return 0, None - - -# dss_progress_bar = None -# dss_progress_desc = '' - - -@api_util.ffi.def_extern() -def dss_python_cb_write(ctx, message_str, message_type: int, message_size: int, message_subtype: int): - global dss_progress_bar - global dss_progress_desc - - # DSS = _ctx2dss(ctx) - - message_str = api_util.ffi.string(message_str).decode(api_util.codec) - if message_type == api_util.lib.DSSMessageType_Error: - #print('DSS Error:', message_str, file=sys.stderr) - pass - elif message_type in (api_util.lib.DSSMessageType_ProgressCaption, api_util.lib.DSSMessageType_ProgressFormCaption): - #dss_progress_desc = message_str - # print('Progress Caption:', message_str, file=sys.stderr) - pass - elif message_type == api_util.lib.DSSMessageType_Progress: - #print('DSS Progress:', message_str, file=sys.stderr) - pass - elif message_type == api_util.lib.DSSMessageType_FireOffEditor: - link_file(message_str) - # try: - # # print('DSSMessageType_FireOffEditor') - # with open(message_str, 'r') as f: - # text = f.read() - - # IPython.display.display({'text/plain': text}, raw=True) - # except: - # print(f'Could not display file "{message_str}"') - # return 1 - - elif message_type == api_util.lib.DSSMessageType_ProgressPercent: - try: - pass - # n = int(message_str) - # desc = '' - # if n == 0 and dss_progress_bar is not None: - # dss_progress_bar = None - - # if dss_progress_bar is None: - # dss_progress_bar = tqdm(total=100, desc=dss_progress_desc) - - # if n < 0: - # del dss_progress_bar - # dss_progress_bar = None - # return 0 - - - # dss_progress_bar.n = n - # dss_progress_bar.refresh() -# if n == 100: -# dss_progress_bar.close() - except: - import traceback - traceback.print_exc() - print('DSS Progress:', message_str) - - # else: - # # print(message_type) - # # print(message_str) - # IPython.display.display({'text/plain': message_str}, raw=True) - else: - # do nothing for now... - pass - - return 0 - - -@api_util.ffi.def_extern() -def dss_python_cb_plot(ctx, paramsStr): - params = json.loads(api_util.ffi.string(paramsStr)) - result = 0 - try: - DSS = IDSS._get_instance(ctx=ctx) - result, fig = dss_plot(DSS, **params) - if _do_show: - fig.show() - except: - from traceback import print_exc - print('DSS: Error while plotting. Parameters:', params, file=sys.stderr) - print_exc() - return 0 if result is None else result - -_original_allow_forms = None -_do_show = True -_enabled = False - -def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True, ctx: IDSS = None): - """ - Enables the plotting subsystem from DSS-Extensions. - - Set plot3d to `True` to try to reproduce some of the plots from the - alternative OpenDSS Visualization Tool / OpenDSS Viewer addition - to OpenDSS. - - Use `show` to control whether this backend should call `pyplot.show()` - or leave that to the system or the user. If the user plans to customize - the figure, it is better to set `show=False` in order to preserve the - figures, since `pyplot.show()` discards them. - """ - - global include_3d - global _original_allow_forms - global _do_show - global _enabled - global DSSPlotCtx - - if ctx is not None: - DSSPlotCtx = ctx - - _do_show = show - _enabled = True - - if plot3d and plot2d: - include_3d = 'both' - elif plot3d and not plot2d: - include_3d = '3d' - elif plot2d and not plot3d: - include_3d = '2d' - - api_util.lib.DSS_RegisterPlotCallback(api_util.lib.dss_python_cb_plot) - api_util.lib.DSS_RegisterMessageCallback(api_util.lib.dss_python_cb_write) - _original_allow_forms = DSSPlotCtx.AllowForms - DSSPlotCtx.AllowForms = True - -def disable(): - global _enabled - _enabled = False - api_util.lib.DSS_RegisterPlotCallback(api_util.ffi.NULL) - api_util.lib.DSS_RegisterMessageCallback(api_util.ffi.NULL) - if _original_allow_forms is not None: - DSSPlotCtx.AllowForms = _original_allow_forms - - -def plot_dsv(fn: Union[str, FilePath]): - return DSVHandler(fn).parse() - -__all__ = ['enable', 'disable', 'plot_dsv', ] From 3fbccca945bbf5f6706b528d4f94dcea0a03b50f Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:47:28 -0300 Subject: [PATCH 71/82] Update project metadata --- pyproject.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index df1d4b94..1dd0ac98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ packages = ["dss"] name = "dss-python" dynamic = ["version"] dependencies = [ - "dss_python_backend==0.14.6a1", + "dss_python_backend==0.15.0b1", "numpy>=2,<3", "typing_extensions>=4.5,<5", ] @@ -43,12 +43,10 @@ keywords = ["opendss", "altdss", "electric power systems", "opendssdirect", "pow classifiers = [ 'Intended Audience :: Science/Research', 'Intended Audience :: Education', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: CPython', 'Development Status :: 5 - Production/Stable', 'Topic :: Scientific/Engineering', From dfc517fe2d688f21cc7bc32196d620c7e289444c Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:54:15 -0300 Subject: [PATCH 72/82] Plotting: general updates and fixes (WIP) --- docs/examples/Plotting.ipynb | 262 ++++++++++++++++++++++++++++++++++- dss/IDSS.py | 27 ++-- dss/notebook.py | 37 +++-- dss/plot.py | 41 +++--- 4 files changed, 329 insertions(+), 38 deletions(-) diff --git a/docs/examples/Plotting.ipynb b/docs/examples/Plotting.ipynb index 004e26d5..0e941166 100644 --- a/docs/examples/Plotting.ipynb +++ b/docs/examples/Plotting.ipynb @@ -10,9 +10,11 @@ "source": [ "# Integrated plotting in Python\n", "\n", - "*Last major update: May, 2023* \n", + "*Last major update: Feb 2026 (added how to use EPRI's Delphi OpenDSS and C++ OpenDSS-C implementations)* \n", "*Original author: Paulo Meira*\n", "\n", + "***In this document:** How to use the plotting tools from DSS-Python (DSS-Extensions), OpenDSSDirect.py and AltDSS-Python. Example gallery.*\n", + "\n", "**Due to the high number of images, this notebook is stored in the Git repository without outputs.**\n", "\n", "You can open and then run this notebook on Google Colab for a quick overview if you don't want to set up a local environment: **[Open in Colab](https://colab.research.google.com/github/dss-extensions/dss_python/blob/master/docs/examples/Plotting.ipynb)**.\n", @@ -1504,6 +1506,262 @@ "# Toggle auto-showing back\n", "dss.Plotting.enable(show=True)" ] + }, + { + "cell_type": "markdown", + "id": "8e4a94e3-25cf-4c0d-aa1f-a3ff59e3dd69", + "metadata": {}, + "source": [ + "# .DSV files from EPRI's OpenDSS/OpenDSS-C\n", + "\n", + "EPRI's OpenDSS distributions, both Delphi OpenDSS and C++ OpenDSS-C, save the definition of the figure as a .DSV file, a custom format. An initial DSV parser that maps the DSV constructs to matplotlib is implemented in DSS-Python's plotting system.\n", + "\n", + "*Note: when using \"OpenDSS Viewer\", DSV files are not used.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c037c570-c2bf-4846-9f36-f47fe05c3e64", + "metadata": {}, + "outputs": [], + "source": [ + "from dss.plot import plot_dsv\n", + "\n", + "? plot_dsv" + ] + }, + { + "cell_type": "markdown", + "id": "534fccf6-decd-40a7-934c-5353dc28d5b1", + "metadata": {}, + "source": [ + "**Load the target library**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bd4a638-911a-4105-b04a-89994eae24f2", + "metadata": {}, + "outputs": [], + "source": [ + "from dss import IOddieDSS\n", + "dss = IOddieDSS('libOpenDSSC.so') # reminder: you might need the full path to the library/DLL here" + ] + }, + { + "cell_type": "markdown", + "id": "bbc342e1-6daa-4bd7-8a19-766ba73e3fac", + "metadata": {}, + "source": [ + "**Set it as the notebook target**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fab81e6-3ae8-438f-ac2f-a0aed090a683", + "metadata": {}, + "outputs": [], + "source": [ + "from dss import plot\n", + "plot.set_nb_target(dss)" + ] + }, + { + "cell_type": "markdown", + "id": "61fae66f-d14c-4c87-9fac-b7813e2ce2ec", + "metadata": {}, + "source": [ + "**Run the plot commands**\n", + "\n", + "Plot commands return the .DSV created as a result. By wrapping and checking each line, DSS-Python can then plot the resulting .DSV. Note that the commands need be run through the notebook directly, running a .DSS script that calls plot commands would not work." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72a18ecf-0733-4031-bfe9-726208fc0d7a", + "metadata": {}, + "outputs": [], + "source": [ + "%%dss\n", + "redirect electricdss-tst/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss\n", + "solve\n", + "visualize powers Transformer.xfm1" + ] + }, + { + "cell_type": "markdown", + "id": "2a9e7bd0-6546-4de5-8340-190acf4bec7f", + "metadata": {}, + "source": [ + "Of course, users can provide saved .DSVs instead of recreating them. For example, let's check where the result from one of the `visualize` commands:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b9b2da2-15ec-4fee-b4e2-db558bb1cdff", + "metadata": {}, + "outputs": [], + "source": [ + "dss('visualize powers Transformer.xfm1')\n", + "dsv_fn = dss.Text.Result\n", + "print(dsv_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "f0bbd70f-947e-421d-b2ea-0828d48f60fa", + "metadata": {}, + "source": [ + "If this `IEEE13Nodeckt_Transformer_xfm1_PQ.DSV` was archived, the user can them plot it directly, without running any simulation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1839947b-a595-4651-a85b-adc0d1c42116", + "metadata": {}, + "outputs": [], + "source": [ + "plot.dsv(dsv_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "ce4ce0aa-cee7-473c-a16b-078a0bde52df", + "metadata": {}, + "source": [ + "One of the reasons to load the DSV on DSS-Python would be to use matplotlib's capabilities to manipulate the figure. For this, just like with the other examples, just disable the automatic calls to `plt.show` (see [`matplotlib.pyplot.show`](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.show.html)):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c3c2a11-e8f4-4a48-b51b-3f5f6eb44570", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "plot.dsv(dsv_fn, show=False)\n", + "plt.annotate(\n", + " \"Look here!\",\n", + " xy=(0.2, 0.68),\n", + " xycoords='figure fraction',\n", + " xytext=(0.2, 0.75),\n", + " arrowprops=dict(width=2, headwidth=8),\n", + " fontweight='bold',\n", + " ha='center',\n", + " color='red'\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ac60c5c1-86d3-44e1-94dd-e4e06821f228", + "metadata": {}, + "source": [ + "# Experimental plotting API\n", + "\n", + "The code for the plotting tools has been refactored. A new class (`DSSPlotter`) is available and AltDSS-specific code was ported back to the classic OpenDSS API. Consequently, the new plotting tools can be used with all DSS engines (AltDSS, OpenDSS, and OpenDSS-C), either via DSS-Python/OpenDSSdirect.py **and** through EPRI's OpenDSS COM engine.\n", + "\n", + "That is, besides the previously mentioned .DSV tools, the new plotting API is an alternative. The plotting API does not use DSS commands, instead relying in Python calls instead.\n", + "\n", + "The method arguments are very close to the arguments passed to the DSS `plot` command. The arguments might change in a future release as the plotting API is finalized (i.e., exit the experimental stage)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd6ee5a1-01bc-4343-9fe2-b8bb9fafb4b6", + "metadata": {}, + "outputs": [], + "source": [ + "from dss import dss\n", + "dss('redirect electricdss-tst/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4d46c61-6d31-428f-83b0-490dd58dacef", + "metadata": {}, + "outputs": [], + "source": [ + "# Grab the default instance\n", + "plotter = dss.Plotting\n", + "\n", + "? plotter.circuit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "676488b6-dd2a-427e-a911-0c9f01f8d121", + "metadata": {}, + "outputs": [], + "source": [ + "plotter.circuit(Labels=True, Color1='red')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "514b7078-f0fb-4827-ace8-3c8b703079e4", + "metadata": {}, + "outputs": [], + "source": [ + "plotter.loadshape(ObjectName='default')" + ] + }, + { + "cell_type": "markdown", + "id": "37b39842-0625-4702-ab1f-406e981afa0c", + "metadata": {}, + "source": [ + "## Handling multiple engines and DSS contexts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1dbf765-98b3-44cf-b8f7-dc99055beeea", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the main AltDSS engine context\n", + "from dss import dss\n", + "plotter = dss.Plotting\n", + "dss.ClearAll()\n", + "dss.NewCircuit('circ1')\n", + "\n", + "# Create a new AltDSS context\n", + "dss2 = dss.NewContext()\n", + "dss2.ClearAll()\n", + "dss2.NewCircuit('circ2')\n", + "plotter2 = dss2.Plotting\n", + "dss2.ActiveCircuit.LoadShapes.Name = 'default'\n", + "dss2.ActiveCircuit.LoadShapes.Pmult = dss2.ActiveCircuit.LoadShapes.Pmult * 2\n", + "\n", + "# Create another context, this time loading EPRI's OpenDSS-C library\n", + "from dss import IOddieDSS\n", + "dss3 = IOddieDSS('libOpenDSSC.so') # reminder: you might need the full path to the library/DLL here\n", + "dss3.ClearAll()\n", + "dss3.AllowForms = False\n", + "dss3.NewCircuit('circ3')\n", + "plotter3 = dss3.Plotting\n", + "dss3.ActiveCircuit.LoadShapes.Name = 'default'\n", + "dss3.ActiveCircuit.LoadShapes.Pmult = dss3.ActiveCircuit.LoadShapes.Pmult * 3\n", + "\n", + "# Notice how each \"default\" LoadShape has different scales (1, 2, 3), as expected.\n", + "plotter.loadshape(ObjectName='default')\n", + "plotter2.loadshape(ObjectName='default')\n", + "plotter3.loadshape(ObjectName='default')" + ] } ], "metadata": { @@ -1522,7 +1780,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.13.12" } }, "nbformat": 4, diff --git a/dss/IDSS.py b/dss/IDSS.py index 524dfa5a..c65d92c9 100644 --- a/dss/IDSS.py +++ b/dss/IDSS.py @@ -51,6 +51,7 @@ class IDSS(Base): 'ZIP', '_version', '_altdss', + '_plotter', ] _columns = [ @@ -112,6 +113,7 @@ def __init__(self, api_util): api_util._dss_python = self self._version = None + self._plotter = None #: Provides access to the circuit attributes and objects in general. self.ActiveCircuit = ICircuit(api_util) @@ -505,21 +507,26 @@ def __call__(self, cmds: Union[AnyStr, List[AnyStr]]): @property def Plotting(self): ''' - Shortcut for the plotting module. This property is equivalent to: + Shortcut for the plotting tools for the current DSS engine. - ``` - from dss import plot - return plot - ``` + *Previously, this was just a shortcut to the plotting module. Since + the plotting tools were extended and refactored, an instance + of the DSS plotter is return, allowing the user to plot from + multiple instances and different engines.* - Gives access to the `enable()` and `disable()` functions. - Requires matplotlib and SciPy to be installed, hence it is an - optional feature. + Gives access to the `enable()`/`disable()` functions, and the new + experimental plotting API in Python. + + Requires matplotlib and SciPy to be installed, hence it is an optional + feature, lazily imported. **(API Extension)** ''' - from dss import plot - return plot + if self._plotter is None: + from dss.plot import get_plotter + self._plotter = get_plotter(self) + + return self._plotter @property def AdvancedTypes(self) -> bool: diff --git a/dss/notebook.py b/dss/notebook.py index eb299f99..3d569f36 100644 --- a/dss/notebook.py +++ b/dss/notebook.py @@ -20,6 +20,16 @@ class DSSMessageType(IntEnum): ShowTreeView = 11 + +def set_nb_target(dss: IDSS): + if not hasattr(dss, '_api_util'): + raise ValueError("A DSS engine context compatible with DSS-Python was expected.") + + plot.dss_nb_ctx = dss + if hasattr(dss, '_api_util') and not dss._api_util._is_oddie: + #TODO: save original state? + dss.AllowChangeDir = False + try: from IPython import get_ipython from IPython.display import FileLink, display, display_html, HTML @@ -44,19 +54,23 @@ def show(text): @register_cell_magic def dss(line, cell): - if isinstance(plot.DSSPlotCtx, IDSS) and not plot.DSSPlotCtx._api_util._is_oddie: - plot.DSSPlotCtx.Text.Commands(cell) + dss_ctx = plot.dss_nb_ctx + if dss_ctx is None: + raise ValueError("Invalid DSS-Python instance registered.") + return + + plotter = plot.get_plotter(dss_ctx) + if isinstance(dss_ctx, IDSS) and not dss_ctx._api_util._is_oddie: + dss_ctx.Text.Commands(cell) else: for line in cell.split('\n'): - plot.DSSPlotCtx(line) - res = plot.DSSPlotCtx.Text.Result - if res.endswith('.DSV'): - if _enabled and FilePath(res).exists(): - plot_dsv(res) + dss_ctx(line) + res = dss_ctx.Text.Result + if res.lower().endswith('.dsv'): + plotter.dsv(res) + + set_nb_target(plot.dss_nb_ctx) - if isinstance(plot.DSSPlotCtx, IDSS) and not plot.DSSPlotCtx._api_util._is_oddie: - #TODO: save original state? - plot.DSSPlotCtx.AllowChangeDir = False except: def link_file(fn): print(f'Output file: "{fn}"') @@ -143,3 +157,6 @@ def dss_python_cb_write(ctx, message_str, message_type: int, message_size: int, pass return 0 + + +__all__ = ['set_nb_target', ] \ No newline at end of file diff --git a/dss/plot.py b/dss/plot.py index 2f757caa..79ff1fe3 100644 --- a/dss/plot.py +++ b/dss/plot.py @@ -23,7 +23,7 @@ from dss_python_backend import loader_lib from . import api_util -from . import DSS as DSSPlotCtx +from . import DSS as dss_nb_ctx from ._cffi_api_util import AltDSSAPIUtil, Iterable as DSSIterable from .IDSS import IDSS from .IBus import IBus @@ -670,7 +670,7 @@ def parse_and_plot(self): getattr(self, item_name)(rest[0] if rest else '') # let the exception propagate on error if self._do_show: - self.fig.show() + plt.show(self.fig) else: return self.fig, self.ax @@ -687,6 +687,23 @@ def __init__(self, dss: IDSS): self._do_show = True self._enabled = False + + def dsv(self, fn: Union[str, FilePath], show: Optional[bool] = None): + """ + Plot a .DSV file from OpenDSS. This is the format that the classic `DSSView.exe` uses. + + `DSSView.exe` is the default plotting utility distributed with OpenDSS. As such, one + can use the OpenDSS GUI for some analysis, plot some results, and then replot then + using DSS-Python to further manipulate the resulting figures. + + *The new OpenDSS Viewer, a separate download, does not use the .DSV format.* + + If `show` is `None`, the "show" setting from the default plotter will be used as default. + Users can provide `show=True` or `show=False` directly to override the behavior. Useful + especially when only plotting DSVs, without further features from the DSS plotter. + """ + return DSVHandler(fn, show=self._do_show if show is None else show).parse_and_plot() + def monitor(self, *, ObjectName: str = None, @@ -2613,7 +2630,7 @@ def _dss_plot(DSS: IDSS, **kwargs: Unpack[PlotParams]): func = getattr(plotter, dss_plot_methods.get(ptype)) fig = func(**kwargs) if plotter._do_show and fig is not None: - fig.show() + plt.show(fig) return 0 @@ -2643,15 +2660,6 @@ def _dss_python_cb_plot(ctx, paramsStr): return 0 if result is None else result -def plot_dsv(fn: Union[str, FilePath], show=True): - """ - Plot an OpenDSS DSV file. - - When passing `show=False`, the user can modify the figure before showing it, - using Matplotlib's API. In that case, the function returns a tuple `(figure, ax)`. - """ - return DSVHandler(fn, show=show).parse_and_plot() - def get_plotter(ctx: IDSS, create=True): """ Returns the DSS plotter associated with the context `ctx`, if any. @@ -2668,8 +2676,9 @@ def get_plotter(ctx: IDSS, create=True): return plotter -plot = DSSPlotter(DSSPlotCtx) # Main plotter instance (default DSS context) -enable = plot.enable -disable = plot.disable +plotter = DSSPlotter(dss_nb_ctx) # Main plotter instance (default DSS context) +enable = plotter.enable +disable = plotter.disable +dsv = plotter.dsv -__all__ = ['enable', 'disable', 'plot_dsv', 'plot', 'get_plotter'] +__all__ = ['enable', 'disable', 'plotter', 'get_plotter', 'dsv'] From 31b79110e1e9d3e4385ae4423f325791f2a3cffc Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:32:08 -0300 Subject: [PATCH 73/82] Tests: Merge old changes related to Oddie tests (esp. OpenDSS-C). --- dss/Oddie.py | 2 +- dss/_cffi_api_util.py | 6 +- dss/patch_dss_com.py | 30 +++++- tests/_settings.py | 10 +- tests/test_events.py | 14 ++- tests/test_general.py | 206 +++++++++++++++++++++++++++++--------- tests/test_past_issues.py | 53 +++++++--- 7 files changed, 245 insertions(+), 76 deletions(-) diff --git a/dss/Oddie.py b/dss/Oddie.py index fc3a0b60..34340798 100644 --- a/dss/Oddie.py +++ b/dss/Oddie.py @@ -112,7 +112,7 @@ def __init__(self, library_path: str = '', load_flags: Optional[int] = None, odd rb'OpenDSSDirect.dll', # Try from the general path, let the system resolve it ] - for library_path in _lib_paths: + for library_path in _win32_lib_paths: lib.Oddie_SetLibOptions(library_path, c_load_flags) ctx = lib.ctx_New() if ctx != NULL: diff --git a/dss/_cffi_api_util.py b/dss/_cffi_api_util.py index a4046c95..52125b7d 100644 --- a/dss/_cffi_api_util.py +++ b/dss/_cffi_api_util.py @@ -31,8 +31,8 @@ # AltDSS_PyContext is not available. import dss_python_backend._func_info as _func_info -# Assumed UTF8; unless the fast C extension (dss_python_backend._fast_strs) is not -# used, this now has no effect but left to avoid breaking it for downstream users. +# Assumed UTF8; unless the fast C extension is not used, this now has no effect, +# but was left to avoid breaking it for downstream users. codec = 'UTF8' interface_classes = set() @@ -1033,8 +1033,8 @@ def unregister_callbacks(self): mgr.unregister_func(AltDSSEvent.Clear, altdss_python_util_callback) mgr.unregister_func(AltDSSEvent.ReprocessBuses, altdss_python_util_callback) - # The context will die, no need to do anything else currently. def __del__(self): + # The base context itself will die, no need to do anything else currently. Callbacks for AltDSS need to be cleared. if self._is_oddie: return diff --git a/dss/patch_dss_com.py b/dss/patch_dss_com.py index 18e5997f..7ca00a02 100644 --- a/dss/patch_dss_com.py +++ b/dss/patch_dss_com.py @@ -34,6 +34,25 @@ from .IStorages import IStorages from .IWindGens import IWindGens +class IBusesWrapper: + def __init__(self, DSS, Buses): + self.DSS = DSS + self.Buses = Buses + + def __call__(self, *args, **kwargs): + return self.Buses(*args, **kwargs) + + def __call__(self, *args, **kwargs): + return self.Buses(*args, **kwargs) + + def __iter__(self): + circ = self.DSS.ActiveCircuit + for i in range(circ.NumBuses): + circ.SetActiveBusi(i) + yield circ.ActiveBus + + def __len__(self): + return self.DSS.ActiveCircuit.NumBuses def custom_iter(self): idx = self.First @@ -135,9 +154,14 @@ def _get_BusNames(self, removeNodes=False): type(obj.ActiveCircuit.ActiveBus).__iter__ = custom_bus_iter type(obj.ActiveCircuit.ActiveBus).__len__ = custom_bus_len type(obj.ActiveCircuit.ActiveBus)._columns = IBus._columns - type(obj.ActiveCircuit.Buses).__iter__ = custom_bus_iter - type(obj.ActiveCircuit.Buses).__len__ = custom_bus_len - type(obj.ActiveCircuit.Buses)._columns = IBus._columns + Buses = obj.ActiveCircuit.Buses + try: + type(Buses).__iter__ = custom_bus_iter + type(Buses).__len__ = custom_bus_len + type(Buses)._columns = IBus._columns + except: + type(obj.ActiveCircuit).Buses = IBusesWrapper(obj, Buses) + def add_dunders(cls): cls.__iter__ = custom_iter diff --git a/tests/_settings.py b/tests/_settings.py index 1244a0a0..659c470b 100644 --- a/tests/_settings.py +++ b/tests/_settings.py @@ -4,18 +4,16 @@ import faulthandler faulthandler.disable() from dss import DSS -DSS.ActiveCircuit.Settings.COMErrorResults = False +# DSS.ActiveCircuit.Settings.COMErrorResults = False try: from dss import IOddieDSS except: pass -faulthandler.enable() - org_dir = os.getcwd() -USE_ODDIE = os.getenv('DSS_PYTHON_ODDIE', None) -if USE_ODDIE: +USE_ODDIE = os.getenv('DSS_EXTENSIONS_TEST_ODDIE', None) +if USE_ODDIE is not None and USE_ODDIE.upper() not in ('0', 'FALSE', 'F', 'OFF', 'NO'): # print("Using Oddie:", USE_ODDIE) if USE_ODDIE != '1': DSS = IOddieDSS(USE_ODDIE) @@ -24,6 +22,8 @@ os.chdir(org_dir) +DSS.ClearAll() +faulthandler.enable() WIN32 = (sys.platform == 'win32') if os.path.exists('../../electricdss-tst/'): diff --git a/tests/test_events.py b/tests/test_events.py index ce8f2eb2..3eb8be4b 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,10 +1,10 @@ import pytest -from dss import DSS, DSSException +from dss import DSSException try: - from ._settings import BASE_DIR + from ._settings import BASE_DIR, DSS except ImportError: - from _settings import BASE_DIR + from _settings import BASE_DIR, DSS class EventHandler: # Note: for real usage, prefer to generate local classes bound to some @@ -23,6 +23,10 @@ def OnCheckControls(self): def test_events_style_win32com(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the DSSEvents interface through the DirectDLL API.") + return + EventHandler.event_sequence.clear() evt_conn = DSS.Events.WithEvents(EventHandler) @@ -38,6 +42,10 @@ def test_events_style_win32com(): def test_events_style_comtypes(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the DSSEvents interface through the DirectDLL API.") + return + EventHandler.event_sequence.clear() evt_conn = DSS.Events.GetEvents(EventHandler()) diff --git a/tests/test_general.py b/tests/test_general.py index a08845e8..244fa193 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -15,7 +15,7 @@ import dss -from dss import IDSS, DSSException, SparseSolverOptions, SolveModes, set_case_insensitive_attributes, DSSCompatFlags, LoadModels, DSSPropertyNameStyle, IOddieDSS +from dss import IDSS, DSSException, SparseSolverOptions, SolveModes, set_case_insensitive_attributes, DSSCompatFlags, LoadModels, DSSPropertyNameStyle org_dir = os.getcwd() def setup_function(): @@ -23,9 +23,9 @@ def setup_function(): DSS.AllowForms = False DSS.ActiveCircuit.Settings.AdvancedTypes = False - DSS.ActiveCircuit.Settings.CompatFlags = 0 - if not DSS._api_util._is_oddie: + if not DSS.is_oddie(): + DSS.ActiveCircuit.Settings.CompatFlags = 0 DSS.AllowEditor = False DSS.AllowChangeDir = True DSS.ActiveCircuit.Settings.COMErrorResults = False @@ -34,6 +34,10 @@ def setup_function(): DSS.Text.Command = 'set DefaultBaseFreq=60' def test_zip_redirect(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the DSS-Extensions ZIP interface.") + return + with pytest.raises(DSSException): DSS.ZIP.Redirect('13Bus/IEEE13Nodeckt.dss') @@ -47,6 +51,10 @@ def test_zip_redirect(): def test_zip_contains(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the DSS-Extensions ZIP interface.") + return + with pytest.raises(DSSException): assert 'before open' in DSS.ZIP @@ -57,10 +65,18 @@ def test_zip_contains(): def test_zip_exists(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the DSS-Extensions ZIP interface.") + return + with pytest.raises(DSSException): DSS.ZIP.Open('something1/something2/something3.zip') def test_zip_filelist(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the DSS-Extensions ZIP interface.") + return + DSS.ZIP.Open(ZIP_FN) assert set(DSS.ZIP.List()) == {'13Bus/', '13Bus/IEEE13Node_BusXY.csv', '13Bus/IEEE13Nodeckt.dss', '13Bus/IEEELineCodes.DSS', '13Bus/README.txt'} assert DSS.ZIP.List('.*/RE.*') == ['13Bus/README.txt'] @@ -72,14 +88,15 @@ def test_zipv(): DSS.Text.Command = 'new load.test_load' load = DSS.ActiveCircuit.Loads load.First - - with pytest.raises(DSSException): - # Too few elements - load.ZIPV = [1, 2, 3, 4] - with pytest.raises(DSSException): - # Too many elements - load.ZIPV = [1, 2, 3, 4, 5, 6, 7, 8] + if not DSS.is_oddie(): + with pytest.raises(DSSException): + # Too few elements + load.ZIPV = [1, 2, 3, 4] + + with pytest.raises(DSSException): + # Too many elements + load.ZIPV = [1, 2, 3, 4, 5, 6, 7, 8] load.ZIPV = [1, 0, 0, 1, 0, 0, 0.6] assert list(load.ZIPV) == [1, 0, 0, 1, 0, 0, 0.6] @@ -99,20 +116,28 @@ def _run_mode(mode): def test_sparse_options(): - expected = _run_mode(SparseSolverOptions.ReuseNothing) - for mode in [ - SparseSolverOptions.AlwaysResetYPrimInvalid, - SparseSolverOptions.ReuseCompressedMatrix | SparseSolverOptions.AlwaysResetYPrimInvalid, - SparseSolverOptions.ReuseCompressedMatrix, - SparseSolverOptions.ReuseSymbolicFactorization | SparseSolverOptions.AlwaysResetYPrimInvalid, - SparseSolverOptions.ReuseSymbolicFactorization, - SparseSolverOptions.ReuseNumericFactorization | SparseSolverOptions.AlwaysResetYPrimInvalid, - SparseSolverOptions.ReuseNumericFactorization, - ]: - np.testing.assert_allclose(expected, _run_mode(mode)) + try: + expected = _run_mode(SparseSolverOptions.ReuseNothing) + for mode in [ + SparseSolverOptions.AlwaysResetYPrimInvalid, + SparseSolverOptions.ReuseCompressedMatrix | SparseSolverOptions.AlwaysResetYPrimInvalid, + SparseSolverOptions.ReuseCompressedMatrix, + SparseSolverOptions.ReuseSymbolicFactorization | SparseSolverOptions.AlwaysResetYPrimInvalid, + SparseSolverOptions.ReuseSymbolicFactorization, + SparseSolverOptions.ReuseNumericFactorization | SparseSolverOptions.AlwaysResetYPrimInvalid, + SparseSolverOptions.ReuseNumericFactorization, + ]: + np.testing.assert_allclose(expected, _run_mode(mode)) + except DSSException as ex: + if ex.args[0] == 42: #TODO: enum for Oddie errors + pytest.skip("This DSS engine does not seem to implement SolverOptions.") def test_pd_extras(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the DSS-Extensions PD extras.") + return + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.ActiveCircuit.Solution.Solve() @@ -171,6 +196,10 @@ def test_case_check(): def test_basic_ctx(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the AltDSS Context API and user-managed threads.") + return + prime_engine = DSS prime_engine.AllowChangeDir = False prime_engine.Text.Command = 'new circuit.test_prime' @@ -186,6 +215,10 @@ def test_basic_ctx(): def test_compat_precision(): + if DSS.is_oddie(): + pytest.skip("Precision compatibility only available in the AltDSS engine.") + return + DSS.ZIP.Open(ZIP_FN) DSS.ZIP.Redirect('13Bus/IEEE13Nodeckt.dss') DSS.ZIP.Close() @@ -199,6 +232,9 @@ def test_compat_precision(): def test_compat_activeline(): DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' + if DSS.is_oddie(): + pytest.skip("Test not required EPRI's OpenDSS and OpenDSS-C; different general behavior and error-handling expected.") + return Lines = DSS.ActiveCircuit.Lines Lines.First @@ -207,9 +243,8 @@ def test_compat_activeline(): name = Lines.Name DSS.ActiveCircuit.Loads.First - - assert name == Lines.Name + assert name == Lines.Name DSS.ActiveCircuit.Settings.CompatFlags = DSSCompatFlags.ActiveLine with pytest.raises(DSSException): assert name == Lines.Name @@ -268,7 +303,11 @@ def test_set_mode(): def test_pm_threads(): - if not isinstance(DSS, IOddieDSS): + if DSS.is_oddie(): + pytest.skip("Disabled with EPRI's engines until we investigate more; getting some crashes with PM.") + return + + if not DSS.is_oddie(): DSS.AllowChangeDir = False Parallel = DSS.ActiveCircuit.Parallel @@ -353,7 +392,7 @@ def test_pm_threads(): assert max(abs(v_pm[3] - v_pm[0])) > 1e-1 assert dt_pm < dt_seq - if not isinstance(DSS, IOddieDSS): + if not DSS.is_oddie(): # Let's run with threads, using DSSContexts too v_ctx = [None] * 4 @@ -390,6 +429,10 @@ def _run(ctx, i): def test_threading2(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support the AltDSS Context API and user-managed threads.") + return + DSS.AllowChangeDir = False # EPRITestCircuits/epri_dpv/M1 has loads with zero power @@ -773,9 +816,9 @@ def test_capacitor_reactor(DSS: IDSS = DSS): DSS.Text.Command = 'clear' debug_print('clear') DSS.Text.Command = f'set DefaultBaseFreq={f}' - debug_print(DSS.Text.Command) + # debug_print(DSS.Text.Command) DSS.Text.Command = f'new circuit.test{f} bus1={bus}' - debug_print(DSS.Text.Command) + # debug_print(DSS.Text.Command) for conn in ('delta', 'wye'): for phases0 in (1, 2, 3): phases = phases0 @@ -790,10 +833,10 @@ def test_capacitor_reactor(DSS: IDSS = DSS): kV_eff = kV * sqrt(3) DSS.Text.Command = f'new Line.{bus}-{bus + 1} bus1={bus} bus2={bus + 1}' - debug_print(DSS.Text.Command) + # debug_print(DSS.Text.Command) bus += 1 DSS.Text.Command = f'new {component}.{conn}_{phases} bus1={bus} phases={phases} conn={conn} kva={kVA} kV={kV_eff}' - debug_print(DSS.Text.Command) + # debug_print(DSS.Text.Command) DSS.Text.Command = 'solve' # debug_print(DSS.Text.Command) assert DSS.ActiveCircuit.ActiveCktElement.Name == f'{component}.{conn}_{phases}' @@ -806,7 +849,8 @@ def test_capacitor_reactor(DSS: IDSS = DSS): assert DSS.ActiveCircuit.ActiveCktElement.Name == f'{component}.{conn}_{phases}_alt' Y_dss2 = DSS.ActiveCircuit.ActiveCktElement.Yprim - np.testing.assert_allclose(Y_dss, Y_dss2) + if not DSS.is_oddie(): + np.testing.assert_allclose(Y_dss, Y_dss2) if conn == 'wye': VA_branch = 1000 * kVA / phases @@ -845,10 +889,10 @@ def test_capacitor_reactor(DSS: IDSS = DSS): phases = phases0 model = int(LoadModels.ConstZ) DSS.Text.Command = f'new Line.{bus}-{bus + 1} bus1={bus} bus2={bus + 1}' - debug_print(DSS.Text.Command) + # debug_print(DSS.Text.Command) bus += 1 DSS.Text.Command = f'new Load.{conn}_{phases} bus1={bus} phases={phases} conn={conn} kw=0 kvar={-sign * kVA} kV={kV_eff} model={model} Xneut=0 Rneut=0' - debug_print(DSS.Text.Command) + # debug_print(DSS.Text.Command) DSS.Text.Command = 'solve' # debug_print(DSS.Text.Command) assert DSS.ActiveCircuit.ActiveCktElement.Name == f'Load.{conn}_{phases}' @@ -869,18 +913,39 @@ def test_capacitor_reactor(DSS: IDSS = DSS): def test_patch_comtypes(): if WIN32: + if DSS.is_oddie(): + pytest.skip("Skipping COM test; OpenDSSDirect.DLL already loaded.") + return + import comtypes.client DSS_COM = dss.patch_dss_com(comtypes.client.CreateObject("OpenDSSengine.DSS")) test_essentials(DSS_COM) + else: + if DSS.is_oddie(): + pytest.skip("Skipping COM test (requires Windows).") + return + def test_patch_win32com(): if WIN32: + if DSS.is_oddie(): + pytest.skip("Skipping COM test; OpenDSSDirect.DLL already loaded.") + return + import win32com.client win32com.client.Dispatch("OpenDSSengine.DSS") DSS_COM = dss.patch_dss_com(win32com.client.gencache.EnsureDispatch("OpenDSSengine.DSS")) test_essentials(DSS_COM) + else: + if DSS.is_oddie(): + pytest.skip("Skipping COM test (requires Windows).") + return def test_namingstyle(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support SetPropertyNameStyle.") + return + DSS.ClearAll() DSS('new circuit.test') DSS.ActiveCircuit.Vsources.First @@ -929,11 +994,11 @@ def test_loadshape_extended(): assert LS.Npts == 24 ref_p = np.asarray([.677, .6256, .6087, .5833, .58028, .6025, .657, .7477, .832, .88, .94, .989, .985, .98, .9898, .999, 1, .958, .936, .913, .876, .876, .828, .756]) npt.assert_allclose(LS.Pmult, ref_p) - ref_t = range(len(ref_p)) - LS.TimeArray = ref_t + ref_t = list(range(len(ref_p))) + LS.TimeArray = list(ref_t) npt.assert_allclose(LS.TimeArray, ref_t) ref_q = LS.TimeArray + ref_p - LS.Qmult = ref_q + LS.Qmult = list(ref_q) npt.assert_allclose(LS.Qmult, ref_q) DSS.Text.Command = 'new loadshape.test npts=3 pmult=[1.1, 2.2, 3.3] qmult=[4.5, 4.6, 4.7] hour=[1, 2, 7]' @@ -942,12 +1007,12 @@ def test_loadshape_extended(): npt.assert_allclose(LS.Pmult, [1.1, 2.2, 3.3]) npt.assert_allclose(LS.Qmult, [4.5, 4.6, 4.7]) npt.assert_allclose(LS.TimeArray, [1, 2, 7]) - LS.Pmult *= 2 + LS.Pmult = list(np.asarray(LS.Pmult) * 2) npt.assert_allclose(LS.Pmult, [2.2, 4.4, 6.6]) - LS.Qmult /= 2.5 - npt.assert_allclose(LS.Qmult * 2.5, [4.5, 4.6, 4.7]) - LS.TimeArray *= 12 - npt.assert_allclose(LS.TimeArray / 12, [1, 2, 7]) + LS.Qmult = list(np.asarray(LS.Qmult) / 2.5) + npt.assert_allclose(np.asarray(LS.Qmult) * 2.5, [4.5, 4.6, 4.7]) + LS.TimeArray = list(np.asarray(LS.TimeArray) * 12) + npt.assert_allclose(np.asarray(LS.TimeArray) / 12, [1, 2, 7]) DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.Text.Command = 'new loadshape.test npts=3 pmult=[1.1, 2.2, 3.3] qmult=[4.5, 4.6, 4.7] hour=[1, 2, 7]' @@ -980,7 +1045,6 @@ def test_loadshape_extended(): def test_xycurve_extended(): - # Added for OpenDSSC DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' @@ -1000,23 +1064,36 @@ def test_xycurve_extended(): def test_line_parent_compat(): - from dss import DSSCompatFlags + from dss import DSSCompatFlags, DSSException DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.Text.Command = 'new energymeter.m1 element=transformer.sub' DSS.Text.Command = 'solve mode=snap' - DSS.ActiveCircuit.Settings.CompatFlags = DSSCompatFlags.ActiveLine Lines = DSS.ActiveCircuit.Lines + no_compat_expected = (1, 2, '632670', 3, '670671', 2, '632670', 1, '650632') + + if not DSS.is_oddie(): + DSS.ActiveCircuit.Settings.CompatFlags = DSSCompatFlags.ActiveLine + res_compat = Lines.First, Lines.Next, Lines.Name, Lines.Next, Lines.Name, Lines.Parent, Lines.Name, Lines.Parent, Lines.Name - DSS.ActiveCircuit.Settings.CompatFlags = 0 - res_no_compat = Lines.First, Lines.Next, Lines.Name, Lines.Next, Lines.Name, Lines.Parent, Lines.Name, Lines.Parent, Lines.Name + + if not DSS.is_oddie(): + DSS.ActiveCircuit.Settings.CompatFlags = 0 + res_no_compat = Lines.First, Lines.Next, Lines.Name, Lines.Next, Lines.Name, Lines.Parent, Lines.Name, Lines.Parent, Lines.Name - assert res_no_compat == (1, 2, '632670', 3, '670671', 2, '632670', 1, '650632') + if not DSS.is_oddie(): + assert res_no_compat == no_compat_expected + else: + res_no_compat = no_compat_expected # The indices returned in compat mode are "wrong", to match the official DSS implementation assert res_compat[3:2:] == res_no_compat[3:2:] def test_skip_commands(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support SkipCommands.") + return + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.ActiveCircuit.Settings.SkipCommands = ['clear'] # Since we are skipping the clear command, an exception should be raised @@ -1036,6 +1113,10 @@ def test_skip_commands(): def test_skip_files(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support SkipFileRegExp.") + return + DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.Text.Command = 'clear' @@ -1100,6 +1181,10 @@ def test_settings_context(): def test_share_general(): + if DSS.is_oddie(): + pytest.skip("EPRI's OpenDSS and OpenDSS-C do not support ShareGeneral.") + return + DSS2 = DSS.NewContext() DSS('new loadshape.sharedloadshape npts=4 pmult=[1, 2, 3, 4]') DSS.ShareGeneral(DSS2) @@ -1118,6 +1203,35 @@ def test_share_general(): assert list(LS2.Pmult) == [1., 2., 3., 4.] +def test_windgen_iteration(): + ''' + Added to test fix in official OpenDSS v10.2.0.1. + We had it already fixed in AltDSS, so this test ensures all engines work fine. + + Do not use this as a sample/example; it's not intended as a full working sample. + ''' + DSS.ClearAll() + DSS('new circuit.1') + DSS('new windgen.w1') + DSS('new windgen.w2') + assert DSS.ActiveCircuit.WindGens.Next == 0 + assert DSS.ActiveCircuit.WindGens.AllNames == ['w1', 'w2'] + assert DSS.ActiveCircuit.WindGens.First == 1 + assert DSS.ActiveCircuit.WindGens.Name == 'w1' + assert DSS.ActiveCircuit.WindGens.Next == 2 + assert DSS.ActiveCircuit.WindGens.Name == 'w2' + assert DSS.ActiveCircuit.WindGens.Next == 0 + assert DSS.ActiveCircuit.WindGens.Name == 'w2' + assert DSS.ActiveCircuit.WindGens.Count == 2 + + DSS.ClearAll() + DSS('new circuit.2') + assert DSS.ActiveCircuit.WindGens.AllNames == [] + assert DSS.ActiveCircuit.WindGens.Next == 0 + assert DSS.ActiveCircuit.WindGens.First == 0 + assert DSS.ActiveCircuit.WindGens.Count == 0 + + if __name__ == '__main__': DSS.AllowForms = False print(DSS.Version) diff --git a/tests/test_past_issues.py b/tests/test_past_issues.py index cf1b9851..9c9b230a 100644 --- a/tests/test_past_issues.py +++ b/tests/test_past_issues.py @@ -1,7 +1,10 @@ +import faulthandler +faulthandler.disable() + import sys, os from time import perf_counter import dss -from dss import DSS, IDSS, DSSException, SparseSolverOptions, SolveModes, set_case_insensitive_attributes +from dss import IDSS, DSSException, SparseSolverOptions, SolveModes, set_case_insensitive_attributes import numpy as np import pytest import scipy.sparse as sp @@ -11,14 +14,16 @@ except ImportError: from _settings import BASE_DIR, WIN32, ZIP_FN, DSS +faulthandler.enable() + def setup_function(): DSS.ClearAll() DSS.AllowForms = False DSS.ActiveCircuit.Settings.AdvancedTypes = False - DSS.ActiveCircuit.Settings.CompatFlags = 0 - if not DSS._api_util._is_oddie: + if not DSS.is_oddie(): + DSS.ActiveCircuit.Settings.CompatFlags = 0 DSS.AllowEditor = False DSS.AllowChangeDir = True DSS.ActiveCircuit.Settings.COMErrorResults = False @@ -32,22 +37,27 @@ def test_rxmatrix(): DSS.NewCircuit('test_rxmatrix') for r_or_x in 'rx': DSS.Text.Command = f'new Line.ourline{r_or_x} phases=3' - DSS.Text.Command = f'~ {r_or_x}matrix=[1,2,3]' + + if not DSS.is_oddie(): # This works but pops up an annoying window with the Delphi ODD.DLL + DSS.Text.Command = f'~ {r_or_x}matrix=[1,2,3]' + DSS.Text.Command = f'~ {r_or_x}matrix=[1,2,3 | 4,5,6 | 7,8,9]' DSS.Text.Command = f'? Line.ourline{r_or_x}.{r_or_x}matrix' assert DSS.Text.Result == '[1 |4 5 |7 8 9 ]' - with pytest.raises(DSSException): - DSS.Text.Command = f'~ {r_or_x}matrix=[10,20,30,40]' + if not DSS.is_oddie(): # This works but pops up an annoying window with the Delphi ODD.DLL + with pytest.raises(DSSException): + DSS.Text.Command = f'~ {r_or_x}matrix=[10,20,30,40]' DSS.Text.Command = f'? Line.ourline{r_or_x}.{r_or_x}matrix' assert DSS.Text.Result == '[1 |4 5 |7 8 9 ]' - with pytest.raises(DSSException): - DSS.Text.Command = f'~ {r_or_x}matrix={list(range(1000))}' + if not DSS.is_oddie(): # This would crash the official Delphi ODD.DLL + with pytest.raises(DSSException): + DSS.Text.Command = f'~ {r_or_x}matrix={list(range(1000))}' - with pytest.raises(DSSException): - DSS.Text.Command = f'~ {r_or_x}matrix=[1,2,3 | 4,5,6,7]' + with pytest.raises(DSSException): + DSS.Text.Command = f'~ {r_or_x}matrix=[1,2,3 | 4,5,6,7]' DSS.Text.Command = f'~ {r_or_x}matrix=[11 | 22, 33 | 44, 55, 66]' DSS.Text.Command = f'? Line.ourline{r_or_x}.{r_or_x}matrix' @@ -60,17 +70,30 @@ def test_create_no_circuit(): 'TShape', 'TCC_Curve', 'TSData', 'XfmrCode', 'XYcurve', 'WireData', ) for cls in DSS.Classes: + if cls == 'Solution': + continue # Added for OpenDSSDirect.DLL + DSS.ClearAll() if cls in general_classes: + if cls == 'GrowthShape' and DSS.is_oddie(): + continue + DSS.Text.Command = f'new {cls}.test' else: - with pytest.raises(DSSException, match=r'\(#(279)|(265)\)'): - DSS.Text.Command = f'new {cls}.test' - pytest.fail(f'Object of type "{cls}" was allowed to be created without a circuit!') + if not DSS.is_oddie(): + with pytest.raises(DSSException, match=r'\(#(279)|(265)\)'): + DSS.Text.Command = f'new {cls}.test' + pytest.fail(f'Object of type "{cls}" was allowed to be created without a circuit!') + + DSS.Text.Command = 'new circuit.test' def test_create_with_circuit(): + if DSS.is_oddie(): + pytest.skip("This test is dangerous with EPRI's OpenDSS and OpenDSS-C. Skipping.") + return + for cls in DSS.Classes: DSS.ClearAll() DSS.NewCircuit(f'test_{cls}') @@ -102,5 +125,5 @@ def test_ymatrix_csc(): DSS.Text.Command = f'redirect "{BASE_DIR}/Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss"' DSS.ActiveCircuit.Solution.Solve() DSS.ActiveCircuit.Settings.AdvancedTypes = True - assert np.all(DSS.ActiveCircuit.SystemY == sp.csc_matrix(DSS.YMatrix.GetCompressedYMatrix())) - + ydense = DSS.ActiveCircuit.SystemY + assert np.all(ydense == sp.csc_matrix(DSS.YMatrix.GetCompressedYMatrix())) From 78ba2b22fc56c4a1de63e98f4b29e756eadaf543 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:20:33 -0300 Subject: [PATCH 74/82] Tests: make SciPy optional (only used for sparse matrix). --- tests/test_past_issues.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_past_issues.py b/tests/test_past_issues.py index 9c9b230a..79bbd255 100644 --- a/tests/test_past_issues.py +++ b/tests/test_past_issues.py @@ -1,13 +1,18 @@ import faulthandler faulthandler.disable() -import sys, os +import sys, os, warnings from time import perf_counter import dss from dss import IDSS, DSSException, SparseSolverOptions, SolveModes, set_case_insensitive_attributes import numpy as np import pytest -import scipy.sparse as sp +try: + import scipy.sparse as sp +except: + sp = None + + try: from ._settings import BASE_DIR, WIN32, ZIP_FN, DSS @@ -126,4 +131,7 @@ def test_ymatrix_csc(): DSS.ActiveCircuit.Solution.Solve() DSS.ActiveCircuit.Settings.AdvancedTypes = True ydense = DSS.ActiveCircuit.SystemY - assert np.all(ydense == sp.csc_matrix(DSS.YMatrix.GetCompressedYMatrix())) + if sp is not None: + assert np.all(ydense == sp.csc_matrix(DSS.YMatrix.GetCompressedYMatrix())) + else: + pytest.skip("SciPy is not installed, skipping sparse-dense comparison.") From 2f069470112730b9437f3a240e8f3141a7530537 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 20 Feb 2026 01:43:07 -0300 Subject: [PATCH 75/82] Test backend 0.15.0b3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1dd0ac98..91952d8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ packages = ["dss"] name = "dss-python" dynamic = ["version"] dependencies = [ - "dss_python_backend==0.15.0b1", + "dss_python_backend==0.15.0b3", "numpy>=2,<3", "typing_extensions>=4.5,<5", ] From c0c2fa54efd5f62ffce442855bbb342ba943244c Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 20 Feb 2026 01:44:24 -0300 Subject: [PATCH 76/82] Revert "Fuses: update to match OpenDSS v11." This reverts commit e97c0a9bb287bc9b2bef8fa126fc8724bbbb3bd5. --- dss/IFuses.py | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/dss/IFuses.py b/dss/IFuses.py index 279981c4..9452074b 100644 --- a/dss/IFuses.py +++ b/dss/IFuses.py @@ -3,7 +3,6 @@ # Copyright (c) 2018-2025 DSS-Extensions contributors from ._cffi_api_util import Iterable from typing import List, AnyStr -import warnings class IFuses(Iterable): __slots__ = [] @@ -110,11 +109,9 @@ def NumPhases(self) -> int: @property def RatedCurrent(self) -> float: ''' - Fuse continuous rated current in Amps. Defaults to 0. - - Not used internally for either power flow or reporting. + Multiplier or actual amps for the TCCcurve object. Defaults to 1.0. - **NOTE:** *previous* to OpenDSS v11, or AltDSS engine/DSS C-API v0.15, this used to be the multiplier for the TCC curve. + Multiply current values of TCC curve by this to get actual amps. Original COM help: https://opendss.epri.com/RatedCurrent.html ''' @@ -122,8 +119,6 @@ def RatedCurrent(self) -> float: @RatedCurrent.setter def RatedCurrent(self, Value: float): - #TODO: suppress warning on older engines? - warnings.warn("RatedCurrent is not used internally by the DSS engine anymore since OpenDSS v11 and AltDSS engine v0.15. Please see `CurveMultiplier`.", UserWarning, stacklevel=2) self._lib.Fuses_Set_RatedCurrent(Value) @property @@ -191,34 +186,3 @@ def NormalState(self) -> List[str]: @NormalState.setter def NormalState(self, Value: List[AnyStr]): self._set_string_array(self._lib.Fuses_Set_NormalState, Value) - - @property - def CurveMultiplier(self) -> float: - ''' - Multiplier or actual amps for the TCCcurve object. Defaults to 1.0. - - Multiply current values of TCC curve by this to get actual amps. - - *New in OpenDSS engine v11, AltDSS engine v0.15.* - ''' - return self._lib.Fuses_Get_CurveMultiplier() - - @CurveMultiplier.setter - def CurveMultiplier(self, Value: float): - self._lib.Fuses_Set_CurveMultiplier(Value) - - @property - def InterruptingRating(self) -> float: - ''' - Fuse rated interrupting current in Amps. Defaults to 0. - - Not used internally for either power flow or reporting. - - *New in OpenDSS engine v11, AltDSS engine v0.15.* - ''' - return self._lib.Fuses_Get_InterruptingRating() - - @InterruptingRating.setter - def InterruptingRating(self, Value: float): - self._lib.Fuses_Set_InterruptingRating(Value) - From 370f15a8d2741370af5f5785bfc95853f2e579ca Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:00:22 -0300 Subject: [PATCH 77/82] CI: Update and try running tests --- .github/workflows/builds.yml | 278 ++++++++++++++++++++++------------- ci/build_wheel.sh | 2 +- ci/test_wheel.sh | 39 +++-- 3 files changed, 205 insertions(+), 114 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 23063767..e13db43e 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -4,7 +4,7 @@ name: Builds env: ARTIFACTS_FOLDER: '${{ github.workspace }}/artifacts' - DSS_CAPI_TAG: '0.14.5' + DSS_CAPI_TAG: '0.15.0b3' on: # release: @@ -13,204 +13,282 @@ on: jobs: build_linux_x64: + continue-on-error: true name: 'Linux x64' runs-on: ubuntu-latest strategy: matrix: - container-image: [ - 'quay.io/pypa/manylinux_2_28_x86_64', - 'quay.io/pypa/manylinux2014_x86_64' - ] + include: + - container_image: 'quay.io/pypa/manylinux_2_28_x86_64' + SKIP_SCIPY: 0 + - container_image: 'quay.io/pypa/manylinux2014_x86_64' + SKIP_SCIPY: 1 container: - image: ${{ matrix.container-image }} + image: ${{ matrix.container_image }} env: - CONDA_SUBDIR: 'linux-64' - CONDA: "/opt/miniconda/" + DSS_PYTHON_TEST_LINUX: '1' + SKIP_SCIPY: "${{ matrix.SKIP_SCIPY }}" steps: - - name: 'Checkout' + - name: 'Checkout DSS-Python' run: | git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY dss_python cd dss_python git checkout $GITHUB_SHA + + - name: 'Get electricdss-tst' + run: | + git clone --depth=1 https://github.com/dss-extensions/electricdss-tst.git + - name: 'Download/extract message catalogs' run: | curl -s -L https://github.com/dss-extensions/dss_capi/releases/download/${DSS_CAPI_TAG}/messages.tar.gz -o messages.tar.gz cd dss_python/dss tar zxf ../../messages.tar.gz + - name: Build wheel run: | mkdir -p artifacts mkdir -p artifacts_raw bash dss_python/ci/build_linux.sh x64 - # - name: Build conda packages - # continue-on-error: true - # run: | - # bash dss_python/ci/build_conda.sh - - name: Try installing the wheel - run: bash dss_python/ci/test_wheel.sh + + - name: Install wheel + run: | + bash dss_python/ci/test_wheel.sh + - name: 'Upload artifacts' - uses: "actions/upload-artifact@v3" + uses: "actions/upload-artifact@v4" + if: ${{ matrix.container_image == 'quay.io/pypa/manylinux_2_28_x86_64' }} with: - name: 'packages' + name: 'dss_python-wheel' path: '${{ github.workspace }}/artifacts' - # build_linux_x86: - # name: 'Linux x86' - # runs-on: ubuntu-latest - # env: - # CONDA_SUBDIR: 'linux-32' - # DOCKER_IMAGE: 'pmeira/manylinux_wheel_fpc322_i686' - # steps: - # - name: 'Checkout' - # run: | - # git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY dss_python - # cd dss_python - # git checkout $GITHUB_SHA - # - name: 'Setup Docker' - # run: | - # docker pull $DOCKER_IMAGE - # - name: 'Download/extract message catalogs' - # run: | - # curl -s -L https://github.com/dss-extensions/dss_capi/releases/download/${DSS_CAPI_TAG}/messages.tar.gz -o messages.tar.gz - # cd dss_python/dss - # tar zxf ../../messages.tar.gz - # - name: Build wheel - # run: | - # mkdir -p artifacts - # mkdir -p artifacts_raw - # docker run -e GITHUB_SHA -e GITHUB_REF -v "${PWD}:/build" -w /build $DOCKER_IMAGE bash /build/dss_python/ci/build_linux.sh x86 - # - name: 'Upload artifacts' - # uses: "actions/upload-artifact@v3" - # with: - # name: 'packages' - # path: '${{ github.workspace }}/artifacts' + - name: Run tests (FastDSS) + shell: bash + run: | + export PATH=/opt/python/cp313-cp313/bin/:$PATH + cd dss_python + pip install cffi numpy pytest + DSS_EXTENSIONS_FASTDSS=1 python -m pytest + + - name: Run tests (CFFI) + shell: bash + run: | + export PATH=/opt/python/cp313-cp313/bin/:$PATH + cd dss_python + pip install cffi numpy pytest + DSS_EXTENSIONS_FASTDSS=0 python -m pytest build_macos_x64: + continue-on-error: true name: 'macOS x64' - runs-on: 'macos-11' + runs-on: 'macos-15-intel' env: - SDKROOT: '${{ github.workspace }}/MacOSX10.13.sdk' PYTHON: python3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 path: 'dss_python' + + - name: "Get electricdss-tst" + uses: actions/checkout@v6 + with: + fetch-depth: 1 + repository: 'dss-extensions/electricdss-tst' + path: 'electricdss-tst' + + - name: 'Prepare Python' + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + - name: 'Download/extract message catalogs' run: | curl -s -L https://github.com/dss-extensions/dss_capi/releases/download/${DSS_CAPI_TAG}/messages.tar.gz -o messages.tar.gz cd dss_python/dss tar zxf ../../messages.tar.gz + - name: Build wheel run: | bash dss_python/ci/build_wheel.sh - # - name: Build conda packages - # continue-on-error: true - # run: | - # sudo chown -R $UID $CONDA - # bash dss_python/ci/build_conda.sh - - name: 'Upload artifacts' - uses: "actions/upload-artifact@v3" - with: - name: 'packages' - path: '${{ github.workspace }}/artifacts' + + - name: Install wheel + shell: bash + run: | + bash dss_python/ci/test_wheel.sh + + - name: Run tests (FastDSS) + shell: bash + run: | + cd dss_python + DSS_EXTENSIONS_FASTDSS=1 python -m pytest + + - name: Run tests (CFFI) + shell: bash + run: | + cd dss_python + DSS_EXTENSIONS_FASTDSS=0 python -m pytest build_macos_arm64: + continue-on-error: true name: 'macOS ARM64' - runs-on: 'macos-11' + runs-on: 'macos-15' env: PYTHON: python3 - _PYTHON_HOST_PLATFORM: macosx-11.0-arm64 ARCHFLAGS: '-arch arm64' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 path: 'dss_python' + + - name: "Get electricdss-tst" + uses: actions/checkout@v6 + with: + fetch-depth: 1 + repository: 'dss-extensions/electricdss-tst' + path: 'electricdss-tst' + + - name: 'Prepare Python' + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + - name: 'Download/extract message catalogs' run: | curl -s -L https://github.com/dss-extensions/dss_capi/releases/download/${DSS_CAPI_TAG}/messages.tar.gz -o messages.tar.gz cd dss_python/dss tar zxf ../../messages.tar.gz - # - name: 'Download macOS SDK 10.13' - # run: | - # curl -s -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz -o macOSsdk.tar.xz - # tar xf macOSsdk.tar.xz + - name: Build wheel run: | bash dss_python/ci/build_wheel.sh - # - name: Build conda packages - # continue-on-error: true - # run: | - # sudo chown -R $UID $CONDA - # bash dss_python/ci/build_conda.sh - - name: 'Upload artifacts' - uses: "actions/upload-artifact@v3" - with: - name: 'packages' - path: '${{ github.workspace }}/artifacts' + + - name: Install wheel + shell: bash + run: | + bash dss_python/ci/test_wheel.sh + + - name: Run tests (FastDSS) + shell: bash + run: | + cd dss_python + DSS_EXTENSIONS_FASTDSS=1 python -m pytest + + - name: Run tests (CFFI) + shell: bash + run: | + cd dss_python + DSS_EXTENSIONS_FASTDSS=0 python -m pytest build_win_x64: + continue-on-error: true name: 'Windows x64' - runs-on: windows-2019 + runs-on: windows-latest env: - CONDA_SUBDIR: 'win-64' PYTHON: python steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 path: 'dss_python' + + - name: "Get electricdss-tst" + uses: actions/checkout@v6 + with: + fetch-depth: 1 + repository: 'dss-extensions/electricdss-tst' + path: 'electricdss-tst' + - name: 'Download/extract message catalogs' shell: cmd run: | "c:\Program Files\Git\mingw64\bin\curl" -s -L https://github.com/dss-extensions/dss_capi/releases/download/%DSS_CAPI_TAG%/messages.zip -o messages.zip cd dss_python\dss tar zxf ..\..\messages.zip + - name: Build wheel shell: bash run: | bash dss_python/ci/build_wheel.sh - # - name: Build conda packages - # continue-on-error: true - # shell: bash - # run: | - # bash dss_python/ci/build_conda.sh - - name: 'Upload artifacts' - uses: "actions/upload-artifact@v3" - with: - name: 'packages' - path: '${{ github.workspace }}/artifacts' + + - name: Install wheel + shell: bash + run: | + bash dss_python/ci/test_wheel.sh + + - name: Run tests (FastDSS) + shell: cmd + run: | + set DSS_EXTENSIONS_FASTDSS=1 + cd dss_python + python -m pytest + + - name: Run tests (CFFI) + shell: cmd + run: | + set DSS_EXTENSIONS_FASTDSS=0 + cd dss_python + python -m pytest + build_win_x86: + continue-on-error: true name: 'Windows x86' - runs-on: windows-2019 + runs-on: windows-latest env: - CONDA_SUBDIR: 'win-32' PYTHON: python + SKIP_SCIPY: '1' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 path: 'dss_python' - - uses: actions/setup-python@v3 + + - name: "Get electricdss-tst" + uses: actions/checkout@v6 + with: + fetch-depth: 1 + repository: 'dss-extensions/electricdss-tst' + path: 'electricdss-tst' + + - name: 'Prepare Python' + uses: actions/setup-python@v6 with: - python-version: '3.7' + python-version: '3.11' + cache: 'pip' architecture: 'x86' + - name: 'Download/extract message catalogs' shell: cmd run: | "c:\Program Files\Git\mingw64\bin\curl" -s -L https://github.com/dss-extensions/dss_capi/releases/download/%DSS_CAPI_TAG%/messages.zip -o messages.zip cd dss_python\dss tar zxf ..\..\messages.zip + - name: Build wheel shell: bash run: | bash dss_python/ci/build_wheel.sh - - name: 'Upload artifacts' - uses: "actions/upload-artifact@v3" - with: - name: 'packages' - path: '${{ github.workspace }}/artifacts' + - name: Install wheel + shell: bash + run: | + bash dss_python/ci/test_wheel.sh + + - name: Run tests (FastDSS) + shell: cmd + run: | + set DSS_EXTENSIONS_FASTDSS=1 + cd dss_python + python -m pytest + + - name: Run tests (CFFI) + shell: cmd + run: | + set DSS_EXTENSIONS_FASTDSS=0 + cd dss_python + python -m pytest diff --git a/ci/build_wheel.sh b/ci/build_wheel.sh index a67d1fde..9fdddcb6 100644 --- a/ci/build_wheel.sh +++ b/ci/build_wheel.sh @@ -1,5 +1,5 @@ mkdir -p artifacts cd dss_python $PYTHON -m pip install --upgrade pip wheel hatch -$PYTHON -m pip install cffi +$PYTHON -m pip install cffi numpy pytest $PYTHON -m hatch build "$ARTIFACTS_FOLDER" diff --git a/ci/test_wheel.sh b/ci/test_wheel.sh index ff1bd5fc..e02c1a9e 100644 --- a/ci/test_wheel.sh +++ b/ci/test_wheel.sh @@ -1,15 +1,28 @@ -# Currently only for Linux - set -e -x -ORIGINAL_PATH=$PATH -PYTHON_DIRS="cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311" - -for pydir in $PYTHON_DIRS -do - echo Installing for CPython $pydir - export PATH=/opt/python/${pydir}/bin/:$ORIGINAL_PATH - python -m pip install scipy matplotlib - python -m pip install artifacts/dss_python-*.whl - python -c 'from dss import DSS; DSS.Plotting.enable(); DSS("new circuit.test123")' -done +if [[ "x${DSS_PYTHON_TEST_LINUX}" == "x1" ]]; then + ORIGINAL_PATH=$PATH + PYTHON_DIRS="cp311-cp311 cp312-cp312 cp313-cp313 cp314-cp314" + for pydir in $PYTHON_DIRS + do + echo Installing for CPython $pydir + export PATH=/opt/python/${pydir}/bin/:$ORIGINAL_PATH + if [[ "x${SKIP_SCIPY}" != "x1" ]]; then + python -m pip install scipy matplotlib + python -m pip install artifacts/dss_python-*.whl + python -c 'from dss import DSS; DSS.Plotting.enable(); DSS("new circuit.test123")' + else + python -m pip install artifacts/dss_python-*.whl + python -c 'from dss import DSS; DSS("new circuit.test123")' + fi + done +else + if [[ "x${SKIP_SCIPY}" != "x1" ]]; then + python -m pip install scipy matplotlib + python -m pip install artifacts/dss_python-*.whl + python -c 'from dss import DSS; DSS.Plotting.enable(); DSS("new circuit.test123")' + else + python -m pip install artifacts/dss_python-*.whl + python -c 'from dss import DSS; DSS("new circuit.test123")' + fi +fi \ No newline at end of file From 056e46c8eaa5038e7c56d5110589878d3a6e212f Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:52:54 -0300 Subject: [PATCH 78/82] Tests: skip COM test if the modules are not installed We should install/register a COM DLL from EPRI to test this. --- tests/test_general.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_general.py b/tests/test_general.py index 244fa193..f5921c3b 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -917,7 +917,12 @@ def test_patch_comtypes(): pytest.skip("Skipping COM test; OpenDSSDirect.DLL already loaded.") return - import comtypes.client + try: + import comtypes.client + except: + pytest.skip("Skipping COM test; comtypes is not installed") + return + DSS_COM = dss.patch_dss_com(comtypes.client.CreateObject("OpenDSSengine.DSS")) test_essentials(DSS_COM) else: @@ -932,7 +937,12 @@ def test_patch_win32com(): pytest.skip("Skipping COM test; OpenDSSDirect.DLL already loaded.") return - import win32com.client + try: + import win32com.client + except: + pytest.skip("Skipping COM test; win32com is not installed") + return + win32com.client.Dispatch("OpenDSSengine.DSS") DSS_COM = dss.patch_dss_com(win32com.client.gencache.EnsureDispatch("OpenDSSengine.DSS")) test_essentials(DSS_COM) From 71c31cf056adb5ea9f44d190a76257f026af538a Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:52:28 -0300 Subject: [PATCH 79/82] CI: disable testing manylinux2014 Notably, NumPy does not provide wheels for it, so it takes 10+ minutes to build it. The import tests are already done in dss_python_backend, so we know it works. --- .github/workflows/builds.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index e13db43e..7f19dfff 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -21,8 +21,8 @@ jobs: include: - container_image: 'quay.io/pypa/manylinux_2_28_x86_64' SKIP_SCIPY: 0 - - container_image: 'quay.io/pypa/manylinux2014_x86_64' - SKIP_SCIPY: 1 + # - container_image: 'quay.io/pypa/manylinux2014_x86_64' + # SKIP_SCIPY: 1 container: image: ${{ matrix.container_image }} env: From 64fdcdb0f694bc31da654bbaff5dbf463dedf7b1 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:44:59 -0300 Subject: [PATCH 80/82] Tests: update save_outputs to the current versions. - Try to exclude some v11+ items. Those items are not implemented and/or fully tested yet. - Adjust suffix in outputs; check if C-API only in first line of the version string. - Adjust COM patches to patch the more recent interfaces. --- dss/patch_dss_com.py | 3 + tests/compare_outputs.py | 15 +++- tests/save_outputs.py | 181 +++++++++++++++++++++++++-------------- 3 files changed, 130 insertions(+), 69 deletions(-) diff --git a/dss/patch_dss_com.py b/dss/patch_dss_com.py index 7ca00a02..2f00c9c1 100644 --- a/dss/patch_dss_com.py +++ b/dss/patch_dss_com.py @@ -22,6 +22,7 @@ from .IMonitors import IMonitors from .IPDElements import IPDElements from .IPVSystems import IPVSystems +from .IReactors import IReactors from .IRelays import IRelays from .IReclosers import IReclosers from .ISensors import ISensors @@ -182,10 +183,12 @@ def add_dunders(cls): 'Monitors': IMonitors, 'PDElements': IPDElements, 'PVSystems': IPVSystems, + 'Reactors': IReactors, 'Relays': IRelays, 'Reclosers': IReclosers, 'Sensors': ISensors, 'RegControls': IRegControls, + 'Storages': IStorages, 'SwtControls': ISwtControls, 'Vsources': IVsources, 'Transformers': ITransformers, diff --git a/tests/compare_outputs.py b/tests/compare_outputs.py index da008fc5..df774754 100644 --- a/tests/compare_outputs.py +++ b/tests/compare_outputs.py @@ -236,6 +236,11 @@ def compare(self, a, b, org_path=None): continue if isinstance(va, list): + if not isinstance(vb, list): + if (path[-4], path[-1]) in (('Relays', 'State'), ('Relays', 'NormalState')): + continue + + if ((va == ['none'] or va == ['NONE']) and vb == []) or (va == [] and (vb == ['none'] or vb == ['NONE'])): continue @@ -430,8 +435,8 @@ def compare_all(self): print('Skipping, not converged in A:', fn) continue - self.A_IS_COM = 'C-API' not in dataA['DSS']['Version'] - self.B_IS_COM = 'C-API' not in dataB['DSS']['Version'] + self.A_IS_COM = 'C-API' not in dataA['DSS']['Version'].split('\n')[0] + self.B_IS_COM = 'C-API' not in dataB['DSS']['Version'].split('\n')[0] try: self.compare(dataA, dataB, [fn]) if not self.per_file[fn]: @@ -487,8 +492,10 @@ def compare_all(self): df_a = pd.read_csv(sfA) except pd.errors.EmptyDataError: continue - - df_b = pd.read_csv(sfB) + try: + df_b = pd.read_csv(sfB) + except pd.errors.EmptyDataError: + continue df_a.columns = [x.strip() for x in df_a.columns] df_b.columns = [x.strip() for x in df_b.columns] diff --git a/tests/save_outputs.py b/tests/save_outputs.py index 5b60612f..1299d555 100644 --- a/tests/save_outputs.py +++ b/tests/save_outputs.py @@ -167,7 +167,21 @@ def export_dss_api_cls(dss: dss.IDSS, dss_cls): is_ckt_element = getattr(type(dss_cls), '_is_circuit_element', False) ckt_elem = dss.ActiveCircuit.ActiveCktElement ckt_elem_columns = set(type(ckt_elem)._columns) - ckt_elem_columns_meta - pc_elem_columns - {'Handle', 'IsIsolated', 'HasOCPDevice'} - fields = list(type(dss_cls)._columns) + try: + fields = list(type(dss_cls)._columns) + except: + print(dss_cls, '_columns not found, skipping...') + return + + if lname.endswith('fuses') and IS_V11: + for f in ['CurveMultiplier', 'InterruptingRating']: + if f in fields: + fields.remove(f) + + # if lname.endswith('swtcontrols') and IS_V11: + # for f in ['Action', 'NormalState', 'State', 'RatedCurrent', 'Open', 'Close', ]: + # if f in fields: + # fields.remove(f) if lname.endswith('solution'): fields.extend(['IncMatrix', 'Laplacian', 'IncMatrixCols', 'IncMatrixRows', ]) @@ -243,80 +257,88 @@ def iter_cls(): except: pass + if 'loadshapes' in lname: + dss('//!AltDSS PushCompatFlags') + dss('//!AltDSS SetCompatFlag PermissiveProperties') - for _ in items: - record = {} - for field in fields: - # printv('>', getattr(_, 'Name', '---'), field) - try: - record[field] = adjust_to_json(dss_cls, field) - except DSSException as e: - # Check for methods not implemented - if 'not implemented' in e.args[1].lower(): - #print(e.args) + try: + for _ in items: + record = {} + for field in fields: + # printv('>', getattr(_, 'Name', '---'), field) + try: + record[field] = adjust_to_json(dss_cls, field) + except DSSException as e: + # Check for methods not implemented + if 'not implemented' in e.args[1].lower(): + #print(e.args) + continue + raise + except StopIteration: + # Some fields are functions, skip those + continue + except AttributeError: + # Depending on the version, a field doesn't exist continue - raise - except StopIteration: - # Some fields are functions, skip those - continue - except AttributeError: - # Depending on the version, a field doesn't exist - continue - if meter_section_fields: - if dss_cls.NumSections > 0: - dss_cls.SetActiveSection(1) - for field in meter_section_fields: + if meter_section_fields: + if dss_cls.NumSections > 0: + dss_cls.SetActiveSection(1) + for field in meter_section_fields: + # printv('>', field) + try: + record[field] = adjust_to_json(dss_cls, field) + except StopIteration: + # Some fields are functions, skip those + continue + + if is_ckt_element: + # also dump the circuit element info + ckt_record = {} + for field in ckt_elem_columns: # printv('>', field) - try: - record[field] = adjust_to_json(dss_cls, field) - except StopIteration: - # Some fields are functions, skip those - continue + ckt_record[field] = adjust_to_json(ckt_elem, field) - if is_ckt_element: - # also dump the circuit element info - ckt_record = {} - for field in ckt_elem_columns: - # printv('>', field) - ckt_record[field] = adjust_to_json(ckt_elem, field) + record['ActiveCktElement'] = ckt_record - record['ActiveCktElement'] = ckt_record + if not has_iter: + # simple record + return record - if not has_iter: - # simple record - return record + # accumulate records + records.append(record) - # accumulate records - records.append(record) + if is_ckt_element and not metadata_record: + if records: + for field in ckt_elem_columns_meta: + # printv('>', field) + metadata_record[field] = adjust_to_json(ckt_elem, field) - if is_ckt_element and not metadata_record: - if records: - for field in ckt_elem_columns_meta: + for field in ckt_iter_columns_meta: # printv('>', field) - metadata_record[field] = adjust_to_json(ckt_elem, field) + try: + metadata_record[field] = adjust_to_json(dss_cls, field) + except DSSException as e: + if 'not implemented' in e.args[1].lower(): + # print(e.args) + continue - for field in ckt_iter_columns_meta: - # printv('>', field) - try: - metadata_record[field] = adjust_to_json(dss_cls, field) - except DSSException as e: - if 'not implemented' in e.args[1].lower(): - # print(e.args) - continue + raise - raise + if 'Meters' in type(dss_cls).__name__: + # This breaks the iteration + extra = {'Totals': adjust_to_json(dss_cls, 'Totals')} - if 'Meters' in type(dss_cls).__name__: - # This breaks the iteration - extra = {'Totals': adjust_to_json(dss_cls, 'Totals')} + # elif has_iter and not metadata_record: + # for field in iter_columns_meta: + # metadata_record[field] = adjust_to_json(dss_cls, field) - # elif has_iter and not metadata_record: - # for field in iter_columns_meta: - # metadata_record[field] = adjust_to_json(dss_cls, field) + finally: + if 'loadshapes' in lname: + dss('//!AltDSS PopCompatFlags') return {'records': records, 'metadata': metadata_record, **extra} @@ -342,19 +364,23 @@ def save_state(dss: dss.IDSS, runtime: float = 0.0) -> str: 'Monitors': dss.ActiveCircuit.Monitors, 'PDElements': dss.ActiveCircuit.PDElements, 'PVSystems': dss.ActiveCircuit.PVSystems, - 'Reclosers': dss.ActiveCircuit.Reclosers, 'RegControls': dss.ActiveCircuit.RegControls, 'Relays': dss.ActiveCircuit.Relays, 'Sensors': dss.ActiveCircuit.Sensors, 'Settings': dss.ActiveCircuit.Settings, 'Solution': dss.ActiveCircuit.Solution, - 'SwtControls': dss.ActiveCircuit.SwtControls, 'Topology': dss.ActiveCircuit.Topology, 'Transformers': dss.ActiveCircuit.Transformers, 'Vsources': dss.ActiveCircuit.Vsources, 'XYCurves': dss.ActiveCircuit.XYCurves, } + if not IS_V11: + dss_classes.update({ + 'Reclosers': dss.ActiveCircuit.Reclosers, + 'SwtControls': dss.ActiveCircuit.SwtControls, + }) + try: dss_classes.update({ 'Storages': dss.ActiveCircuit.Storages, @@ -369,12 +395,18 @@ def save_state(dss: dss.IDSS, runtime: float = 0.0) -> str: except AttributeError: pass + try: + dss_classes.update({ + 'Reactors': dss.ActiveCircuit.Reactors, + }) + except AttributeError: + pass + try: dss_classes.update({ 'CNData': dss.ActiveCircuit.CNData, 'LineGeometries': dss.ActiveCircuit.LineGeometries, 'LineSpacings': dss.ActiveCircuit.LineSpacings, - 'Reactors': dss.ActiveCircuit.Reactors, 'TSData': dss.ActiveCircuit.TSData, 'WireData': dss.ActiveCircuit.WireData, }) @@ -412,6 +444,8 @@ def get_archive_fn(live_fn, fn_prefix=None): return archive_fn if __name__ == '__main__': + IS_V11 = False + if os.path.exists('../../electricdss-tst/'): ROOT_DIR = os.path.abspath('../../electricdss-tst/') else: @@ -443,14 +477,18 @@ def get_archive_fn(live_fn, fn_prefix=None): elif SAVE_DSSX_OUTPUT: from dss import DSS, DSSCompatFlags + extrasuffix = '' + if DSS.ActiveCircuit.Settings.COMErrorResults: + extrasuffix += '_CER' + DSS.ActiveCircuit.Settings.CompatFlags = 0 # DSSCompatFlags.InvControl9611 print("Using DSS-Extensions:", DSS.Version) match = re.match('DSS C-API Library version ([^ ]+) revision.* ([0-9]+);.*', DSS.Version) dssx_ver, dssx_timestamp = match.groups() if (DSSCompatFlags.InvControl9611 & DSS.CompatFlags): - suffix = f'-dssx_InvControl9611-{sys.platform}-{platform.machine()}-{dssx_ver}-{dssx_timestamp}' - else: - suffix = f'-dssx-{sys.platform}-{platform.machine()}-{dssx_ver}-{dssx_timestamp}' + extrasuffix += '_InvControl9611' + + suffix = f'-dssx{extrasuffix}-{sys.platform}-{platform.machine()}-{dssx_ver}-{dssx_timestamp}' DSS.AllowEditor = False else: @@ -462,6 +500,7 @@ def get_archive_fn(live_fn, fn_prefix=None): com_ver = DSS.Version.split(' ')[1] suffix = f'-COM-{platform.machine()}-{com_ver}' + IS_V11 = hasattr(DSS.ActiveCircuit.Fuses, 'InterruptingRating') DSS.AllowForms = False try: @@ -472,7 +511,16 @@ def get_archive_fn(live_fn, fn_prefix=None): else: DSS.Text.Command = r'set Editor="C:\Program Files\Git\usr\bin\true.exe"' - DSS.Text.Command = 'set ShowExport=NO' + try: + DSS.Text.Command = 'set ShowExport=NO' + except: + pass + + try: + DSS.Text.Command = 'set ShowReports=NO' + except: + pass + check_error() sleep(0.1) DSS.Text.Command = 'clear' @@ -514,8 +562,11 @@ def get_archive_fn(live_fn, fn_prefix=None): exit() except OSError: traceback.print_exc() + print('Last file was:') + print(fn) exit() except: + print('=' * 60) print('ERROR:', fn) if colorizer: colorizer.colorize_traceback(*sys.exc_info()) From 23120eea6cd8d2ee4357a1064ced3e38c3f16381 Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:34:18 -0300 Subject: [PATCH 81/82] AltDSS/DSS C-API and backend 0.15.0b4 --- .github/workflows/builds.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 7f19dfff..6fd6b911 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -4,7 +4,7 @@ name: Builds env: ARTIFACTS_FOLDER: '${{ github.workspace }}/artifacts' - DSS_CAPI_TAG: '0.15.0b3' + DSS_CAPI_TAG: '0.15.0b4' on: # release: diff --git a/pyproject.toml b/pyproject.toml index 91952d8b..8c458402 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ packages = ["dss"] name = "dss-python" dynamic = ["version"] dependencies = [ - "dss_python_backend==0.15.0b3", + "dss_python_backend==0.15.0b4", "numpy>=2,<3", "typing_extensions>=4.5,<5", ] From 525f192a4372e82f1edb36983312e473539d1c4f Mon Sep 17 00:00:00 2001 From: Paulo Meira <10246101+PMeira@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:49:59 -0300 Subject: [PATCH 82/82] CI: avoid virtualenv v21, hatch doesn't like it. --- ci/build_linux.sh | 1 + ci/build_wheel.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/build_linux.sh b/ci/build_linux.sh index 3029e32e..dc503efb 100644 --- a/ci/build_linux.sh +++ b/ci/build_linux.sh @@ -3,5 +3,6 @@ export PATH=/opt/python/cp39-cp39/bin/:$PATH cd dss_python python3 -m pip install --upgrade pip wheel hatch +python3 -m pip install 'virtualenv<21' python3 -m hatch build "../artifacts" cd .. diff --git a/ci/build_wheel.sh b/ci/build_wheel.sh index 9fdddcb6..87279e94 100644 --- a/ci/build_wheel.sh +++ b/ci/build_wheel.sh @@ -1,5 +1,5 @@ mkdir -p artifacts cd dss_python $PYTHON -m pip install --upgrade pip wheel hatch -$PYTHON -m pip install cffi numpy pytest +$PYTHON -m pip install cffi numpy pytest 'virtualenv<21' $PYTHON -m hatch build "$ARTIFACTS_FOLDER"