diff --git a/.github/workflows/pull_request_tests.yml b/.github/workflows/pull_request_tests.yml index 19d51fd5..3e042148 100644 --- a/.github/workflows/pull_request_tests.yml +++ b/.github/workflows/pull_request_tests.yml @@ -26,6 +26,9 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e '.[dev]' + - name: Run pre-commit checks + run: | + python -m pre_commit run --all-files - name: Run pytests run: | - python -m pytest -v --disable-warnings + python -m pytest -v diff --git a/.gitignore b/.gitignore index ce62d2d4..f18aa407 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ simulation-run.toml *.bak docs/source/pydss/* dist/ +examples/openmdao_reports/ +examples/**/OpenMDAOReports/ # Created by https://www.toptal.com/developers/gitignore/api/python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b49064aa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.5 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format \ No newline at end of file diff --git a/OPENMDAO_MIGRATION_PLAN.md b/OPENMDAO_MIGRATION_PLAN.md new file mode 100644 index 00000000..491865c9 --- /dev/null +++ b/OPENMDAO_MIGRATION_PLAN.md @@ -0,0 +1,178 @@ +# PyDSS OpenMDAO Migration Plan + +## Current Status + +The core migration is implemented and verified. All registered controller types +are OpenMDAO explicit components, nonlinear convergence is owned by OpenMDAO, +and the full test suite passes. The legacy controller-disable setting and +runtime bypass have been removed. + +Remaining work is verification depth rather than a second controller runtime: +add dedicated integration coverage for HELICS, socket control, Monte Carlo, +harmonic sweeps, and dry-run behavior, then review performance and numerical +baselines for each supported mode. + +## Goal + +Replace the PyDSS controller convergence implementation with an OpenMDAO model. The refactor is intentionally breaking: there is no legacy controller API, priority loop, `DSSInstance` convergence management, or compatibility translation for the old controller configuration format. + +OpenMDAO will own nonlinear convergence for each simulation time point. OpenDSS remains the circuit solver and model backend, but it is called by an explicit OpenMDAO circuit component. + +## Target Architecture + +```text +PyDSS time-step orchestration + -> OpenMDAO Problem / Group + -> CircuitExplicitComponent + inputs: controller commands, exogenous updates, time + compute: apply inputs to OpenDSS, solve once, read measurements + outputs: voltages, powers, currents, frequency, states + -> ControllerExplicitComponent instances + inputs: circuit measurements, time, profiles, controller settings + compute: calculate commands and residual/error outputs + outputs: OpenDSS commands and controller diagnostics + -> OpenMDAO nonlinear solver + owns iteration, residual evaluation, tolerance, and iteration limit + -> result export and post-processing +``` + +The circuit and controller components form an OpenMDAO cycle: controller outputs are connected to circuit inputs, and circuit measurements are connected back to controller inputs. The OpenMDAO nonlinear solver resolves that cycle. Components must not call `reSolve`, run a controller loop, or inspect an iteration priority. + +### Component contracts + +`CircuitExplicitComponent` + +- Owns the OpenDSS instance/adapter and all OpenDSS writes and solve calls. +- Declares stable, namespaced inputs for controller commands and exogenous simulation updates. +- Declares stable, namespaced outputs for every measurement consumed by controllers or exports. +- Calls the appropriate OpenDSS solve exactly as required by one `compute` evaluation. +- Reports OpenDSS convergence as a component diagnostic and raises the configured OpenDSS error when the backend solve fails. +- Exposes a time-step commit/reset operation outside `compute` for stateful simulation progression. + +`ControllerExplicitComponent` + +- Subclasses `openmdao.api.ExplicitComponent`. +- Implements `initialize`, `setup`, and `compute`; `compute_partials` is optional and initially uses finite difference metadata where derivatives are needed. +- Reads only OpenMDAO inputs and immutable controller configuration during `compute`. +- Writes only OpenMDAO outputs; it does not mutate OpenDSS objects directly. +- Emits command outputs plus a normalized residual/diagnostic output where useful for reporting. +- Uses explicit time-step lifecycle methods for committed state. Nonlinear trial evaluations must not advance time-series state. + +`OpenDSSModel` + +- Replaces the current `OpenDSS` controller/convergence responsibilities with model construction, time-step execution, and result integration. +- Builds one OpenMDAO `Problem` per active circuit/model context and one reusable `Group` per simulation. +- Configures the selected OpenMDAO nonlinear solver from the project convergence settings. +- Keeps result storage, HELICS exchange, profiles, frequency sweeps, and post-processors outside the nonlinear model unless a signal is required as an explicit component input/output. + +## Breaking API and Configuration Changes + +1. Add OpenMDAO as a required project dependency. +2. Replace the abstract `Update(Priority, Time, UpdateResults)` contract with OpenMDAO lifecycle methods. +3. Remove `CONTROLLER_PRIORITIES`, `ControllerManager` priority iteration, `_update_controllers`, and controller-local convergence aggregation. +4. Remove `dssSolver` from controller constructors. Controllers receive OpenMDAO inputs instead of a solver or DSS object. +5. Remove direct DSS object access from controller implementations. A narrow measurement/command schema becomes the only controller data contract. +6. Remove all priority fields and enums, including PV watt/var priority behavior. Any resulting command arbitration must be represented explicitly as a controller output rule or circuit command constraint. +7. Replace `Control1`, `Control2`, and `Control3` configuration with an ordered or named control strategy declared by each component. The new schema must not translate old keys. +8. Remove `disable_pydss_controllers`, PyDSS controller iteration settings, and PyDSS controller convergence thresholds from runtime behavior. Retain only settings that configure OpenMDAO nonlinear solving and backend OpenDSS failure policy. +9. Update default controller TOML files, scenario files, registry models, docs, examples, and CLI/config validation together. Old files should fail validation with a clear migration error rather than being silently interpreted. + +## Implementation Phases + +### Phase 1: Establish the OpenMDAO integration boundary + +- Add the dependency and verify the supported OpenMDAO version on Python 3.10+. +- Introduce an OpenDSS adapter with explicit methods for model initialization, applying commands, solving, reading measurements, advancing time, and resetting state. +- Define typed measurement and command schemas with deterministic variable names and units. +- Define the `CircuitExplicitComponent` skeleton and a minimal test circuit. +- Define the OpenMDAO model factory and solver configuration from `ProjectModel`. +- Add tests proving a circuit-only OpenMDAO problem can execute a time point and expose OpenDSS convergence failures. + +**Exit criteria:** an OpenMDAO problem can execute one circuit evaluation without any controller code and produces stable, documented variables. + +### Phase 2: Replace controller discovery and construction + +- Replace `pyController.Create` and constructor injection of DSS globals with a registry of `ExplicitComponent` classes and validated component settings. +- Replace `ControllerAbstract` with the new component base class and shared helpers for variable declarations, units, diagnostics, and committed state. +- Update controller model classes to describe component inputs, outputs, and settings rather than legacy method arguments. +- Make controller registration deterministic and independent of OpenDSS active-class iteration. +- Add model-factory tests for one controller instance, multiple controller instances, missing elements, and invalid settings. + +**Exit criteria:** controllers are created as OpenMDAO subsystems and connected by variable names, with no `dssInstance`, `dssSolver`, or priority argument in the construction path. + +### Phase 3: Migrate controllers by behavioral family + +Migrate each implementation to `setup` and `compute`, moving all DSS interaction to declared variables and the circuit component. + +1. `PvController`: constant PF, variable PF, volt/var, volt/watt, and cutoff strategies. Remove priority-based watt/var selection and internal `reSolve` calls. +2. `GenController` and `StorageController`: dispatch, PF, volt/var, scheduled, TOU, demand-charge, and non-export behavior. Represent circuit power and time as inputs; commit energy/state history once per accepted time step. +3. `MotorStall`, `MotorStallBackup`, and `MotorStallSimple`: voltage/current/power measurements to model commands. Decide whether backup behavior remains a separate component or becomes a strategy setting. +4. `PvVoltageRideThru`, `PvFrequencyRideThru`, and `DynamicVoltageSupport`: ride-through state machines with explicit state inputs/outputs and a time-step commit boundary. +5. `ThermostaticLoad` and `xfmrController`: load and regulator command outputs driven by declared measurements. +6. `FaultController`: event/time input to fault command outputs. +7. `SocketController`: external command inputs/outputs represented as an explicit interface component, with socket I/O outside nonlinear trial evaluation. +8. `PvDynamic`: isolate PVDER interaction behind declared inputs/outputs and define derivative behavior for the external dynamic model. + +For every controller, preserve intended physical behavior but remove `Update`, priority branching, direct DSS calls, controller-owned solves, and active-class traversal. + +**Exit criteria:** every registered controller is an `ExplicitComponent`; no controller source contains a priority parameter, direct OpenDSS solve, or direct mutation of a DSS element. + +### Phase 4: Integrate the nonlinear model into simulation modes + +- Replace the controller block in `OpenDSS.RunStep` with one OpenMDAO model execution per time step. +- Configure the OpenMDAO nonlinear solver with the selected maximum iterations, absolute/relative tolerance, and print/recording options. +- Map OpenMDAO solver failure to the existing user-facing simulation failure policy, then remove the old PyDSS convergence counters and checks. +- Keep OpenDSS backend convergence separate from OpenMDAO model convergence and report both distinctly. +- Define the accepted-step lifecycle: apply profiles/HELICS inputs, execute the OpenMDAO problem, commit controller state, update exports, then advance OpenDSS time. +- Integrate snapshot, QSTS, dynamic, harmonic sweep, HELICS, Monte Carlo, dry-run, and post-processing paths. + +**Exit criteria:** all supported simulation modes execute through OpenMDAO, and no simulation path invokes the old controller manager or priority loop. + +### Phase 5: Remove legacy implementation and migrate data + +- Delete or replace `controllers.py` legacy manager, `pyControllerAbstract.py` legacy protocol, legacy factory paths, and unused priority enumerations. +- Remove old convergence fields and controller-specific iteration reporting from `dssInstance.py`. +- Migrate all default and test scenario TOML files to the new schema. +- Update registry serialization, scenario creation, CLI editing, docs, examples, and API callers. +- Remove obsolete compatibility code and imports after repository-wide reference checks. + +**Exit criteria:** repository search finds no legacy `Update` contract, priority loop, `CONTROLLER_PRIORITIES`, controller `reSolve`, or old controller configuration keys in active code/configuration. + +### Phase 6: Verification and performance hardening + +- Add isolated component tests using a fake circuit adapter and OpenMDAO problem tests using real OpenDSS fixtures. +- Add convergence tests for coupled controller/circuit cycles, solver iteration limits, tolerances, backend failure, and non-convergence reporting. +- Re-run existing controller scenarios and compare exported physical results against approved baselines. +- Add statefulness tests proving repeated nonlinear trial evaluations do not advance time or double-count energy/history. +- Add integration coverage for HELICS, socket control, dynamic voltage support, ride-through, frequency sweep, and post-processors. +- Profile repeated OpenDSS reads and OpenMDAO evaluations; cache only immutable metadata and measurements that are valid within one evaluation. + +**Exit criteria:** focused tests and the full test suite pass, result deltas are reviewed for each controller family, and OpenMDAO iteration diagnostics are available for failed simulations. + +## Validation Matrix + +| Area | Required validation | +|---|---| +| Circuit component | Inputs are applied once per evaluation; outputs reflect the solved circuit; backend failure is surfaced | +| Controller component | `setup` variables are complete; `compute` is deterministic for identical inputs; no DSS or solver dependency | +| Coupling | OpenMDAO nonlinear solver converges a controller/circuit feedback loop within configured limits | +| Stateful controls | Trial evaluations are side-effect free; accepted time steps commit exactly once | +| Configuration | New schema rejects legacy keys and validates component-specific settings | +| Simulation modes | Snapshot, QSTS, dynamic, harmonic, HELICS, Monte Carlo, and dry-run behavior remains covered | +| Results | HDF5 exports, reports, convergence diagnostics, and `skip_export_on_convergence_error` semantics are verified | +| Regression | Existing controller project fixtures are migrated and compared against approved outputs | + +## Design Decisions Required Before Implementation + +These are the only decisions that materially change the implementation shape: + +1. **OpenDSS instance isolation:** should each OpenMDAO `Problem` own a dedicated OpenDSS engine instance, or should the existing process-global `opendssdirect` engine be retained behind the adapter? Dedicated instances are the safer boundary for repeated problems and future parallel execution; retaining the global engine minimizes initial changes but prevents independent simultaneous models. +2. **OpenMDAO nonlinear solver:** should the default be `NewtonSolver`, `BroydenSolver`, or `NonlinearBlockGS`? The proposed initial default is `NonlinearBlockGS` because the OpenDSS component is a black-box explicit evaluation and finite-difference Jacobians may be expensive or unreliable. A Newton/Broyden option can be exposed after convergence baselines exist. +3. **Derivatives:** should all components initially declare finite-difference partials, or should the first release declare no derivatives and use a solver that does not require them? The proposed default is no analytic derivatives and `NonlinearBlockGS`, with finite-difference metadata added only where a chosen solver requires it. +4. **Controller composition:** should a configured controller with multiple behaviors become one component with a strategy setting, or should each behavior be a separate connected component? The proposed default is one component per configured controlled element, with internal strategy dispatch that remains pure inside `compute`. +5. **OpenMDAO grouping:** should all controller instances share one group per circuit, or should each controller family be placed in a subgroup? The proposed default is one circuit group with family subgroups for naming and diagnostics. +6. **State lifecycle API:** is it acceptable for the simulation driver to call an explicit `commit_step()` method after a successful OpenMDAO solve? This is the proposed approach because mutating state inside `compute` is unsafe under nonlinear trial evaluations. + +## Approval Gate + +Implementation starts only after this plan and the six decisions above are approved. The first implementation change will be the Phase 1 boundary and its focused circuit-component test; no legacy behavior will be preserved while the migration is underway. \ No newline at end of file diff --git a/README.md b/README.md index fa605434..9e889088 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ ## Key Features -- **Custom Control Algorithms** — Define Python-based controllers for any circuit element, executed at each simulation time step. 13 built-in controllers are included (PV, storage, motor stall, fault, transformer, thermostat, and more). +- **OpenMDAO Controller Convergence** — Couple Python controller components and OpenDSS in a + nonlinear solve at each simulation time step, with configurable convergence tolerances and + diagnostics. 14 built-in controllers are included (PV, storage, motor stall, fault, transformer, + thermostat, and more). - **HELICS Co-simulation** — Integrate with external simulators via the [HELICS](https://github.com/GMLC-TDC/HELICS) framework for cyber-physical co-simulation studies. - **Scenario Management** — Run multiple scenarios on a shared OpenDSS model with independent controllers, exports, and post-processing. - **Flexible Data Export** — Export results to HDF5 or CSV with per-element filtering, regex-based selection, moving averages, and group aggregation. diff --git a/docs/make_model_tables.py b/docs/make_model_tables.py index 7ed3294e..99078989 100644 --- a/docs/make_model_tables.py +++ b/docs/make_model_tables.py @@ -8,7 +8,7 @@ from collections import defaultdict from pathlib import Path -import click +import typer from pydss import common from pydss import simulation_input_models @@ -41,16 +41,14 @@ ABSTRACT_TYPES = ("InputsBaseModel", "ReportsBaseModel", "ReportBase") -@click.command() -@click.option( - "-o", - "--output", - default="build/model_tables", - show_default=True, - help="output directory", - callback=lambda _, __, x: Path(x), -) -def make_tables(output): +def make_tables( + output: Path = typer.Option( + Path("build/model_tables"), + "-o", + "--output", + help="output directory", + ), +): os.makedirs(output, exist_ok=True) ordered_names, classes = get_ordered_class_names() all_names = set(ordered_names) @@ -131,8 +129,7 @@ def get_ordered_class_names(): items = inspect.getmembers( simulation_input_models, - lambda x: inspect.isclass(x) - and issubclass(x, simulation_input_models.InputsBaseModel), + lambda x: inspect.isclass(x) and issubclass(x, simulation_input_models.InputsBaseModel), ) classes = {x[0]: x[1] for x in items} class_names = set(classes.keys()) @@ -189,9 +186,9 @@ def parse_property_types(ordered_names, classes): definition = match.group(1) property_types[name][title] = definition else: - print(f"WARNING: Possible bug: need handling of %s", vals["allOf"]) + print("WARNING: Possible bug: need handling of %s", vals["allOf"]) return property_types if __name__ == "__main__": - make_tables() + typer.run(make_tables) diff --git a/docs/source/CLI interface.rst b/docs/source/CLI interface.rst index 596c4117..f766e49e 100644 --- a/docs/source/CLI interface.rst +++ b/docs/source/CLI interface.rst @@ -1,7 +1,10 @@ CLI Interface ^^^^^^^^^^^^^ -.. click:: pydss.cli.pydss:cli - :prog: Command line interface - :nested: full +The command-line interface is exposed as ``pydss``. Use ``pydss --help`` to list the available +commands and ``pydss --help`` for command-specific options. + +.. automodule:: pydss.cli.pydss + :members: cli + :undoc-members: diff --git a/docs/source/Co-simulation Interfaces.rst b/docs/source/Co-simulation Interfaces.rst index be34377d..f36567d1 100644 --- a/docs/source/Co-simulation Interfaces.rst +++ b/docs/source/Co-simulation Interfaces.rst @@ -17,7 +17,8 @@ The HELICS interface for PyDSS is built to reduce the complexity of setting up l co-simulation scenarios. The user defines publications and subscriptions to exchange data with external federates. -A minimal HELICS example is availble in the ``examples`` folder (top directory of the repository). Enabling the HELICS interface requires user to define additional parammeters in the scenario TOML file. +A minimal HELICS example is available in the ``examples`` folder at the repository root. Enabling +the HELICS interface requires additional parameters in the scenario TOML file. Interface Overview @@ -137,7 +138,7 @@ The Socket Interface -------------------- The socket interface is implemented as a PyDSS controller -(:py:class:`pydss.pyControllers.Controllers.SocketController.SocketController`). +(:py:class:`pydss.py_controllers.controllers.socket_controller.SocketController`). It is well suited for situations where an existing external controller needs to be integrated into the simulation environment — for example, integrating a controller for thermostatically controlled loads implemented in Modelica or Python. This allows integration without modifying diff --git a/docs/source/Models.rst b/docs/source/Models.rst index aa67e30a..41e1304b 100644 --- a/docs/source/Models.rst +++ b/docs/source/Models.rst @@ -30,8 +30,6 @@ Simulation input models .. autopydantic_model:: pydss.simulation_input_models.FeederLossesReportModel -.. autopydantic_model:: pydss.simulation_input_models.FeederLossesReportModel - .. autopydantic_model:: pydss.simulation_input_models.PvClippingReportModel .. autopydantic_model:: pydss.simulation_input_models.PvCurtailmentReportModel @@ -49,9 +47,7 @@ Simulation input models Scenario setup models ************************ -.. autopydantic_model:: pydss.controllers.ControllerBaseModel - -.. autopydantic_model:: pydss.controllers.PvControllerModel +.. autopydantic_model:: pydss.py_controllers.models.PvControllerModel Thermal metrics models @@ -80,8 +76,6 @@ Voltage metrics models .. autopydantic_model:: pydss.node_voltage_metrics.VoltageMetric5 -.. autopydantic_model:: pydss.node_voltage_metrics.VoltageMetric5 - .. autopydantic_model:: pydss.node_voltage_metrics.VoltageMetricsSummaryModel .. autopydantic_model:: pydss.node_voltage_metrics.VoltageMetricsModel diff --git a/docs/source/Scenario_settings.toml b/docs/source/Scenario_settings.toml index 50c33e4c..ebe135df 100644 --- a/docs/source/Scenario_settings.toml +++ b/docs/source/Scenario_settings.toml @@ -13,10 +13,10 @@ "End Time (min)" = 1439.0 "Date offset" = 0 "Step resolution (sec)" = 900.0 -"Max Control Iterations" = 15 +"Max Control Iterations" = 50 "Error tolerance" = 0.001 +"OpenMDAO Reports" = false "Control mode" = "Time" -"Disable pydss controllers" = false "Simulation Type" = "QSTS" "Project Path" = "" "Active Project" = "" diff --git a/docs/source/_autosummary/PyDSS.Extensions.rst b/docs/source/_autosummary/PyDSS.Extensions.rst index dc5870ab..2e9a6b5d 100644 --- a/docs/source/_autosummary/PyDSS.Extensions.rst +++ b/docs/source/_autosummary/PyDSS.Extensions.rst @@ -1,7 +1,7 @@ -pydss.Extensions +pydss.extensions ================ -.. automodule:: pydss.Extensions +.. automodule:: pydss.extensions .. rubric:: Modules @@ -10,4 +10,4 @@ pydss.Extensions :toctree: :recursive: - MonteCarlo + monte_carlo diff --git a/docs/source/_autosummary/PyDSS.ProfileManager.common.rst b/docs/source/_autosummary/PyDSS.ProfileManager.common.rst deleted file mode 100644 index 3547d273..00000000 --- a/docs/source/_autosummary/PyDSS.ProfileManager.common.rst +++ /dev/null @@ -1,6 +0,0 @@ -pydss.ProfileManager.common -=========================== - -.. automodule:: pydss.ProfileManager.common - - \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.api.server.rst b/docs/source/_autosummary/PyDSS.api.server.rst index 5d43455a..9cc87edf 100644 --- a/docs/source/_autosummary/PyDSS.api.server.rst +++ b/docs/source/_autosummary/PyDSS.api.server.rst @@ -14,7 +14,7 @@ .. autosummary:: find_version - getJSONschema + get_json_schema read @@ -25,7 +25,7 @@ .. autosummary:: - pydss_server + PydssServer diff --git a/docs/source/_autosummary/PyDSS.api.src.app.DataWriter.rst b/docs/source/_autosummary/PyDSS.api.src.app.DataWriter.rst index 8a09519c..58b42214 100644 --- a/docs/source/_autosummary/PyDSS.api.src.app.DataWriter.rst +++ b/docs/source/_autosummary/PyDSS.api.src.app.DataWriter.rst @@ -1,7 +1,7 @@ -pydss.api.src.app.DataWriter +pydss.api.src.app.data_writer ============================ -.. automodule:: pydss.api.src.app.DataWriter +.. automodule:: pydss.api.src.app.data_writer .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.api.src.app.HDF5.rst b/docs/source/_autosummary/PyDSS.api.src.app.HDF5.rst index fb9e3405..6bca12cd 100644 --- a/docs/source/_autosummary/PyDSS.api.src.app.HDF5.rst +++ b/docs/source/_autosummary/PyDSS.api.src.app.HDF5.rst @@ -1,12 +1,12 @@ -pydss.api.src.app.HDF5 +pydss.api.src.app.hdf5 ====================== -.. automodule:: pydss.api.src.app.HDF5 +.. automodule:: pydss.api.src.app.hdf5 .. rubric:: Classes .. autosummary:: - hdf5Writer + Hdf5Writer \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.api.src.app.JSON_writer.rst b/docs/source/_autosummary/PyDSS.api.src.app.JSON_writer.rst index f2e15391..949fdba3 100644 --- a/docs/source/_autosummary/PyDSS.api.src.app.JSON_writer.rst +++ b/docs/source/_autosummary/PyDSS.api.src.app.JSON_writer.rst @@ -1,12 +1,12 @@ pydss.api.src.app.JSON\_writer ============================== -.. automodule:: pydss.api.src.app.JSON_writer +.. automodule:: pydss.api.src.app.json_writer .. rubric:: Classes .. autosummary:: - JSONwriter + JsonWriter \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.api.src.app.Tester.rst b/docs/source/_autosummary/PyDSS.api.src.app.Tester.rst index 8c154f01..0efd4336 100644 --- a/docs/source/_autosummary/PyDSS.api.src.app.Tester.rst +++ b/docs/source/_autosummary/PyDSS.api.src.app.Tester.rst @@ -1,7 +1,7 @@ -pydss.api.src.app.Tester +pydss.api.src.app.tester ======================== -.. automodule:: pydss.api.src.app.Tester +.. automodule:: pydss.api.src.app.tester .. rubric:: Functions diff --git a/docs/source/_autosummary/PyDSS.api.src.web.create_schema.rst b/docs/source/_autosummary/PyDSS.api.src.web.create_schema.rst index b1b11cdf..ab5e236d 100644 --- a/docs/source/_autosummary/PyDSS.api.src.web.create_schema.rst +++ b/docs/source/_autosummary/PyDSS.api.src.web.create_schema.rst @@ -8,5 +8,5 @@ pydss.api.src.web.create\_schema .. autosummary:: - getJSONschema + get_json_schema \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.api.src.web.handler.rst b/docs/source/_autosummary/PyDSS.api.src.web.handler.rst index f6906b54..01a427f1 100644 --- a/docs/source/_autosummary/PyDSS.api.src.web.handler.rst +++ b/docs/source/_autosummary/PyDSS.api.src.web.handler.rst @@ -3,27 +3,9 @@ .. automodule:: pydss.api.src.web.handler - - - - - - - - - .. rubric:: Classes .. autosummary:: - - Handler - - - - - - - - + Handler diff --git a/docs/source/_autosummary/PyDSS.cli.add_post_process.rst b/docs/source/_autosummary/PyDSS.cli.add_post_process.rst index c5f1f0e2..b3d1d6fe 100644 --- a/docs/source/_autosummary/PyDSS.cli.add_post_process.rst +++ b/docs/source/_autosummary/PyDSS.cli.add_post_process.rst @@ -1,6 +1,11 @@ -pydss.cli.add\_post\_process +pydss.cli.add\_post\_process ============================ .. automodule:: pydss.cli.add_post_process - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + add_post_process diff --git a/docs/source/_autosummary/PyDSS.cli.add_scenario.rst b/docs/source/_autosummary/PyDSS.cli.add_scenario.rst index 863d7c2d..11b71ecc 100644 --- a/docs/source/_autosummary/PyDSS.cli.add_scenario.rst +++ b/docs/source/_autosummary/PyDSS.cli.add_scenario.rst @@ -1,4 +1,4 @@ -pydss.cli.add\_scenario +pydss.cli.add\_scenario ======================= .. automodule:: pydss.cli.add_scenario @@ -8,5 +8,6 @@ pydss.cli.add\_scenario .. autosummary:: + add_scenario build_scenario \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.cli.controllers.rst b/docs/source/_autosummary/PyDSS.cli.controllers.rst index e52cc2ef..c3b57a98 100644 --- a/docs/source/_autosummary/PyDSS.cli.controllers.rst +++ b/docs/source/_autosummary/PyDSS.cli.controllers.rst @@ -1,6 +1,14 @@ -pydss.cli.controllers +pydss.cli.controllers ===================== .. automodule:: pydss.cli.controllers - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + register + reset_defaults + show + unregister diff --git a/docs/source/_autosummary/PyDSS.cli.convert.rst b/docs/source/_autosummary/PyDSS.cli.convert.rst index e3374466..4ea5d90b 100644 --- a/docs/source/_autosummary/PyDSS.cli.convert.rst +++ b/docs/source/_autosummary/PyDSS.cli.convert.rst @@ -1,6 +1,12 @@ -pydss.cli.convert +pydss.cli.convert ================= .. automodule:: pydss.cli.convert - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + excel_to_toml + simulation_file diff --git a/docs/source/_autosummary/PyDSS.cli.create_project.rst b/docs/source/_autosummary/PyDSS.cli.create_project.rst index f6479c23..cddcf374 100644 --- a/docs/source/_autosummary/PyDSS.cli.create_project.rst +++ b/docs/source/_autosummary/PyDSS.cli.create_project.rst @@ -1,6 +1,11 @@ -pydss.cli.create\_project +pydss.cli.create\_project ========================= .. automodule:: pydss.cli.create_project - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + create_project diff --git a/docs/source/_autosummary/PyDSS.cli.edit_scenario.rst b/docs/source/_autosummary/PyDSS.cli.edit_scenario.rst index 5c53432e..d5ccf35a 100644 --- a/docs/source/_autosummary/PyDSS.cli.edit_scenario.rst +++ b/docs/source/_autosummary/PyDSS.cli.edit_scenario.rst @@ -1,6 +1,12 @@ -pydss.cli.edit\_scenario +pydss.cli.edit\_scenario ======================== .. automodule:: pydss.cli.edit_scenario - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + main + update_controllers diff --git a/docs/source/_autosummary/PyDSS.cli.export.rst b/docs/source/_autosummary/PyDSS.cli.export.rst index 5715de11..7127985e 100644 --- a/docs/source/_autosummary/PyDSS.cli.export.rst +++ b/docs/source/_autosummary/PyDSS.cli.export.rst @@ -1,6 +1,11 @@ -pydss.cli.export +pydss.cli.export ================ .. automodule:: pydss.cli.export - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + export diff --git a/docs/source/_autosummary/PyDSS.cli.extract.rst b/docs/source/_autosummary/PyDSS.cli.extract.rst index 467b796e..ec347bb5 100644 --- a/docs/source/_autosummary/PyDSS.cli.extract.rst +++ b/docs/source/_autosummary/PyDSS.cli.extract.rst @@ -1,6 +1,12 @@ -pydss.cli.extract +pydss.cli.extract ================= .. automodule:: pydss.cli.extract - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + extract + extract_element_files diff --git a/docs/source/_autosummary/PyDSS.cli.pydss.rst b/docs/source/_autosummary/PyDSS.cli.pydss.rst index 981870a8..3ea0c6f5 100644 --- a/docs/source/_autosummary/PyDSS.cli.pydss.rst +++ b/docs/source/_autosummary/PyDSS.cli.pydss.rst @@ -1,6 +1,11 @@ -pydss.cli.pydss +pydss.cli.pydss =============== .. automodule:: pydss.cli.pydss - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + main diff --git a/docs/source/_autosummary/PyDSS.cli.reports.rst b/docs/source/_autosummary/PyDSS.cli.reports.rst index 0b5530ce..90e5a167 100644 --- a/docs/source/_autosummary/PyDSS.cli.reports.rst +++ b/docs/source/_autosummary/PyDSS.cli.reports.rst @@ -1,4 +1,4 @@ -pydss.cli.reports +pydss.cli.reports ================= .. automodule:: pydss.cli.reports @@ -8,7 +8,8 @@ pydss.cli.reports .. autosummary:: - getAvailableReports - getReportTypes - printReport + get_available_reports + get_report_types + print_report + reports \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.cli.run.rst b/docs/source/_autosummary/PyDSS.cli.run.rst index 370a294d..1b495d9a 100644 --- a/docs/source/_autosummary/PyDSS.cli.run.rst +++ b/docs/source/_autosummary/PyDSS.cli.run.rst @@ -1,6 +1,11 @@ -pydss.cli.run +pydss.cli.run ============= .. automodule:: pydss.cli.run - \ No newline at end of file + + .. rubric:: Functions + + .. autosummary:: + + run diff --git a/docs/source/_autosummary/PyDSS.controllers.rst b/docs/source/_autosummary/PyDSS.controllers.rst deleted file mode 100644 index 1f7303b8..00000000 --- a/docs/source/_autosummary/PyDSS.controllers.rst +++ /dev/null @@ -1,19 +0,0 @@ -pydss.controllers -================= - -.. automodule:: pydss.controllers - - - .. rubric:: Functions - - .. autosummary:: - - make_default_volt_var_controller - - .. rubric:: Classes - - .. autosummary:: - - CircuitElementController - ControllerManager - \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.dssBus.rst b/docs/source/_autosummary/PyDSS.dssBus.rst index b71ee6bf..ea7b122b 100644 --- a/docs/source/_autosummary/PyDSS.dssBus.rst +++ b/docs/source/_autosummary/PyDSS.dssBus.rst @@ -1,12 +1,12 @@ -pydss.dssBus +pydss.dss_bus ============ -.. automodule:: pydss.dssBus +.. automodule:: pydss.dss_bus .. rubric:: Classes .. autosummary:: - dssBus + DssBus \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.dssCircuit.rst b/docs/source/_autosummary/PyDSS.dssCircuit.rst index 2db0407f..793a13a8 100644 --- a/docs/source/_autosummary/PyDSS.dssCircuit.rst +++ b/docs/source/_autosummary/PyDSS.dssCircuit.rst @@ -1,12 +1,12 @@ -pydss.dssCircuit +pydss.dss_circuit ================ -.. automodule:: pydss.dssCircuit +.. automodule:: pydss.dss_circuit .. rubric:: Classes .. autosummary:: - dssCircuit + DssCircuit \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.dssElement.rst b/docs/source/_autosummary/PyDSS.dssElement.rst index cd82c087..f2dfbb3d 100644 --- a/docs/source/_autosummary/PyDSS.dssElement.rst +++ b/docs/source/_autosummary/PyDSS.dssElement.rst @@ -1,12 +1,12 @@ -pydss.dssElement +pydss.dss_element ================ -.. automodule:: pydss.dssElement +.. automodule:: pydss.dss_element .. rubric:: Classes .. autosummary:: - dssElement + DssElement \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.dssElementFactory.rst b/docs/source/_autosummary/PyDSS.dssElementFactory.rst index 02b64cf4..e99a8f91 100644 --- a/docs/source/_autosummary/PyDSS.dssElementFactory.rst +++ b/docs/source/_autosummary/PyDSS.dssElementFactory.rst @@ -1,7 +1,7 @@ -pydss.dssElementFactory +pydss.dss_element_factory ======================= -.. automodule:: pydss.dssElementFactory +.. automodule:: pydss.dss_element_factory .. rubric:: Functions diff --git a/docs/source/_autosummary/PyDSS.dssInstance.rst b/docs/source/_autosummary/PyDSS.dssInstance.rst index 277e4156..914b5bf9 100644 --- a/docs/source/_autosummary/PyDSS.dssInstance.rst +++ b/docs/source/_autosummary/PyDSS.dssInstance.rst @@ -1,7 +1,7 @@ -pydss.dssInstance +pydss.dss_instance ================= -.. automodule:: pydss.dssInstance +.. automodule:: pydss.dss_instance .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.dssObjectBase.rst b/docs/source/_autosummary/PyDSS.dssObjectBase.rst index 37a3ba27..166900c6 100644 --- a/docs/source/_autosummary/PyDSS.dssObjectBase.rst +++ b/docs/source/_autosummary/PyDSS.dssObjectBase.rst @@ -1,12 +1,12 @@ -pydss.dssObjectBase +pydss.dss_object_base =================== -.. automodule:: pydss.dssObjectBase +.. automodule:: pydss.dss_object_base .. rubric:: Classes .. autosummary:: - dssObjectBase + DssObjectBase \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.dssTransformer.rst b/docs/source/_autosummary/PyDSS.dssTransformer.rst index 2192c713..5ef0dcd3 100644 --- a/docs/source/_autosummary/PyDSS.dssTransformer.rst +++ b/docs/source/_autosummary/PyDSS.dssTransformer.rst @@ -1,12 +1,12 @@ -pydss.dssTransformer +pydss.dss_transformer ==================== -.. automodule:: pydss.dssTransformer +.. automodule:: pydss.dss_transformer .. rubric:: Classes .. autosummary:: - dssTransformer + DssTransformer \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.Extensions.MonteCarlo.rst b/docs/source/_autosummary/PyDSS.extensions.monte_carlo.rst similarity index 58% rename from docs/source/_autosummary/PyDSS.Extensions.MonteCarlo.rst rename to docs/source/_autosummary/PyDSS.extensions.monte_carlo.rst index 2ab2b966..c1814e7f 100644 --- a/docs/source/_autosummary/PyDSS.Extensions.MonteCarlo.rst +++ b/docs/source/_autosummary/PyDSS.extensions.monte_carlo.rst @@ -1,7 +1,7 @@ -pydss.Extensions.MonteCarlo +pydss.extensions.monte_carlo =========================== -.. automodule:: pydss.Extensions.MonteCarlo +.. automodule:: pydss.extensions.monte_carlo .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.helics_interface.rst b/docs/source/_autosummary/PyDSS.helics_interface.rst index f38175f7..e1807866 100644 --- a/docs/source/_autosummary/PyDSS.helics_interface.rst +++ b/docs/source/_autosummary/PyDSS.helics_interface.rst @@ -8,5 +8,5 @@ pydss.helics\_interface .. autosummary:: - helics_interface + HelicsInterface \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.modes.Dynamic.rst b/docs/source/_autosummary/PyDSS.modes.Dynamic.rst index 6854b38e..a607e19d 100644 --- a/docs/source/_autosummary/PyDSS.modes.Dynamic.rst +++ b/docs/source/_autosummary/PyDSS.modes.Dynamic.rst @@ -1,7 +1,7 @@ -pydss.modes.Dynamic +pydss.modes.dynamic =================== -.. automodule:: pydss.modes.Dynamic +.. automodule:: pydss.modes.dynamic .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.modes.QSTS.rst b/docs/source/_autosummary/PyDSS.modes.QSTS.rst index 8ac19df8..2812253d 100644 --- a/docs/source/_autosummary/PyDSS.modes.QSTS.rst +++ b/docs/source/_autosummary/PyDSS.modes.QSTS.rst @@ -1,7 +1,7 @@ -pydss.modes.QSTS +pydss.modes.qsts ================ -.. automodule:: pydss.modes.QSTS +.. automodule:: pydss.modes.qsts .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.modes.Snapshot.rst b/docs/source/_autosummary/PyDSS.modes.Snapshot.rst index 21ea8078..33e7c4db 100644 --- a/docs/source/_autosummary/PyDSS.modes.Snapshot.rst +++ b/docs/source/_autosummary/PyDSS.modes.Snapshot.rst @@ -1,7 +1,7 @@ -pydss.modes.Snapshot +pydss.modes.snapshot ==================== -.. automodule:: pydss.modes.Snapshot +.. automodule:: pydss.modes.snapshot .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.ProfileManager.base_definitions.rst b/docs/source/_autosummary/PyDSS.profile_manager.base_definitions.rst similarity index 59% rename from docs/source/_autosummary/PyDSS.ProfileManager.base_definitions.rst rename to docs/source/_autosummary/PyDSS.profile_manager.base_definitions.rst index 0729e74f..15749014 100644 --- a/docs/source/_autosummary/PyDSS.ProfileManager.base_definitions.rst +++ b/docs/source/_autosummary/PyDSS.profile_manager.base_definitions.rst @@ -1,7 +1,7 @@ -pydss.ProfileManager.base\_definitions +pydss.profile_manager.base\_definitions ====================================== -.. automodule:: pydss.ProfileManager.base_definitions +.. automodule:: pydss.profile_manager.base_definitions .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.profile_manager.common.rst b/docs/source/_autosummary/PyDSS.profile_manager.common.rst new file mode 100644 index 00000000..0d05b10e --- /dev/null +++ b/docs/source/_autosummary/PyDSS.profile_manager.common.rst @@ -0,0 +1,6 @@ +pydss.profile_manager.common +=========================== + +.. automodule:: pydss.profile_manager.common + + \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.ProfileManager.hooks.h5.rst b/docs/source/_autosummary/PyDSS.profile_manager.hooks.h5.rst similarity index 61% rename from docs/source/_autosummary/PyDSS.ProfileManager.hooks.h5.rst rename to docs/source/_autosummary/PyDSS.profile_manager.hooks.h5.rst index 3b4d0012..93cd5ff4 100644 --- a/docs/source/_autosummary/PyDSS.ProfileManager.hooks.h5.rst +++ b/docs/source/_autosummary/PyDSS.profile_manager.hooks.h5.rst @@ -1,7 +1,7 @@ -pydss.ProfileManager.hooks.h5 +pydss.profile_manager.hooks.h5 ============================= -.. automodule:: pydss.ProfileManager.hooks.h5 +.. automodule:: pydss.profile_manager.hooks.h5 .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.ProfileManager.hooks.MongoDB.rst b/docs/source/_autosummary/PyDSS.profile_manager.hooks.mongodb.rst similarity index 59% rename from docs/source/_autosummary/PyDSS.ProfileManager.hooks.MongoDB.rst rename to docs/source/_autosummary/PyDSS.profile_manager.hooks.mongodb.rst index 727b2e20..ada01bf6 100644 --- a/docs/source/_autosummary/PyDSS.ProfileManager.hooks.MongoDB.rst +++ b/docs/source/_autosummary/PyDSS.profile_manager.hooks.mongodb.rst @@ -1,7 +1,7 @@ -pydss.ProfileManager.hooks.MongoDB +pydss.profile_manager.hooks.mongodb ================================== -.. automodule:: pydss.ProfileManager.hooks.MongoDB +.. automodule:: pydss.profile_manager.hooks.mongodb .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.ProfileManager.hooks.rst b/docs/source/_autosummary/PyDSS.profile_manager.hooks.rst similarity index 61% rename from docs/source/_autosummary/PyDSS.ProfileManager.hooks.rst rename to docs/source/_autosummary/PyDSS.profile_manager.hooks.rst index c211e8e7..9dd88dbe 100644 --- a/docs/source/_autosummary/PyDSS.ProfileManager.hooks.rst +++ b/docs/source/_autosummary/PyDSS.profile_manager.hooks.rst @@ -1,7 +1,7 @@ -pydss.ProfileManager.hooks +pydss.profile_manager.hooks ========================== -.. automodule:: pydss.ProfileManager.hooks +.. automodule:: pydss.profile_manager.hooks .. rubric:: Modules diff --git a/docs/source/_autosummary/PyDSS.ProfileManager.ProfileInterface.rst b/docs/source/_autosummary/PyDSS.profile_manager.profile_interface.rst similarity index 53% rename from docs/source/_autosummary/PyDSS.ProfileManager.ProfileInterface.rst rename to docs/source/_autosummary/PyDSS.profile_manager.profile_interface.rst index 8c7dbba7..1fa2f4ba 100644 --- a/docs/source/_autosummary/PyDSS.ProfileManager.ProfileInterface.rst +++ b/docs/source/_autosummary/PyDSS.profile_manager.profile_interface.rst @@ -1,7 +1,7 @@ -pydss.ProfileManager.ProfileInterface +pydss.profile_manager.profile_interface ===================================== -.. automodule:: pydss.ProfileManager.ProfileInterface +.. automodule:: pydss.profile_manager.profile_interface .. rubric:: Functions diff --git a/docs/source/_autosummary/PyDSS.ProfileManager.rst b/docs/source/_autosummary/PyDSS.profile_manager.rst similarity index 71% rename from docs/source/_autosummary/PyDSS.ProfileManager.rst rename to docs/source/_autosummary/PyDSS.profile_manager.rst index b12f14c9..0fd8f8c6 100644 --- a/docs/source/_autosummary/PyDSS.ProfileManager.rst +++ b/docs/source/_autosummary/PyDSS.profile_manager.rst @@ -1,7 +1,7 @@ -pydss.ProfileManager +pydss.profile_manager ==================== -.. automodule:: pydss.ProfileManager +.. automodule:: pydss.profile_manager .. rubric:: Modules diff --git a/docs/source/_autosummary/PyDSS.pyContrReader.rst b/docs/source/_autosummary/PyDSS.pyContrReader.rst index 812a0486..65f0f758 100644 --- a/docs/source/_autosummary/PyDSS.pyContrReader.rst +++ b/docs/source/_autosummary/PyDSS.pyContrReader.rst @@ -1,7 +1,7 @@ -pydss.pyContrReader +pydss.py_contr_reader =================== -.. automodule:: pydss.pyContrReader +.. automodule:: pydss.py_contr_reader .. rubric:: Functions @@ -14,7 +14,7 @@ pydss.pyContrReader .. autosummary:: - pyContrReader - pyExportReader - pySubscriptionReader + PyContrReader + PyExportReader + PySubscriptionReader \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.Settings.rst b/docs/source/_autosummary/PyDSS.pyControllers.Controllers.Settings.rst deleted file mode 100644 index dadbbee5..00000000 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.Settings.rst +++ /dev/null @@ -1,6 +0,0 @@ -pydss.pyControllers.Controllers.Settings -======================================== - -.. automodule:: pydss.pyControllers.Controllers.Settings - - \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyControllers.enumerations.rst b/docs/source/_autosummary/PyDSS.pyControllers.enumerations.rst deleted file mode 100644 index 396f5813..00000000 --- a/docs/source/_autosummary/PyDSS.pyControllers.enumerations.rst +++ /dev/null @@ -1,6 +0,0 @@ -pydss.pyControllers.enumerations -================================ - -.. automodule:: pydss.pyControllers.enumerations - - \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyControllers.models.rst b/docs/source/_autosummary/PyDSS.pyControllers.models.rst deleted file mode 100644 index ff3a0be7..00000000 --- a/docs/source/_autosummary/PyDSS.pyControllers.models.rst +++ /dev/null @@ -1,6 +0,0 @@ -pydss.pyControllers.models -========================== - -.. automodule:: pydss.pyControllers.models - - \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyControllers.pyControllerAbstract.rst b/docs/source/_autosummary/PyDSS.pyControllers.pyControllerAbstract.rst deleted file mode 100644 index e7a59a00..00000000 --- a/docs/source/_autosummary/PyDSS.pyControllers.pyControllerAbstract.rst +++ /dev/null @@ -1,12 +0,0 @@ -pydss.pyControllers.pyControllerAbstract -======================================== - -.. automodule:: pydss.pyControllers.pyControllerAbstract - - - .. rubric:: Classes - - .. autosummary:: - - ControllerAbstract - \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyDSS.rst b/docs/source/_autosummary/PyDSS.pyDSS.rst index 7a24769c..890db67b 100644 --- a/docs/source/_autosummary/PyDSS.pyDSS.rst +++ b/docs/source/_autosummary/PyDSS.pyDSS.rst @@ -1,7 +1,7 @@ -pydss.pyDSS +pydss.py_dss =========== -.. automodule:: pydss.pyDSS +.. automodule:: pydss.py_dss .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer.rst b/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer.rst deleted file mode 100644 index 19588fc1..00000000 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer.rst +++ /dev/null @@ -1,12 +0,0 @@ -pydss.pyPostprocessor.PostprocessScripts.DERMSOptimizer -======================================================= - -.. automodule:: pydss.pyPostprocessor.PostprocessScripts.DERMSOptimizer - - - .. rubric:: Classes - - .. autosummary:: - - DERMSOptimizer - \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.rst b/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.rst deleted file mode 100644 index bede30a0..00000000 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.rst +++ /dev/null @@ -1,13 +0,0 @@ -pydss.pyPostprocessor.PostprocessScripts.DERMSOptimizer\_helper\_modules -======================================================================== - -.. automodule:: pydss.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - opt_funcs diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.rst b/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.rst deleted file mode 100644 index 81d2a650..00000000 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.rst +++ /dev/null @@ -1,15 +0,0 @@ -pydss.pyPostprocessor.PostprocessScripts -======================================== - -.. automodule:: pydss.pyPostprocessor.PostprocessScripts - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - DERMSOptimizer - DERMSOptimizer_helper_modules - EdLiFoControl diff --git a/docs/source/_autosummary/PyDSS.pyResults.rst b/docs/source/_autosummary/PyDSS.pyResults.rst index f74a24fd..f1a88234 100644 --- a/docs/source/_autosummary/PyDSS.pyResults.rst +++ b/docs/source/_autosummary/PyDSS.pyResults.rst @@ -1,14 +1,14 @@ -pydss.pyResults +pydss.py_results =============== -.. automodule:: pydss.pyResults +.. automodule:: pydss.py_results .. rubric:: Classes .. autosummary:: - pyContrReader - pyExportReader - pySubscriptionReader + PyContrReader + PyExportReader + PySubscriptionReader \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.FaultController.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.FaultController.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.FaultController.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.FaultController.rst index fb9d282e..4f8b6ffe 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.FaultController.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.FaultController.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.FaultController +pydss.py_controllers.controllers.fault_controller =============================================== -.. automodule:: pydss.pyControllers.Controllers.FaultController +.. automodule:: pydss.py_controllers.controllers.fault_controller .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.GenController.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.GenController.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.GenController.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.GenController.rst index 5959db14..b2723659 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.GenController.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.GenController.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.GenController +pydss.py_controllers.controllers.gen_controller ============================================= -.. automodule:: pydss.pyControllers.Controllers.GenController +.. automodule:: pydss.py_controllers.controllers.gen_controller .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStall.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStall.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStall.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStall.rst index a573dab0..bab57fb6 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStall.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStall.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.MotorStall +pydss.py_controllers.controllers.motor_stall ========================================== -.. automodule:: pydss.pyControllers.Controllers.MotorStall +.. automodule:: pydss.py_controllers.controllers.motor_stall .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStallBackup.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStallBackup.rst similarity index 51% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStallBackup.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStallBackup.rst index 44db259c..38746eff 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStallBackup.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStallBackup.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.MotorStallBackup +pydss.py_controllers.controllers.motor_stall_backup ================================================ -.. automodule:: pydss.pyControllers.Controllers.MotorStallBackup +.. automodule:: pydss.py_controllers.controllers.motor_stall_backup .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStallSimple.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStallSimple.rst similarity index 51% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStallSimple.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStallSimple.rst index f805acf0..042f4289 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.MotorStallSimple.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.MotorStallSimple.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.MotorStallSimple +pydss.py_controllers.controllers.motor_stall_simple ================================================ -.. automodule:: pydss.pyControllers.Controllers.MotorStallSimple +.. automodule:: pydss.py_controllers.controllers.motor_stall_simple .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvController.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvController.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvController.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.PvController.rst index 341af2bd..4c5fc758 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvController.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvController.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.PvController +pydss.py_controllers.controllers.pv_controller ============================================ -.. automodule:: pydss.pyControllers.Controllers.PvController +.. automodule:: pydss.py_controllers.controllers.pv_controller .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvDynamic.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvDynamic.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvDynamic.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.PvDynamic.rst index b2ab10bc..149b173c 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvDynamic.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvDynamic.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.PvDynamic +pydss.py_controllers.controllers.pv_dynamic ========================================= -.. automodule:: pydss.pyControllers.Controllers.PvDynamic +.. automodule:: pydss.py_controllers.controllers.pv_dynamic .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvFrequencyRideThru.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvFrequencyRideThru.rst similarity index 51% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvFrequencyRideThru.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.PvFrequencyRideThru.rst index cef8d596..811ccb98 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvFrequencyRideThru.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvFrequencyRideThru.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.PvFrequencyRideThru +pydss.py_controllers.controllers.pv_frequency_ride_thru =================================================== -.. automodule:: pydss.pyControllers.Controllers.PvFrequencyRideThru +.. automodule:: pydss.py_controllers.controllers.pv_frequency_ride_thru .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvVoltageRideThru.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvVoltageRideThru.rst similarity index 51% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvVoltageRideThru.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.PvVoltageRideThru.rst index 13a5612a..39f4889b 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.PvVoltageRideThru.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.PvVoltageRideThru.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.PvVoltageRideThru +pydss.py_controllers.controllers.pv_voltage_ride_thru ================================================= -.. automodule:: pydss.pyControllers.Controllers.PvVoltageRideThru +.. automodule:: pydss.py_controllers.controllers.pv_voltage_ride_thru .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.SocketController.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.SocketController.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.SocketController.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.SocketController.rst index 76f7e9db..46965660 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.SocketController.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.SocketController.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.SocketController +pydss.py_controllers.controllers.socket_controller ================================================ -.. automodule:: pydss.pyControllers.Controllers.SocketController +.. automodule:: pydss.py_controllers.controllers.socket_controller .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.StorageController.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.StorageController.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.StorageController.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.StorageController.rst index 5ffbc006..d8e75072 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.StorageController.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.StorageController.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.StorageController +pydss.py_controllers.controllers.storage_controller ================================================= -.. automodule:: pydss.pyControllers.Controllers.StorageController +.. automodule:: pydss.py_controllers.controllers.storage_controller .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.ThermostaticLoad.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.ThermostaticLoad.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.ThermostaticLoad.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.ThermostaticLoad.rst index 01d004ab..8de44c7a 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.ThermostaticLoad.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.ThermostaticLoad.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.ThermostaticLoad +pydss.py_controllers.controllers.thermostatic_load ================================================ -.. automodule:: pydss.pyControllers.Controllers.ThermostaticLoad +.. automodule:: pydss.py_controllers.controllers.thermostatic_load .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.rst similarity index 82% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.rst index 17097605..ebd82eee 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers +pydss.py_controllers.controllers =============================== -.. automodule:: pydss.pyControllers.Controllers +.. automodule:: pydss.py_controllers.controllers .. rubric:: Modules diff --git a/docs/source/_autosummary/PyDSS.py_controllers.controllers.settings.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.settings.rst new file mode 100644 index 00000000..78388f6c --- /dev/null +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.settings.rst @@ -0,0 +1,6 @@ +pydss.py_controllers.controllers.settings +========================================= + +.. automodule:: pydss.py_controllers.controllers.settings + + \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.xfmrController.rst b/docs/source/_autosummary/PyDSS.py_controllers.controllers.xfmrController.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyControllers.Controllers.xfmrController.rst rename to docs/source/_autosummary/PyDSS.py_controllers.controllers.xfmrController.rst index d87267aa..43c117ce 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.Controllers.xfmrController.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.controllers.xfmrController.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.xfmrController +pydss.py_controllers.controllers.xfmr_controller ============================================== -.. automodule:: pydss.pyControllers.Controllers.xfmrController +.. automodule:: pydss.py_controllers.controllers.xfmr_controller .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.py_controllers.enumerations.rst b/docs/source/_autosummary/PyDSS.py_controllers.enumerations.rst new file mode 100644 index 00000000..f6dc9c91 --- /dev/null +++ b/docs/source/_autosummary/PyDSS.py_controllers.enumerations.rst @@ -0,0 +1,6 @@ +pydss.py_controllers.enumerations +================================ + +.. automodule:: pydss.py_controllers.enumerations + + \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.py_controllers.models.rst b/docs/source/_autosummary/PyDSS.py_controllers.models.rst new file mode 100644 index 00000000..fa47f974 --- /dev/null +++ b/docs/source/_autosummary/PyDSS.py_controllers.models.rst @@ -0,0 +1,6 @@ +pydss.py_controllers.models +========================== + +.. automodule:: pydss.py_controllers.models + + \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyControllers.pyController.rst b/docs/source/_autosummary/PyDSS.py_controllers.pyController.rst similarity index 54% rename from docs/source/_autosummary/PyDSS.pyControllers.pyController.rst rename to docs/source/_autosummary/PyDSS.py_controllers.pyController.rst index c74374f1..b5cd73d4 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.pyController.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.pyController.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.pyController +pydss.py_controllers.py_controller ================================ -.. automodule:: pydss.pyControllers.pyController +.. automodule:: pydss.py_controllers.py_controller .. rubric:: Functions diff --git a/docs/source/_autosummary/PyDSS.pyControllers.rst b/docs/source/_autosummary/PyDSS.py_controllers.rst similarity index 58% rename from docs/source/_autosummary/PyDSS.pyControllers.rst rename to docs/source/_autosummary/PyDSS.py_controllers.rst index 8f27fbb7..c5271df9 100644 --- a/docs/source/_autosummary/PyDSS.pyControllers.rst +++ b/docs/source/_autosummary/PyDSS.py_controllers.rst @@ -1,7 +1,7 @@ -pydss.pyControllers +pydss.py_controllers =================== -.. automodule:: pydss.pyControllers +.. automodule:: pydss.py_controllers .. rubric:: Modules @@ -13,5 +13,4 @@ pydss.pyControllers Controllers enumerations models - pyController - pyControllerAbstract + py_controller diff --git a/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer.rst new file mode 100644 index 00000000..0cdc62a5 --- /dev/null +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer.rst @@ -0,0 +1,12 @@ +pydss.py_postprocessor.postprocess_scripts.derms_optimizer +======================================================= + +.. automodule:: pydss.py_postprocessor.postprocess_scripts.derms_optimizer + + + .. rubric:: Classes + + .. autosummary:: + + derms_optimizer + \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.opt_funcs.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.opt_funcs.rst similarity index 76% rename from docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.opt_funcs.rst rename to docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.opt_funcs.rst index a504a26d..66b5bb18 100644 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.opt_funcs.rst +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.opt_funcs.rst @@ -1,7 +1,7 @@ -pydss.pyPostprocessor.PostprocessScripts.DERMSOptimizer\_helper\_modules.opt\_funcs +pydss.py_postprocessor.postprocess_scripts.derms_optimizer\_helper\_modules.opt\_funcs =================================================================================== -.. automodule:: pydss.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.opt_funcs +.. automodule:: pydss.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.opt_funcs .. rubric:: Functions diff --git a/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.rst new file mode 100644 index 00000000..4d5b2fdc --- /dev/null +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.rst @@ -0,0 +1,13 @@ +pydss.py_postprocessor.postprocess_scripts.derms_optimizer\_helper\_modules +======================================================================== + +.. automodule:: pydss.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + opt_funcs diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.EdLiFoControl.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.ed_li_fo_control.rst similarity index 71% rename from docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.EdLiFoControl.rst rename to docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.ed_li_fo_control.rst index 520054e0..724c805e 100644 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.PostprocessScripts.EdLiFoControl.rst +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.ed_li_fo_control.rst @@ -1,7 +1,7 @@ -pydss.pyPostprocessor.PostprocessScripts.EdLiFoControl +pydss.py_postprocessor.postprocess_scripts.ed_li_fo_control ====================================================== -.. automodule:: pydss.pyPostprocessor.PostprocessScripts.EdLiFoControl +.. automodule:: pydss.py_postprocessor.postprocess_scripts.ed_li_fo_control .. rubric:: Functions @@ -22,5 +22,5 @@ pydss.pyPostprocessor.PostprocessScripts.EdLiFoControl .. autosummary:: - EdLiFoControl + ed_li_fo_control \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.rst new file mode 100644 index 00000000..5c6b6084 --- /dev/null +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.postprocess_scripts.rst @@ -0,0 +1,15 @@ +pydss.py_postprocessor.postprocess_scripts +======================================== + +.. automodule:: pydss.py_postprocessor.postprocess_scripts + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + derms_optimizer + derms_optimizer_helper_modules + ed_li_fo_control diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.pyPostprocess.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.py_postprocess.rst similarity index 54% rename from docs/source/_autosummary/PyDSS.pyPostprocessor.pyPostprocess.rst rename to docs/source/_autosummary/PyDSS.py_postprocessor.py_postprocess.rst index bb3e88a3..46bea44a 100644 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.pyPostprocess.rst +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.py_postprocess.rst @@ -1,7 +1,7 @@ -pydss.pyPostprocessor.pyPostprocess +pydss.py_postprocessor.py_postprocess =================================== -.. automodule:: pydss.pyPostprocessor.pyPostprocess +.. automodule:: pydss.py_postprocessor.py_postprocess .. rubric:: Functions diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.pyPostprocessAbstract.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.py_postprocess_abstract.rst similarity index 53% rename from docs/source/_autosummary/PyDSS.pyPostprocessor.pyPostprocessAbstract.rst rename to docs/source/_autosummary/PyDSS.py_postprocessor.py_postprocess_abstract.rst index 0a44a83d..668a5cdd 100644 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.pyPostprocessAbstract.rst +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.py_postprocess_abstract.rst @@ -1,7 +1,7 @@ -pydss.pyPostprocessor.pyPostprocessAbstract +pydss.py_postprocessor.py_postprocess_abstract =========================================== -.. automodule:: pydss.pyPostprocessor.pyPostprocessAbstract +.. automodule:: pydss.py_postprocessor.py_postprocess_abstract .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.pyPostprocessor.rst b/docs/source/_autosummary/PyDSS.py_postprocessor.rst similarity index 52% rename from docs/source/_autosummary/PyDSS.pyPostprocessor.rst rename to docs/source/_autosummary/PyDSS.py_postprocessor.rst index 04afc2e8..099508b1 100644 --- a/docs/source/_autosummary/PyDSS.pyPostprocessor.rst +++ b/docs/source/_autosummary/PyDSS.py_postprocessor.rst @@ -1,7 +1,7 @@ -pydss.pyPostprocessor +pydss.py_postprocessor ===================== -.. automodule:: pydss.pyPostprocessor +.. automodule:: pydss.py_postprocessor .. rubric:: Modules @@ -11,5 +11,5 @@ pydss.pyPostprocessor :recursive: PostprocessScripts - pyPostprocess - pyPostprocessAbstract + py_postprocess + py_postprocess_abstract diff --git a/docs/source/_autosummary/PyDSS.ResultData.rst b/docs/source/_autosummary/PyDSS.result_data.rst similarity index 63% rename from docs/source/_autosummary/PyDSS.ResultData.rst rename to docs/source/_autosummary/PyDSS.result_data.rst index 39ec2874..caa9f831 100644 --- a/docs/source/_autosummary/PyDSS.ResultData.rst +++ b/docs/source/_autosummary/PyDSS.result_data.rst @@ -1,7 +1,7 @@ -pydss.ResultData +pydss.result_data ================ -.. automodule:: pydss.ResultData +.. automodule:: pydss.result_data .. rubric:: Classes diff --git a/docs/source/_autosummary/PyDSS.rst b/docs/source/_autosummary/PyDSS.rst index 5fca4ee8..aa2b8740 100644 --- a/docs/source/_autosummary/PyDSS.rst +++ b/docs/source/_autosummary/PyDSS.rst @@ -10,25 +10,24 @@ :toctree: :recursive: - Extensions - ProfileManager - ResultData - SolveMode + extensions + profile_manager + result_data + solve_mode api apps cli common config_data - controllers dataset_buffer defaults - dssBus - dssCircuit - dssElement - dssElementFactory + DssBus + DssCircuit + DssElement + DssElementFactory dssInstance - dssObjectBase - dssTransformer + DssObjectBase + DssTransformer element_fields element_options exceptions @@ -39,11 +38,13 @@ modes naerm node_voltage_metrics - pyContrReader - pyControllers + openmdao_components + openmdao_model + PyContrReader + py_controllers pyDSS - pyPostprocessor - pyResults + py_postprocessor + py_results pydss_fs_interface pydss_project pydss_results @@ -52,6 +53,6 @@ simulation_input_models storage_filters thermal_metrics - unitDefinations + unit_definitions utils value_storage diff --git a/docs/source/_autosummary/PyDSS.SolveMode.rst b/docs/source/_autosummary/PyDSS.solve_mode.rst similarity index 62% rename from docs/source/_autosummary/PyDSS.SolveMode.rst rename to docs/source/_autosummary/PyDSS.solve_mode.rst index ce3ba586..8915df54 100644 --- a/docs/source/_autosummary/PyDSS.SolveMode.rst +++ b/docs/source/_autosummary/PyDSS.solve_mode.rst @@ -1,13 +1,13 @@ -pydss.SolveMode +pydss.solve_mode =============== -.. automodule:: pydss.SolveMode +.. automodule:: pydss.solve_mode .. rubric:: Functions .. autosummary:: - GetSolver + get_solver get_solver_from_simulation_type \ No newline at end of file diff --git a/docs/source/_autosummary/PyDSS.unitDefinations.rst b/docs/source/_autosummary/PyDSS.unitDefinations.rst index 0f0c8918..b5786ffb 100644 --- a/docs/source/_autosummary/PyDSS.unitDefinations.rst +++ b/docs/source/_autosummary/PyDSS.unitDefinations.rst @@ -1,6 +1,6 @@ -pydss.unitDefinations +pydss.unit_definitions ===================== -.. automodule:: pydss.unitDefinations +.. automodule:: pydss.unit_definitions \ No newline at end of file diff --git a/docs/source/_autosummary/pydss.openmdao_components.rst b/docs/source/_autosummary/pydss.openmdao_components.rst new file mode 100644 index 00000000..effc788e --- /dev/null +++ b/docs/source/_autosummary/pydss.openmdao_components.rst @@ -0,0 +1,15 @@ +pydss.openmdao\_components +========================== + +.. automodule:: pydss.openmdao_components + + + .. rubric:: Classes + + .. autosummary:: + + CircuitAdapter + CircuitExplicitComponent + ControllerExplicitComponent + OpenDSSCircuitAdapter + VariableSpec diff --git a/docs/source/_autosummary/pydss.openmdao_model.rst b/docs/source/_autosummary/pydss.openmdao_model.rst new file mode 100644 index 00000000..76a3b4c6 --- /dev/null +++ b/docs/source/_autosummary/pydss.openmdao_model.rst @@ -0,0 +1,18 @@ +pydss.openmdao\_model +===================== + +.. automodule:: pydss.openmdao_model + + + .. rubric:: Functions + + .. autosummary:: + + build_openmdao_problem + controller_circuit_variable + + .. rubric:: Classes + + .. autosummary:: + + PyDSSOpenMDAOGroup diff --git a/docs/source/_autosummary/pydss.pyControllers.Controllers.DynamicVoltageSupport.rst b/docs/source/_autosummary/pydss.py_controllers.controllers.DynamicVoltageSupport.rst similarity index 51% rename from docs/source/_autosummary/pydss.pyControllers.Controllers.DynamicVoltageSupport.rst rename to docs/source/_autosummary/pydss.py_controllers.controllers.DynamicVoltageSupport.rst index a6d531f7..ed86b807 100644 --- a/docs/source/_autosummary/pydss.pyControllers.Controllers.DynamicVoltageSupport.rst +++ b/docs/source/_autosummary/pydss.py_controllers.controllers.DynamicVoltageSupport.rst @@ -1,7 +1,7 @@ -pydss.pyControllers.Controllers.DynamicVoltageSupport +pydss.py_controllers.controllers.dynamic_voltage_support ===================================================== -.. automodule:: pydss.pyControllers.Controllers.DynamicVoltageSupport +.. automodule:: pydss.py_controllers.controllers.dynamic_voltage_support .. rubric:: Classes diff --git a/docs/source/conf.py b/docs/source/conf.py index 7eab0126..5e73c278 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,24 +18,25 @@ # -- Project information ----------------------------------------------------- + def read(*names, **kwargs): with io.open( - os.path.join(os.path.dirname(__file__), *names), - encoding=kwargs.get("encoding", "utf8") + os.path.join(os.path.dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8") ) as fp: return fp.read() + def find_version(*file_paths): version_file = read(*file_paths) - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") -project = 'PyDSS' -copyright = '2018-2026, Aadil Latif' -author = 'Aadil Latif' + +project = "PyDSS" +copyright = "2018-2026, Aadil Latif" +author = "Aadil Latif" # The full version, including alpha/beta/rc tags release = find_version("../../src/pydss", "__init__.py") @@ -46,16 +47,31 @@ def find_version(*file_paths): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.napoleon', 'sphinx.ext.ifconfig', "sphinx_click", "sphinxcontrib.openapi", 'sphinxcontrib.redoc', - 'sphinx.ext.autosectionlabel', 'sphinx.ext.githubpages', 'sphinx.ext.todo', "sphinxcontrib.autodoc_pydantic", - 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinx.ext.extlinks', - 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.doctest', - 'sphinx.ext.inheritance_diagram', 'sphinx.ext.imgmath', "sphinx_enum_extend", - 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'enum_tools.autoenum', + "sphinx.ext.napoleon", + "sphinx.ext.ifconfig", + "sphinx_click", + "sphinxcontrib.openapi", + "sphinxcontrib.redoc", + "sphinx.ext.autosectionlabel", + "sphinx.ext.githubpages", + "sphinx.ext.todo", + "sphinxcontrib.autodoc_pydantic", + "sphinx.ext.todo", + "sphinx.ext.autosummary", + "sphinx.ext.extlinks", + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.imgmath", + "sphinx_enum_extend", + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "enum_tools.autoenum", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -68,8 +84,8 @@ def find_version(*file_paths): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -#html_theme = 'classic' -html_theme = 'furo' +# html_theme = 'classic' +html_theme = "furo" # html_theme_options = { # 'collapse_navigation': False, @@ -80,20 +96,20 @@ def find_version(*file_paths): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] autosectionlabel_prefix_document = True redoc = [ { - 'name': 'Pydss API', - 'page': 'api', - 'spec': 'spec/swagger.yml', - 'embed': True, + "name": "Pydss API", + "page": "api", + "spec": "spec/swagger.yml", + "embed": True, }, ] autosummary_generate = True autodoc_pydantic_model_show_json = True autodoc_pydantic_settings_show_json = True -autodoc_pydantic_model_erdantic_figure_collapsed = True \ No newline at end of file +autodoc_pydantic_model_erdantic_figure_collapsed = True diff --git a/docs/source/controllers.rst b/docs/source/controllers.rst index a0196ad3..77c84038 100644 --- a/docs/source/controllers.rst +++ b/docs/source/controllers.rst @@ -2,8 +2,17 @@ Controllers ########### -PyDSS includes 13 built-in controllers for common distribution system elements. -Each controller is configured via TOML files in a scenario's ``pyControllerList/`` directory. +PyDSS includes 14 built-in OpenMDAO controller components for common distribution system +elements. Each controller is configured via TOML files in a scenario's +``pyControllerList/`` directory and connected to the OpenDSS circuit through declared +measurements and commands. + +Controller calculations are evaluated by OpenMDAO's nonlinear solver. A controller does not call +OpenDSS directly, advance simulation time, or run its own iteration loop. State is committed only +after the coupled circuit/controller solve converges. + +Configuration is component-specific. Use the settings documented on each controller page; control +behavior is no longer assembled from numbered control slots. For detailed documentation on each controller, see :doc:`controllers_overview`. diff --git a/docs/source/controllers/FaultController.rst b/docs/source/controllers/FaultController.rst index 9bb6ac54..c18e0dad 100644 --- a/docs/source/controllers/FaultController.rst +++ b/docs/source/controllers/FaultController.rst @@ -15,7 +15,7 @@ start time and cleared after a specified duration. Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.FaultController.FaultController +.. py:class:: pydss.py_controllers.controllers.fault_controller.FaultController The controller is configured with the following TOML settings: diff --git a/docs/source/controllers/GenController.rst b/docs/source/controllers/GenController.rst index d381900a..c2258664 100644 --- a/docs/source/controllers/GenController.rst +++ b/docs/source/controllers/GenController.rst @@ -15,9 +15,10 @@ generators with grid-support functions. Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.GenController.GenController +.. py:class:: pydss.py_controllers.controllers.gen_controller.GenController -The controller is configured with the following TOML settings: +The controller reads component-specific settings from its TOML section. The primary settings are +listed below using the current configuration names: .. list-table:: GenController Settings :header-rows: 1 @@ -26,39 +27,24 @@ The controller is configured with the following TOML settings: * - Parameter - Type - Description - * - ``Control1`` - - string - - First control mode (``"None"`` or ``"VVar"``) - * - ``Control2`` - - string - - Second control mode - * - ``Control3`` - - string - - Third control mode - * - ``Priority`` - - string - - Control priority (``"Var"`` or ``"Watt"``) - * - ``DampCoef`` + * - ``damp_coef`` - float - Damping coefficient for heavy-ball algorithm - * - ``PFlim`` + * - ``q_limit`` - float - - Power factor limit - * - ``uMin`` + - Reactive power limit + * - ``u_min`` - float - Minimum voltage threshold for Volt-Var curve (p.u.) - * - ``uMax`` + * - ``u_max`` - float - Maximum voltage threshold for Volt-Var curve (p.u.) - * - ``uDbMin`` + * - ``u_db_min`` - float - Lower deadband voltage (p.u.) - * - ``uDbMax`` + * - ``u_db_max`` - float - Upper deadband voltage (p.u.) - * - ``Model as PVsystem`` - - bool - - If true, models the generator with PVSystem-like behavior Usage Example ------------- @@ -66,14 +52,9 @@ Usage Example .. code-block:: toml ["Generator.gen1"] - Control1 = "VVar" - Control2 = "None" - Control3 = "None" - Priority = "Var" - DampCoef = 0.8 - PFlim = 0.9 - uMin = 0.92 - uMax = 1.08 - uDbMin = 0.98 - uDbMax = 1.02 - Model as PVsystem = false + damp_coef = 0.8 + q_limit = 1.0 + u_min = 0.92 + u_max = 1.08 + u_db_min = 0.98 + u_db_max = 1.02 diff --git a/docs/source/controllers/MotorStall.rst b/docs/source/controllers/MotorStall.rst index 94cba987..85464ce3 100644 --- a/docs/source/controllers/MotorStall.rst +++ b/docs/source/controllers/MotorStall.rst @@ -11,8 +11,8 @@ Motor Stall Model Controller overview ------------------- -This controller implments the motor stall model for single phase motors (residential air conditioners) and models two states of operations -i.e. run state and stall state. As supply voltage is +This controller implements the motor-stall model for single-phase motors, such as residential air +conditioners, and models two operating states: run and stall. As supply voltage is decreased, the motor slowly begins to consume a higher amount of active and reactive power. At some voltage level (“stall voltage”), the supply voltage is no longer adequate to maintain stable motor operation and the motor “stalls” (stops). There is insufficient motor torque to overcome the load torque and therefore the motor stalls. @@ -29,7 +29,7 @@ This is because once the motor stops (stalls), its electrical behavior is essent Controller model ---------------- -.. autopydantic_model:: pydss.pyControllers.models.MotorStallSettings +.. autopydantic_model:: pydss.py_controllers.models.MotorStallSettings diff --git a/docs/source/controllers/MotorStallSimple.rst b/docs/source/controllers/MotorStallSimple.rst index 5ce673be..827b04db 100644 --- a/docs/source/controllers/MotorStallSimple.rst +++ b/docs/source/controllers/MotorStallSimple.rst @@ -27,7 +27,7 @@ when compared to the complete motor stall model Controller model ---------------- -.. autopydantic_model:: pydss.pyControllers.models.MotorStallSimpleSettings +.. autopydantic_model:: pydss.py_controllers.models.MotorStallSimpleSettings Usage example ------------- \ No newline at end of file diff --git a/docs/source/controllers/PvController.rst b/docs/source/controllers/PvController.rst index 07492e09..2a235ac3 100644 --- a/docs/source/controllers/PvController.rst +++ b/docs/source/controllers/PvController.rst @@ -5,13 +5,13 @@ PV System Smart Controller Controller overview ------------------- -This controller implments three reactive power compensation techniques namely, +This controller implements three reactive power compensation techniques: - constant power factor mode - variable power factor mode - voltage droop mode (a.k.a. volt / var control) -Additionally it implements the volt / watt control and finally implents the trip logic for legacy invereters. +It also implements Volt/Watt control and inverter trip logic. In constant power factor mode, inverter reactive power follows its active power output, thus ensuring power factor remains constant. Constant power factor control mode is cost effective as no @@ -59,19 +59,19 @@ it is possible to increase the installed PV capacity and energy yield while prev Controller model ---------------- -.. autopydantic_model:: pydss.pyControllers.models.PvControllerModel +.. autopydantic_model:: pydss.py_controllers.models.PvControllerModel Controller options ------------------ -.. autoenum:: pydss.pyControllers.enumerations.SmartControls +.. autoenum:: pydss.py_controllers.enumerations.SmartControls -.. autoenum:: pydss.pyControllers.enumerations.ControlPriority +.. autoenum:: pydss.py_controllers.enumerations.ControlPriority -.. autoenum:: pydss.pyControllers.enumerations.VoltWattCurtailmentStrategy +.. autoenum:: pydss.py_controllers.enumerations.VoltWattCurtailmentStrategy -.. autoenum:: pydss.pyControllers.enumerations.VoltageCalcModes +.. autoenum:: pydss.py_controllers.enumerations.VoltageCalcModes Usage example diff --git a/docs/source/controllers/PvDynamic.rst b/docs/source/controllers/PvDynamic.rst index e15519e6..319eccff 100644 --- a/docs/source/controllers/PvDynamic.rst +++ b/docs/source/controllers/PvDynamic.rst @@ -22,7 +22,7 @@ This controller is intended for dynamic simulations (sub-second time steps) and Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.PvDynamic.PvDynamic +.. py:class:: pydss.py_controllers.controllers.pv_dynamic.PvDynamic The controller accepts an extensive set of parameters organized into categories: diff --git a/docs/source/controllers/PvFrequencyRideThru.rst b/docs/source/controllers/PvFrequencyRideThru.rst index 3da2410c..f85bba39 100644 --- a/docs/source/controllers/PvFrequencyRideThru.rst +++ b/docs/source/controllers/PvFrequencyRideThru.rst @@ -20,7 +20,7 @@ frequency thresholds per the IEEE 1547-2018 standard. Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.PvFrequencyRideThru.PvFrequencyRideThru +.. py:class:: pydss.py_controllers.controllers.pv_frequency_ride_thru.PvFrequencyRideThru .. list-table:: PvFrequencyRideThru Settings :header-rows: 1 @@ -62,12 +62,6 @@ Controller Model * - ``Reconnect Pmax time - sec`` - float - Time to ramp back to maximum power after reconnection (seconds) - * - ``Priority`` - - string - - Control priority (``"Var"`` or ``"Watt"``) - * - ``UcalcMode`` - - string - - Voltage calculation mode (``"MAX"``, ``"AVG"``, ``"MIN"``, ``"A"``, ``"B"``, ``"C"``) Usage Example ------------- @@ -89,5 +83,3 @@ Usage Example Ride-through Category = "Category III" Reconnect deadtime - sec = 300 Reconnect Pmax time - sec = 300 - Priority = "Var" - UcalcMode = "AVG" diff --git a/docs/source/controllers/PvVoltageRideThru.rst b/docs/source/controllers/PvVoltageRideThru.rst index 4d89ff4f..d068bce6 100644 --- a/docs/source/controllers/PvVoltageRideThru.rst +++ b/docs/source/controllers/PvVoltageRideThru.rst @@ -9,8 +9,8 @@ This controller implements the voltage ride-through requirements from both IEEE The controller implements ride-through requirements in complete detail. This includes -- Implementation of inverter behaviour in defined regions such as may trip, current limited, permissive etc. -- Implementation of inverter categories for the IEEE 1547-2018 standard. +- Implementation of inverter behavior in defined regions such as may-trip, current-limited, and permissive operation. +- Implementation of inverter categories for the IEEE 1547-2018 standard. - System behavior under multiple disturbances. - System recovery under momentary cessation and trip scenarios. @@ -18,53 +18,54 @@ The controller implements ride-through requirements in complete detail. This inc :width: 400 :alt: Alternative text -The figure above shows inverter response to a fault. -The inverter is configured to provide 'momentary cessation' operation in the 'may trip' region. The inverter enter momentary -cessation region (grey) and the PV is temporarily disconnected. Once the fault is cleared, the PV system goes back to full power -in 0.4s in a ramped manner as per the per IEEE 1547-2018 standard +The figure above shows the inverter response to a fault. The inverter is configured for momentary +cessation in the may-trip region. It enters the momentary-cessation region (gray), and the PV +system is temporarily disconnected. Once the fault is cleared, the PV system returns to full power +in a 0.4-second ramp, consistent with IEEE 1547-2018. .. image:: PvVoltageRideThru2.png :width: 400 :alt: Alternative text -The figure above shows inverter response to multiple faults fault. The inverter is modeled to trip on multiple faults. -As per the standard the inverter should trip on the second consecutive fault. After the first fault, PV system enter momentary -cessation region and starts to recover. On the second fault, the PV system trips and is required to wait at least 300s before it can reconnect +The figure above shows the inverter response to multiple faults. The inverter is configured to trip +after multiple faults. Under the modeled standard, it trips on the second consecutive fault. After +the first fault, the PV system enters momentary cessation and begins to recover. After the second +fault, it trips and must wait at least 300 seconds before reconnecting. .. image:: PvVoltageRideThru1.png :width: 400 :alt: Alternative text -The figure above shows inverter response to multiple faults fault. The inverter is modeled to NOT trip on multiple faults. -With each fault, PV system enter momentary cessation region and starts to recover in a ramped manner. -It finally recovers after the third fault +The figure above shows the inverter response to multiple faults when tripping is disabled. With +each fault, the PV system enters momentary cessation and begins a ramped recovery. It recovers +fully after the third fault. Controller model ---------------- -.. autopydantic_model:: pydss.pyControllers.models.PvVoltageRideThruModel +.. autopydantic_model:: pydss.py_controllers.models.PvVoltageRideThruModel Controller options ------------------ -.. autoenum:: pydss.pyControllers.enumerations.PvStandard +.. autoenum:: pydss.py_controllers.enumerations.PvStandard -.. autoenum:: pydss.pyControllers.enumerations.VoltageCalcModes +.. autoenum:: pydss.py_controllers.enumerations.VoltageCalcModes -.. autoenum:: pydss.pyControllers.enumerations.RideThroughCategory +.. autoenum:: pydss.py_controllers.enumerations.RideThroughCategory -.. autoenum:: pydss.pyControllers.enumerations.PermissiveOperation +.. autoenum:: pydss.py_controllers.enumerations.PermissiveOperation -.. autoenum:: pydss.pyControllers.enumerations.MayTripOperation +.. autoenum:: pydss.py_controllers.enumerations.MayTripOperation -.. autoenum:: pydss.pyControllers.enumerations.MultipleDisturbances +.. autoenum:: pydss.py_controllers.enumerations.MultipleDisturbances -.. autoenum:: pydss.pyControllers.enumerations.CategoryI +.. autoenum:: pydss.py_controllers.enumerations.CategoryI -.. autoenum:: pydss.pyControllers.enumerations.CategoryII +.. autoenum:: pydss.py_controllers.enumerations.CategoryII -.. autoenum:: pydss.pyControllers.enumerations.CategoryIII +.. autoenum:: pydss.py_controllers.enumerations.CategoryIII Usage example ------------- diff --git a/docs/source/controllers/SocketController.rst b/docs/source/controllers/SocketController.rst index b8cf8562..c0db9db3 100644 --- a/docs/source/controllers/SocketController.rst +++ b/docs/source/controllers/SocketController.rst @@ -22,7 +22,7 @@ For detailed co-simulation usage, see :doc:`../Co-simulation Interfaces`. Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.SocketController.SocketController +.. py:class:: pydss.py_controllers.controllers.socket_controller.SocketController .. list-table:: SocketController Settings :header-rows: 1 diff --git a/docs/source/controllers/StorageController.rst b/docs/source/controllers/StorageController.rst index f787ff7b..e18b65e0 100644 --- a/docs/source/controllers/StorageController.rst +++ b/docs/source/controllers/StorageController.rst @@ -5,8 +5,8 @@ Storage Controller Controller Overview ------------------- The Storage Controller provides multiple control implementations for battery energy storage -systems, supporting both behind-the-meter and front-of-meter applications. It supports up to -three cascading control modes that can be combined for complex dispatch strategies. +systems, supporting both behind-the-meter and front-of-meter applications. Dispatch behavior is +selected through component-specific settings. Available control modes: @@ -26,74 +26,29 @@ Available control modes: Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.StorageController.StorageController +.. py:class:: pydss.py_controllers.controllers.storage_controller.StorageController -.. list-table:: StorageController Settings - :header-rows: 1 - :widths: 30 15 55 - - * - Parameter - - Type - - Description - * - ``Control1`` - - string - - Primary control mode (see modes listed above) - * - ``Control2`` - - string - - Secondary control mode - * - ``Control3`` - - string - - Tertiary control mode - * - ``alpha`` - - float - - Smoothing coefficient for control response - * - ``beta`` - - float - - Damping coefficient for control response - * - ``DampCoef`` - - float - - General damping coefficient - * - ``touLoadLim`` - - float - - Load limit for Time of Use control (kW) - * - ``%touCharge`` - - float - - Charge rate during TOU off-peak periods (%) - * - ``touTarrifStructure`` - - list - - TOU tariff schedule definition - * - ``PowerMeaElem`` - - string - - Element used for power measurement in TOU mode - * - ``DemandChgThreh[kWh]`` - - float - - Demand charge threshold (kWh) +The component accepts ``control`` or ``mode`` for dispatch selection. Other settings depend on the +selected mode. For example, scheduled dispatch uses ``schedule`` and ``schedule_period_sec``; +peak shaving uses ``ps_ub`` and ``damp_coef``. Usage Example ------------- -**Time of Use control:** +**Scheduled dispatch:** .. code-block:: toml ["Storage.batt1"] - Control1 = "TOU" - Control2 = "None" - Control3 = "None" - alpha = 0.1 - beta = 0.1 - DampCoef = 0.5 - touLoadLim = 50 - %touCharge = 100 + control = "scheduled" + schedule = [0.0, 25.0, 50.0] + schedule_period_sec = 3600.0 **Peak Shaving control:** .. code-block:: toml ["Storage.batt1"] - Control1 = "PS" - Control2 = "None" - Control3 = "None" - alpha = 0.1 - beta = 0.1 - DampCoef = 0.8 + control = "peak_shaving" + ps_ub = 50.0 + damp_coef = 0.8 diff --git a/docs/source/controllers/ThermostaticLoad.rst b/docs/source/controllers/ThermostaticLoad.rst index 19035d5f..54bd92b7 100644 --- a/docs/source/controllers/ThermostaticLoad.rst +++ b/docs/source/controllers/ThermostaticLoad.rst @@ -30,7 +30,7 @@ When the temperature reaches ``Tmax``, the load turns on (cooling). When it drop Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.ThermostaticLoad.ThermostaticLoad +.. py:class:: pydss.py_controllers.controllers.thermostatic_load.ThermostaticLoad .. list-table:: ThermostaticLoad Settings :header-rows: 1 diff --git a/docs/source/controllers/xfmrController.rst b/docs/source/controllers/xfmrController.rst index 8e3ecae2..de3d8525 100644 --- a/docs/source/controllers/xfmrController.rst +++ b/docs/source/controllers/xfmrController.rst @@ -16,7 +16,7 @@ the controller re-enables the RegControl to allow normal voltage regulation. Controller Model ---------------- -.. py:class:: pydss.pyControllers.Controllers.xfmrController.xfmrController +.. py:class:: pydss.py_controllers.controllers.xfmr_controller.XfmrController .. list-table:: xfmrController Settings :header-rows: 1 diff --git a/docs/source/index.rst b/docs/source/index.rst index aaa92a9d..bfeafc9d 100755 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,8 +15,9 @@ organizational, analytical, and co-simulation capabilities. It is built on top o Key Features ============ -- **Custom Control Algorithms** — Define Python-based controllers for any circuit element, executed - at each simulation time step. 13 built-in controllers are included. +- **OpenMDAO controller convergence** — Python controllers and the OpenDSS circuit are coupled in + an OpenMDAO nonlinear model and solved together at each simulation time step. 14 built-in + controller components are included. - **HELICS Co-simulation** — Integrate with external simulators via the `HELICS `_ framework for cyber-physical co-simulation studies. - **Scenario Management** — Run multiple scenarios on a shared OpenDSS model with independent @@ -64,7 +65,7 @@ Verify the installation: .. note:: - PyDSS requires Python 3.9 or later. Python 3.11 is recommended. + PyDSS requires Python 3.10 or later. Python 3.11 is recommended. ************* @@ -91,6 +92,7 @@ Additional Documentation interfaces co-simulation_support controllers_overview + openmdao_convergence reports hdf-data-format diff --git a/docs/source/openmdao_convergence.rst b/docs/source/openmdao_convergence.rst new file mode 100644 index 00000000..c6737274 --- /dev/null +++ b/docs/source/openmdao_convergence.rst @@ -0,0 +1,75 @@ +************************* +OpenMDAO Controller Solve +************************* + +PyDSS uses OpenMDAO to solve the feedback loop between Python controllers and the OpenDSS +circuit. This is the controller execution model for every simulation time point that has configured +controllers. + +Architecture +============ + +The simulation driver owns time advancement and result export. OpenMDAO owns the nonlinear solve: + +.. code-block:: text + + PyDSS outer loop + | apply profiles, external inputs, and the current time + v + OpenMDAO Problem + |-- ControllerExplicitComponent -> controller commands + |-- CircuitExplicitComponent -> OpenDSS solve and measurements + `-- NonlinearBlockGS -> coupled convergence + v + commit accepted controller state, export results, advance OpenDSS time + +During a nonlinear iteration, a controller reads only its declared OpenMDAO inputs and writes only +its declared outputs. The circuit component applies the command batch, solves OpenDSS once, and +returns the measurements needed by the controllers. The solver repeats this cycle until the +residual is within tolerance or the iteration limit is reached. + +Configuration +============= + +The project settings below configure the OpenMDAO solve: + +``Max Control Iterations`` + Maximum number of nonlinear solver iterations permitted at one simulation time point. + +``Error tolerance`` + Absolute and relative nonlinear solver tolerance passed to OpenMDAO. + +``OpenMDAO Reports`` + Enables OpenMDAO reports and nonlinear solver recording for a direct example or project run. + Reports are written under the project ``OpenMDAOReports/`` directory. + +``Max error tolerance`` and ``Convergence error percent threshold`` + Simulation-level failure policies for convergence errors. These remain outside the OpenMDAO + model and determine whether repeated failed time points stop the simulation. + +A controller configuration uses the settings supported by its component. See the individual +controller pages for the available settings and examples. + +Time-step lifecycle +=================== + +Each outer time point follows this order: + +#. PyDSS applies profiles, HELICS values, or other external updates. +#. PyDSS supplies the current simulation time to the circuit component. +#. OpenMDAO resets transient component state and runs the coupled nonlinear model. +#. A converged solve commits controller and circuit state exactly once. +#. PyDSS exports results and advances OpenDSS time. + +Trial evaluations during nonlinear convergence must not advance time or commit state. If OpenMDAO +exhausts its iteration limit, the time point is marked unconverged and the configured PyDSS +convergence policy is applied. OpenDSS backend failures are reported separately from OpenMDAO +nonlinear failures. + +Reports and failure handling +============================ + +Set ``OpenMDAO Reports`` to ``true`` when OpenMDAO solver reports are needed for a run. Reports are +written under the project's ``OpenMDAOReports/`` directory. A failed nonlinear solve is reported +to PyDSS and handled according to the project's convergence settings; controller state is not +committed as an accepted time step. diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 764201bb..bd514b1e 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -40,6 +40,8 @@ project. Key settings include: - **Simulation type**: QSTS (quasi-static time series), Dynamic, Snapshot, or Monte Carlo - **Start time and duration**: When the simulation starts and how long it runs - **Step resolution**: Time step size (in seconds) +- **Controller convergence**: ``Max Control Iterations`` and ``Error tolerance`` configure the + OpenMDAO nonlinear solve used to couple controllers with OpenDSS. - **Export options**: What data to collect and in what format (HDF5 or CSV) Refer to :ref:`SimulationSettingsModel` for the full list of settings. @@ -49,6 +51,10 @@ Each scenario has its own config files for additional customization: - ``ExportLists/`` — What data to export - ``pyControllerList/`` — Controller configurations +Python controllers are OpenMDAO components. The OpenMDAO solver evaluates controller commands and +the OpenDSS circuit together until the configured tolerance is reached. Use the component-specific +settings documented on each controller page. + Run the simulation ================== diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index e685baa8..1610263f 100755 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -123,7 +123,7 @@ PyDSS includes a Jupyter notebook UI that allows you to plot element results. Note that you must be in the directory where you ran ``pydss run `` when you start the notebook. -.. note:: plotly is required for this application. You may ned to ``pip install plotly``. +.. note:: Plotly is required for this application. Install it with ``pip install plotly`` if needed. .. code-block:: bash diff --git a/examples/custom_contols/ProfileManager.toml b/examples/custom_contols/ProfileManager.toml index cc1dced3..1da59a74 100644 --- a/examples/custom_contols/ProfileManager.toml +++ b/examples/custom_contols/ProfileManager.toml @@ -9,7 +9,6 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false "Simulation Type" = "QSTS" "Project Path" = "C:\\Users\\alatif\\Desktop\\pydss_2.0\\pydss_src\\examples" "Active Project" = "custom_contols" diff --git a/examples/custom_contols/simulation.toml b/examples/custom_contols/simulation.toml index 82454a8f..96cad011 100644 --- a/examples/custom_contols/simulation.toml +++ b/examples/custom_contols/simulation.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples" "Active Project" = "custom_contols" diff --git a/examples/custom_contols/vvar.toml b/examples/custom_contols/vvar.toml index a8c7fd8b..fe1e6a07 100644 --- a/examples/custom_contols/vvar.toml +++ b/examples/custom_contols/vvar.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples" "Active Project" = "custom_contols" @@ -57,7 +57,3 @@ Uninterruptible = true [MonteCarlo] "Number of Monte Carlo scenarios" = -1 - -[Plots] -"Create dynamic plots" = false -"Open plots in browser" = true diff --git a/examples/dynamics/Exports/fault/CapacitorsInfo.csv b/examples/dynamics/Exports/fault/CapacitorsInfo.csv index 90c1b7ff..9b3620a0 100644 --- a/examples/dynamics/Exports/fault/CapacitorsInfo.csv +++ b/examples/dynamics/Exports/fault/CapacitorsInfo.csv @@ -1,4 +1,4 @@ ,bus1,bus2,phases,kvar,kv,conn,cmatrix,cuf,R,XL,Harm,Numsteps,states,normamps,emergamps,faultrate,pctperm,repair,basefreq,enabled,like -Capacitor.c1,b1,b1.0.0.0,3,['600'],12.47,wye,"('0 |2.49009085503988E-315 2.22396058465097E-308 |5.2150182983179E-304 0 2.56377452879502E-308',)",['10.235'],['0'],['0'],['0'],1,['1'],37.5023029706172,50.0030706274896,0.0005,100,3,60,Yes, -Capacitor.c2,b3,b3.0.0.0,3,['600'],12.47,wye,"('0 |2.49009085503988E-315 2.22396058465097E-308 |5.21501829831079E-304 0 2.56377452879502E-308',)",['10.235'],['0'],['0'],['0'],1,['1'],37.5023029706172,50.0030706274896,0.0005,100,3,60,Yes, -Capacitor.cg,bg,bg.0.0.0,3,['600'],0.48,wye,"('0 |2.33198984837068E-315 2.22397401533149E-308 |5.21501758047056E-304 0 2.56377452879502E-308',)",['6907.77'],['0'],['0'],['0'],1,['1'],974.278579257494,1299.03810567666,0.0005,100,3,60,Yes, +Capacitor.c1,b1,b1.0.0.0,3,['600'],12.47,wye,"('0 |4.74303020007597E-316 0 |0 0 0',)",['10.235'],['0'],['0'],['0'],1,['1'],37.5023029706172,50.0030706274896,0.0005,100,3,60,Yes, +Capacitor.c2,b3,b3.0.0.0,3,['600'],12.47,wye,"('0 |3.16202013338398E-316 4.94065645841247E-318 |1.27319747462857E-307 3.16289184304687E-308 3.16289186676203E-308',)",['10.235'],['0'],['0'],['0'],1,['1'],37.5023029706172,50.0030706274896,0.0005,100,3,60,Yes, +Capacitor.cg,bg,bg.0.0.0,3,['600'],0.48,wye,"('0 |1.82804288961261E-316 6.44938472111788E-313 |9.88131291682493E-318 1.30764354484803E-313 0',)",['6907.77'],['0'],['0'],['0'],1,['1'],974.278579257494,1299.03810567666,0.0005,100,3,60,Yes, diff --git a/examples/dynamics/Exports/fault/FeederHeadInfo.json b/examples/dynamics/Exports/fault/FeederHeadInfo.json index 67d48415..96a3c364 100644 --- a/examples/dynamics/Exports/fault/FeederHeadInfo.json +++ b/examples/dynamics/Exports/fault/FeederHeadInfo.json @@ -1 +1 @@ -{"FeederHeadLine": "Line.l7", "FeederHeadLoading": 0.00018408558827956613, "FeederHeadLoadKW": -36.355359408365295, "FeederHeadLoadKVar": 2084.359623722454, "ReversePowerFlow": false} \ No newline at end of file +{"FeederHeadLine": "Line.l7", "FeederHeadLoading": 0.0001836025596483059, "FeederHeadLoadKW": -1241.753206963247, "FeederHeadLoadKVar": 1321.9142708275103, "ReversePowerFlow": false} \ No newline at end of file diff --git a/examples/dynamics/Exports/fault/RelaysInfo.csv b/examples/dynamics/Exports/fault/RelaysInfo.csv index 2f02193c..7c478ed9 100644 --- a/examples/dynamics/Exports/fault/RelaysInfo.csv +++ b/examples/dynamics/Exports/fault/RelaysInfo.csv @@ -1,4 +1,4 @@ ,MonitoredObj,MonitoredTerm,SwitchedObj,SwitchedTerm,type,Phasecurve,Groundcurve,PhaseTrip,GroundTrip,TDPhase,TDGround,PhaseInst,GroundInst,Reset,Shots,RecloseIntervals,Delay,Overvoltcurve,Undervoltcurve,kvbase,47%Pickup,46BaseAmps,46%Pickup,46isqt,Variable,overtrip,undertrip,Breakertime,action,Z1mag,Z1ang,Z0mag,Z0ang,Mphase,Mground,EventLog,DebugTrace,DistReverse,Normal,State,DOC_TiltAngleLow,DOC_TiltAngleHigh,DOC_TripSettingLow,DOC_TripSettingHigh,DOC_TripSettingMag,DOC_DelayInner,DOC_PhaseCurveInner,DOC_PhaseTripInner,DOC_TDPhaseInner,DOC_P1Blocking,basefreq,enabled,like -Relay.mfrov/uv,Line.motorleads,1,Line.motorleads,1,Voltage,,,1,1,1,1,0,0,15,1,['NONE'],0,,,0.48,2,100,20,1,,1.2,0.8,0,open,0.7,64,2.1,68,0.7,0.7,No,No,No,closed,open,90,90,0,-1,-1,-1,1,1,,Yes,60,Yes, -Relay.mfr46,Line.motorleads,1,Line.motorleads,1,46,,,1,1,1,1,0,0,15,4,['0.5 2 2'],0.1,,,0,2,1800,20,1,,1.2,0.8,0,open,0.7,64,2.1,68,0.7,0.7,No,No,No,closed,open,90,90,0,-1,-1,-1,1,1,,Yes,60,Yes, -Relay.mfr47,Line.motorleads,1,Line.motorleads,1,47,,,1,1,1,1,0,0,15,4,['0.5 2 2'],0.1,,,0.48,2,100,20,1,,1.2,0.8,0,open,0.7,64,2.1,68,0.7,0.7,No,No,No,closed,open,90,90,0,-1,-1,-1,1,1,,Yes,60,Yes, +Relay.mfrov/uv,Line.motorleads,1,Line.motorleads,1,Voltage,,,1,1,1,1,0,0,15,1,['NONE'],0,,,0.48,2,100,20,1,,1.2,0.8,0,close,0.7,64,2.1,68,0.7,0.7,No,No,No,closed,closed,90,90,0,-1,-1,-1,1,1,,Yes,60,Yes, +Relay.mfr46,Line.motorleads,1,Line.motorleads,1,46,,,1,1,1,1,0,0,15,4,['0.5 2 2'],0.1,,,0,2,1800,20,1,,1.2,0.8,0,close,0.7,64,2.1,68,0.7,0.7,No,No,No,closed,closed,90,90,0,-1,-1,-1,1,1,,Yes,60,Yes, +Relay.mfr47,Line.motorleads,1,Line.motorleads,1,47,,,1,1,1,1,0,0,15,4,['0.5 2 2'],0.1,,,0.48,2,100,20,1,,1.2,0.8,0,close,0.7,64,2.1,68,0.7,0.7,No,No,No,closed,closed,90,90,0,-1,-1,-1,1,1,,Yes,60,Yes, diff --git a/examples/dynamics/Exports/fault/TransformersInfo.csv b/examples/dynamics/Exports/fault/TransformersInfo.csv index 9082bb42..8667b3f6 100644 --- a/examples/dynamics/Exports/fault/TransformersInfo.csv +++ b/examples/dynamics/Exports/fault/TransformersInfo.csv @@ -1,6 +1,6 @@ ,phases,windings,wdg,bus,conn,kV,kVA,tap,%R,Rneut,Xneut,buses,conns,kVs,kVAs,taps,XHL,XHT,XLT,Xscarray,thermal,n,m,flrise,hsrise,%loadloss,%noloadloss,normhkVA,emerghkVA,sub,MaxTap,MinTap,NumTaps,subname,%imag,ppm_antifloat,%Rs,bank,XfmrCode,XRConst,X12,X13,X23,LeadLag,WdgCurrents,Core,RdcOhms,Seasons,Ratings,normamps,emergamps,faultrate,pctperm,repair,basefreq,enabled,like -Transformer.sub,3,2,2,b0,wye,12.47,20000,1,0.2,-1,0,"['bx', 'b0']","['delta', 'wye']","['115', '12.47']","['20000', '20000']","['1', '1']",7,35,30,['7'],2,0.8,0.8,65,15,0.4,0,22000,30000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,7,35,30,Lag,"5.770615, (58.889), 92.17593, (-121.11), 5.759515, (-61.013), 91.99863, (118.99), 5.773929, (179.12), 92.22886, (-0.87878), ",shell,0.00440585883333333,1,['1100'],110.449616714537,150.613113701641,0.007,0,0,60,Yes, -Transformer.tg,3,2,2,bg,wye,0.48,1500,1,0.2,-1,0,"['b4', 'bg']","['wye', 'wye']","['12.47', '0.48']","['1500', '1500']","['1', '1']",5,35,30,['5'],2,0.8,0.8,65,15,0.4,0,1650,2250,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,5,35,30,Lag,"31.27724, (58.454), 812.5588, (-121.55), 31.1601, (-61.367), 809.5156, (118.63), 31.29942, (178.87), 813.1349, (-1.1326), ",shell,0.00008704,1,['1100'],76.3935801253314,104.17306380727,0.007,0,0,60,Yes, -Transformer.reg1a,1,2,2,bm.1,wye,12.47,2000,1,0.2,-1,0,"['b2.1', 'bm.1']","['wye', 'wye']","['12.47', '12.47']","['2000', '2000']","['1', '1']",1,35,30,['1'],2,0.8,0.8,65,15,0.4,0,2200,3000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,1,35,30,Lag,"62.07116, (58.59), 62.07126, (-121.41), ",shell,0.132175765,1,['1100'],176.423416198877,240.577385725742,0.007,0,0,60,Yes, -Transformer.reg1b,1,2,2,bm.2,wye,12.47,2000,1,0.2,-1,0,"['b2.2', 'bm.2']","['wye', 'wye']","['12.47', '12.47']","['2000', '2000']","['1', '1']",1,35,30,['1'],2,0.8,0.8,65,15,0.4,0,2200,3000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,1,35,30,Lag,"61.88901, (-61.267), 61.88911, (118.73), ",shell,0.132175765,1,['1100'],176.423416198877,240.577385725742,0.007,0,0,60,Yes, -Transformer.reg1c,1,2,2,bm.3,wye,12.47,2000,1,0.2,-1,0,"['b2.3', 'bm.3']","['wye', 'wye']","['12.47', '12.47']","['2000', '2000']","['1', '1']",1,35,30,['1'],2,0.8,0.8,65,15,0.4,0,2200,3000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,1,35,30,Lag,"62.11333, (178.93), 62.11343, (-1.0743), ",shell,0.132175765,1,['1100'],176.423416198877,240.577385725742,0.007,0,0,60,Yes, +Transformer.sub,3,2,2,b0,wye,12.47,20000,1,0.2,-1,0,"['bx', 'b0']","['delta', 'wye']","['115', '12.47']","['20000', '20000']","['1', '1']",7,35,30,['7'],2,0.8,0.8,65,15,0.4,0,22000,30000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,7,35,30,Lag,"4.991874, (17.208), 79.73666, (-162.79), 5.064344, (-103.22), 80.89423, (76.778), 4.999311, (136.38), 79.85545, (-43.623), ",shell,0.00440585883333333,1,['1100'],110.449616714537,150.613113701641,0.007,0,0,60,Yes, +Transformer.tg,3,2,2,bg,wye,0.48,1500,1,0.2,-1,0,"['b4', 'bg']","['wye', 'wye']","['12.47', '0.48']","['1500', '1500']","['1', '1']",5,35,30,['5'],2,0.8,0.8,65,15,0.4,0,1650,2250,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,5,35,30,Lag,"52.25191, (-30.936), 1357.461, (149.06), 53.54234, (-150.53), 1390.985, (29.468), 53.19026, (88.135), 1381.839, (-91.865), ",shell,0.00008704,1,['1100'],76.3935801253314,104.17306380727,0.007,0,0,60,Yes, +Transformer.reg1a,1,2,2,bm.1,wye,12.47,2000,1,0.2,-1,0,"['b2.1', 'bm.1']","['wye', 'wye']","['12.47', '12.47']","['2000', '2000']","['1', '1']",1,35,30,['1'],2,0.8,0.8,65,15,0.4,0,2200,3000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,1,35,30,Lag,"60.87594, (-1.7766), 60.87599, (178.22), ",shell,0.132175765,1,['1100'],176.423416198877,240.577385725742,0.007,0,0,60,Yes, +Transformer.reg1b,1,2,2,bm.2,wye,12.47,2000,1,0.2,-1,0,"['b2.2', 'bm.2']","['wye', 'wye']","['12.47', '12.47']","['2000', '2000']","['1', '1']",1,35,30,['1'],2,0.8,0.8,65,15,0.4,0,2200,3000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,1,35,30,Lag,"62.14947, (-121.99), 62.14952, (58.007), ",shell,0.132175765,1,['1100'],176.423416198877,240.577385725742,0.007,0,0,60,Yes, +Transformer.reg1c,1,2,2,bm.3,wye,12.47,2000,1,0.2,-1,0,"['b2.3', 'bm.3']","['wye', 'wye']","['12.47', '12.47']","['2000', '2000']","['1', '1']",1,35,30,['1'],2,0.8,0.8,65,15,0.4,0,2200,3000,No,1.1,0.9,32,,0,1,"['0.2', '0.2']",,,No,1,35,30,Lag,"61.31873, (117.21), 61.31878, (-62.789), ",shell,0.132175765,1,['1100'],176.423416198877,240.577385725742,0.007,0,0,60,Yes, diff --git a/examples/dynamics/Exports/fault/event_log.csv b/examples/dynamics/Exports/fault/event_log.csv index 1ac7c1a0..91868087 100644 --- a/examples/dynamics/Exports/fault/event_log.csv +++ b/examples/dynamics/Exports/fault/event_log.csv @@ -1,5 +1,2 @@ Hour=0, Sec=0.3, ControlIter=1, Element=Fault.thefault, Action=**APPLIED** -Hour=0, Sec=0.49817, ControlIter=1, Element=Recloser.rec1, Action=OPENED, FAST -Hour=0, Sec=0.49817, ControlIter=1, Element= , Action=PHASE TARGET -Hour=0, Sec=0.49817, ControlIter=2, Element=Fault.thefault, Action=**CLEARED** -Hour=0, Sec=0.99817, ControlIter=1, Element=Recloser.rec1, Action=CLOSED +Hour=0, Sec=0.3, ControlIter=1, Element=Fault.thefault, Action=**CLEARED** diff --git a/examples/dynamics/simulation.toml b/examples/dynamics/simulation.toml index 908c9ab0..b32d69fc 100644 --- a/examples/dynamics/simulation.toml +++ b/examples/dynamics/simulation.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Time" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "Dynamic" "Project Path" = "./examples" "Active Project" = "dynamics" diff --git a/examples/external_interfaces/Helics_example/run_dummy_federate.py b/examples/external_interfaces/Helics_example/run_dummy_federate.py index 0a887681..7d6e627e 100644 --- a/examples/external_interfaces/Helics_example/run_dummy_federate.py +++ b/examples/external_interfaces/Helics_example/run_dummy_federate.py @@ -1,8 +1,5 @@ import time import helics as h -from math import pi -import random -import time initstring = "-f 2 --name=mainbroker" fedinitstring = "--broker=mainbroker --federates=1" @@ -34,7 +31,7 @@ # Set one second message interval # h.helicsFederateInfoSetTimeProperty(fedinfo, h.helics_property_time_delta, deltat) -#h.helicsFederateInfoSetIntegerProperty(fedinfo, h.helics_property_int_log_level, 20) +# h.helicsFederateInfoSetIntegerProperty(fedinfo, h.helics_property_int_log_level, 20) # Create value federate # vfed = h.helicsCreateValueFederate("Test Federate", fedinfo) @@ -43,22 +40,20 @@ pub2 = h.helicsFederateRegisterGlobalTypePublication(vfed, "test.feederhead.voltage", "double", "") pub3 = h.helicsFederateRegisterGlobalTypePublication(vfed, "test.feederhead.angle", "double", "") sub1 = h.helicsFederateRegisterSubscription(vfed, "pydss.Circuit.heco19021.TotalPower", "") -#h.helicsInputSetMinimumChange(sub1, 0.1) +# h.helicsInputSetMinimumChange(sub1, 0.1) # Enter execution mode # h.helicsFederateEnterExecutingMode(vfed) for t in range(1, 30): time_requested = t * 60 - #currenttime = h.helicsFederateRequestTime(vfed, time_requested) + # currenttime = h.helicsFederateRequestTime(vfed, time_requested) iteration_state = h.helics_iteration_result_iterating for i in range(20): currenttime, iteration_state = h.helicsFederateRequestTimeIterative( - vfed, - time_requested, - h.helics_iteration_request_iterate_if_needed + vfed, time_requested, h.helics_iteration_request_iterate_if_needed ) - h.helicsPublicationPublishDouble(pub1, 5.0 + 1. / (1.0 + i)) + h.helicsPublicationPublishDouble(pub1, 5.0 + 1.0 / (1.0 + i)) h.helicsPublicationPublishDouble(pub2, 1.0) h.helicsPublicationPublishDouble(pub3, 120.0) value = h.helicsInputGetVector(sub1) @@ -74,4 +69,4 @@ h.helicsBrokerDestroy(broker) h.helicsBrokerFree(broker) -h.helicsCloseLibrary() \ No newline at end of file +h.helicsCloseLibrary() diff --git a/examples/external_interfaces/Python_example/run_pyDSS.py b/examples/external_interfaces/Python_example/run_pyDSS.py index 161474ac..9253ed1a 100644 --- a/examples/external_interfaces/Python_example/run_pyDSS.py +++ b/examples/external_interfaces/Python_example/run_pyDSS.py @@ -1,31 +1,33 @@ -import click import toml import sys import os +import typer -@click.command() -@click.option('--pydss_path', - default=r'C:\Users\alatif\Desktop\pydss_test') -@click.option('--sim_path', - default=r'C:\Users\alatif\Desktop\pydss_test\examples\external_interfaces\Python_example') -@click.option('--sim_file', - default=r'simulation.toml') -def run_pyDSS(pydss_path, sim_path, sim_file): + +def run_pydss( + pydss_path: str = typer.Option(r"C:\Users\alatif\Desktop\pydss_test"), + sim_path: str = typer.Option( + r"C:\Users\alatif\Desktop\pydss_test\examples\external_interfaces\Python_example" + ), + sim_file: str = typer.Option(r"simulation.toml"), +): sys.path.append(pydss_path) - sys.path.append(os.path.join(pydss_path, 'pydss')) - file1 = open(os.path.join(sim_path, sim_file),"r") + sys.path.append(os.path.join(pydss_path, "pydss")) + file1 = open(os.path.join(sim_path, sim_file), "r") text = file1.read() sim_args = toml.loads(text) - from pydss import instance as dssInstance - a = dssInstance() - dssInstance = a.create_dss_instance(sim_args) + from pydss import instance + + a = instance() + dss_instance = a.create_dss_instance(sim_args) for t in range(5): - x = {'Load.mpx000635970':{'kW':7.28}} - results = dssInstance.RunStep(t, x) - print(results['Load.mpx000635970']['Powers']['E']['value']) - dssInstance.ResultContainer.ExportResults() - dssInstance.DeleteInstance() + x = {"Load.mpx000635970": {"kW": 7.28}} + results = dss_instance.RunStep(t, x) + print(results["Load.mpx000635970"]["Powers"]["E"]["value"]) + dss_instance.ResultContainer.ExportResults() + dss_instance.DeleteInstance() del a -run_pyDSS() +if __name__ == "__main__": + typer.run(run_pydss) diff --git a/examples/external_interfaces/Python_example/simulation.toml b/examples/external_interfaces/Python_example/simulation.toml index b6631015..af2dd608 100644 --- a/examples/external_interfaces/Python_example/simulation.toml +++ b/examples/external_interfaces/Python_example/simulation.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples/external_interfaces" "Active Project" = "pydss_project" diff --git a/examples/external_interfaces/Socket_example/run_socket_controller.py b/examples/external_interfaces/Socket_example/run_socket_controller.py index ee9f6805..81396cb0 100644 --- a/examples/external_interfaces/Socket_example/run_socket_controller.py +++ b/examples/external_interfaces/Socket_example/run_socket_controller.py @@ -4,15 +4,15 @@ import socket import struct -import time # next create a socket object + def example(): sockets = [] for i in range(2): logger.info("socket bound") s = socket.socket() - s.bind(('127.0.0.1', 5001 + i)) + s.bind(("127.0.0.1", 5001 + i)) s.listen(5) sockets.append(s) logger.info(f"{sockets=}") @@ -20,27 +20,26 @@ def example(): while True: # Establish connection with client. conns = [] - for s in sockets: + for s in sockets: c, addr = s.accept() logger.info(f"{c=}, {addr=}") conns.append(c) while True: for c in conns: - Data = c.recv(1024) - logger.info(f"{Data=}") - if Data: - numDoubles = int(len(Data) / 8) - tag = str(numDoubles) + 'd' - Data = list(struct.unpack(tag, Data)) - logger.info(f"{Data=}") - - - for c , v in zip(conns, [5, 3]): + data = c.recv(1024) + logger.info(f"{data=}") + if data: + num_doubles = int(len(data) / 8) + tag = str(num_doubles) + "d" + data = list(struct.unpack(tag, data)) + logger.info(f"{data=}") + + for c, v in zip(conns, [5, 3]): values = [v] logger.info(f"{values=}") - c.sendall(struct.pack('%sd' % len(values), *values)) - + c.sendall(struct.pack("%sd" % len(values), *values)) + + if __name__ == "__main__": example() - diff --git a/examples/external_interfaces/pydss_project/Exports/socket/FeederHeadInfo.json b/examples/external_interfaces/pydss_project/Exports/socket/FeederHeadInfo.json index 590e153a..3639f30a 100644 --- a/examples/external_interfaces/pydss_project/Exports/socket/FeederHeadInfo.json +++ b/examples/external_interfaces/pydss_project/Exports/socket/FeederHeadInfo.json @@ -1 +1 @@ -{"FeederHeadLine": "Line.sl_9", "FeederHeadLoading": 0.06914843903984673, "FeederHeadLoadKW": -3.4849336301157035, "FeederHeadLoadKVar": -0.03366811435864132, "ReversePowerFlow": false} \ No newline at end of file +{"FeederHeadLine": "Line.sl_9", "FeederHeadLoading": 0.012777788641365534, "FeederHeadLoadKW": -0.5725675594623055, "FeederHeadLoadKVar": 0.3413690014931734, "ReversePowerFlow": false} \ No newline at end of file diff --git a/examples/external_interfaces/pydss_project/Exports/socket/LoadsInfo.csv b/examples/external_interfaces/pydss_project/Exports/socket/LoadsInfo.csv index 0ecbc06e..484ec795 100644 --- a/examples/external_interfaces/pydss_project/Exports/socket/LoadsInfo.csv +++ b/examples/external_interfaces/pydss_project/Exports/socket/LoadsInfo.csv @@ -1,5 +1,5 @@ ,phases,bus1,kV,kW,pf,model,yearly,daily,duty,growth,conn,kvar,Rneut,Xneut,status,class,Vminpu,Vmaxpu,Vminnorm,Vminemerg,xfkVA,allocationfactor,kVA,%mean,%stddev,CVRwatts,CVRvars,kwh,kwhdays,Cfactor,CVRcurve,NumCust,ZIPV,%SeriesRL,RelWeight,Vlowpu,puXharm,XRharm,spectrum,basefreq,enabled,like -Load.mpx000635970,1,110_2.1,0.24,5.961,0.987278887205994,1,lp_mpx000635970,,,,wye,0.96,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,6.03780763191409,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, -Load.mpx000460267,1,111_2.1,0.24,13.164,0.985668177157945,1,lp_mpx000460267,,,,wye,2.253,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,13.3554073318638,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, +Load.mpx000635970,1,110_2.1,0.24,0,0.987278887205994,1,lp_mpx000635970,,,,wye,0,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, +Load.mpx000460267,1,111_2.1,0.24,0,0.985668177157945,1,lp_mpx000460267,,,,wye,0,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, Load.mpx000637601,1,112_2.1,0.24,1.575,0.891350249023867,1,lp_mpx000637601,,,,wye,0.801,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,1.76698217308495,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, Load.mpx000594341,1,113_2.1,0.24,5.784,0.988083581678164,1,lp_mpx000594341,,,,wye,0.901,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,5.85375580290125,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, diff --git a/examples/external_interfaces/pydss_project/Exports/socket/TransformersInfo.csv b/examples/external_interfaces/pydss_project/Exports/socket/TransformersInfo.csv index cf5e9ffd..1ed87e89 100644 --- a/examples/external_interfaces/pydss_project/Exports/socket/TransformersInfo.csv +++ b/examples/external_interfaces/pydss_project/Exports/socket/TransformersInfo.csv @@ -1,2 +1,2 @@ ,phases,windings,wdg,bus,conn,kV,kVA,tap,%R,Rneut,Xneut,buses,conns,kVs,kVAs,taps,XHL,XHT,XLT,Xscarray,thermal,n,m,flrise,hsrise,%loadloss,%noloadloss,normhkVA,emerghkVA,sub,MaxTap,MinTap,NumTaps,subname,%imag,ppm_antifloat,%Rs,bank,XfmrCode,XRConst,X12,X13,X23,LeadLag,WdgCurrents,Core,RdcOhms,Seasons,Ratings,normamps,emergamps,faultrate,pctperm,repair,basefreq,enabled,like -Transformer.tsf,1,2,2,tsf.1,wye,0.24,37.5,1,0.38195,-1,0,"['sourcebus.1', 'tsf.1']","['wye', 'wye']","['7.2', '0.24']","['37.5', '37.5']","['1', '1']",2.1,35,30,['2.1'],2,0.8,0.8,65,15,0.7639,0.0851,41.25,50,No,1.1,0.9,32,,0,1,"['0.38195', '0.38195']",,,No,2.1,35,30,Lag,"0.4701593, (-0.55352), 13.96798, (179.44), ",shell,0.0049867392,1,['1100'],5.72916666666667,6.94444444444444,0.007,0,0,60,Yes, +Transformer.tsf,1,2,2,tsf.1,wye,0.24,37.5,1,0.38195,-1,0,"['sourcebus.1', 'tsf.1']","['wye', 'wye']","['7.2', '0.24']","['37.5', '37.5']","['1', '1']",2.1,35,30,['2.1'],2,0.8,0.8,65,15,0.7639,0.0851,41.25,50,No,1.1,0.9,32,,0,1,"['0.38195', '0.38195']",,,No,2.1,35,30,Lag,"0.08992922, (30.804), 2.581351, (-147.64), ",shell,0.0049867392,1,['1100'],5.72916666666667,6.94444444444444,0.007,0,0,60,Yes, diff --git a/examples/external_interfaces/pydss_project/helics.toml b/examples/external_interfaces/pydss_project/helics.toml index c40f0de4..04eb67b5 100644 --- a/examples/external_interfaces/pydss_project/helics.toml +++ b/examples/external_interfaces/pydss_project/helics.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples/external_interfaces" "Active Project" = "pydss_project" diff --git a/examples/external_interfaces/pydss_project/helics_itr.toml b/examples/external_interfaces/pydss_project/helics_itr.toml index 68278f05..7b4bd60f 100644 --- a/examples/external_interfaces/pydss_project/helics_itr.toml +++ b/examples/external_interfaces/pydss_project/helics_itr.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples/external_interfaces" "Active Project" = "pydss_project" diff --git a/examples/external_interfaces/pydss_project/simulation.toml b/examples/external_interfaces/pydss_project/simulation.toml index de8db75a..36be9180 100644 --- a/examples/external_interfaces/pydss_project/simulation.toml +++ b/examples/external_interfaces/pydss_project/simulation.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples/external_interfaces" "Active Project" = "pydss_project" diff --git a/examples/external_interfaces/pydss_project/socket.toml b/examples/external_interfaces/pydss_project/socket.toml index a4c03120..428effca 100644 --- a/examples/external_interfaces/pydss_project/socket.toml +++ b/examples/external_interfaces/pydss_project/socket.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples/external_interfaces" "Active Project" = "pydss_project" diff --git a/examples/harmonics/Exports/qsts/CapacitorsInfo.csv b/examples/harmonics/Exports/qsts/CapacitorsInfo.csv index 2cc0241e..f814b6ad 100644 --- a/examples/harmonics/Exports/qsts/CapacitorsInfo.csv +++ b/examples/harmonics/Exports/qsts/CapacitorsInfo.csv @@ -1,5 +1,5 @@ ,bus1,bus2,phases,kvar,kv,conn,cmatrix,cuf,R,XL,Harm,Numsteps,states,normamps,emergamps,faultrate,pctperm,repair,basefreq,enabled,like -Capacitor.c83,83,83.0.0.0,3,['600'],4.16,wye,"('0 |2.30619868095678E-308 1.42963876636301E254 |7.49918647827998E253 6.96411092560237E258 9.78191238982914E205',)",['91.9673'],['0'],['0'],['0'],1,['1'],112.416759145095,149.889012193461,0.0005,100,3,60,Yes, +Capacitor.c83,83,83.0.0.0,3,['600'],4.16,wye,"('0 |4.34777768340297E-316 2.12199579294153E-308 |8.48798316534329E-308 0 0',)",['91.9673'],['0'],['0'],['0'],1,['1'],112.416759145095,149.889012193461,0.0005,100,3,60,Yes, Capacitor.c88a,88.1,88.0.0.0,1,['50'],2.402,wye,"('0',)",['22.9876'],['0'],['0'],['0'],1,['1'],28.1015820149875,37.4687760199833,0.0005,100,3,60,Yes, Capacitor.c90b,90.2,90.0.0.0,1,['50'],2.402,wye,"('0',)",['22.9876'],['0'],['0'],['0'],1,['1'],28.1015820149875,37.4687760199833,0.0005,100,3,60,Yes, Capacitor.c92c,92.3,92.0.0.0,1,['50'],2.402,wye,"('0',)",['22.9876'],['0'],['0'],['0'],1,['1'],28.1015820149875,37.4687760199833,0.0005,100,3,60,Yes, diff --git a/examples/harmonics/Exports/static/CapacitorsInfo.csv b/examples/harmonics/Exports/static/CapacitorsInfo.csv index df78ab5d..f814b6ad 100644 --- a/examples/harmonics/Exports/static/CapacitorsInfo.csv +++ b/examples/harmonics/Exports/static/CapacitorsInfo.csv @@ -1,5 +1,5 @@ ,bus1,bus2,phases,kvar,kv,conn,cmatrix,cuf,R,XL,Harm,Numsteps,states,normamps,emergamps,faultrate,pctperm,repair,basefreq,enabled,like -Capacitor.c83,83,83.0.0.0,3,['600'],4.16,wye,"('0 |2.49009085503988E-315 2.22396058465097E-308 |5.21501819665421E-304 0 2.56377452879502E-308',)",['91.9673'],['0'],['0'],['0'],1,['1'],112.416759145095,149.889012193461,0.0005,100,3,60,Yes, +Capacitor.c83,83,83.0.0.0,3,['600'],4.16,wye,"('0 |4.34777768340297E-316 2.12199579294153E-308 |8.48798316534329E-308 0 0',)",['91.9673'],['0'],['0'],['0'],1,['1'],112.416759145095,149.889012193461,0.0005,100,3,60,Yes, Capacitor.c88a,88.1,88.0.0.0,1,['50'],2.402,wye,"('0',)",['22.9876'],['0'],['0'],['0'],1,['1'],28.1015820149875,37.4687760199833,0.0005,100,3,60,Yes, Capacitor.c90b,90.2,90.0.0.0,1,['50'],2.402,wye,"('0',)",['22.9876'],['0'],['0'],['0'],1,['1'],28.1015820149875,37.4687760199833,0.0005,100,3,60,Yes, Capacitor.c92c,92.3,92.0.0.0,1,['50'],2.402,wye,"('0',)",['22.9876'],['0'],['0'],['0'],1,['1'],28.1015820149875,37.4687760199833,0.0005,100,3,60,Yes, diff --git a/examples/harmonics/simulation.toml b/examples/harmonics/simulation.toml index cd498e93..d484377a 100644 --- a/examples/harmonics/simulation.toml +++ b/examples/harmonics/simulation.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Time" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples" "Active Project" = "harmonics" diff --git a/examples/harmonics/snapshot.toml b/examples/harmonics/snapshot.toml index fcaa6c60..f0b1740d 100644 --- a/examples/harmonics/snapshot.toml +++ b/examples/harmonics/snapshot.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "Snapshot" "Project Path" = "./examples" "Active Project" = "harmonics" diff --git a/examples/independent_control_demo.py b/examples/independent_control_demo.py deleted file mode 100644 index a14b3a5b..00000000 --- a/examples/independent_control_demo.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Example file to demonstrate how to use pydss controllers""" - -import opendssdirect as dss - -from pydss.controllers import ( - CircuitElementController, - ControllerManager, - PvControllerModel, -) -from pydss.simulation_input_models import ProjectModel - - -def main(): - settings = ProjectModel( - max_control_iterations=50, - error_tolerance=0.0001, - ) - dss.run_command("compile tests/data/project/DSSfiles/Master_Spohn_existing_VV.dss") - - volt_var_model = PvControllerModel( - Control1="VVar", - Control2="None", - Control3="None", - pf=1, - pfMin=0.8, - pfMax=1, - Pmin=0, - Pmax=1, - uMin=0.9399999999999999, - uDbMin=0.97, - uDbMax=1.03, - uMax=1.06, - QlimPU=0.44, - PFlim=0.9, - enable_pf_limit=False, - uMinC=1.06, - uMaxC=1.1, - PminVW=10, - VWtype="Rated Power", - percent_p_cutin=10, - percent_p_cutout=10, - Efficiency=100, - Priority="Var", - DampCoef=0.8, - ) - controller = CircuitElementController(volt_var_model) # Use all elements. - manager = ControllerManager.create([controller], settings) - - done = False - while not done: - has_converged = manager.run_controls() - if has_converged: - print("Reached convergence") - done = True - else: - # TODO: just an example. Real code would have other logic. - raise Exception("Failed to converge") - - -if __name__ == "__main__": - main() diff --git a/examples/monte_carlo/Exports/scenario_1/FeederHeadInfo.json b/examples/monte_carlo/Exports/scenario_1/FeederHeadInfo.json index c17211dd..faa8859e 100644 --- a/examples/monte_carlo/Exports/scenario_1/FeederHeadInfo.json +++ b/examples/monte_carlo/Exports/scenario_1/FeederHeadInfo.json @@ -1 +1 @@ -{"FeederHeadLine": "Line.sl_9", "FeederHeadLoading": 0.008967717520677152, "FeederHeadLoadKW": -0.47567998161488056, "FeederHeadLoadKVar": -0.057826091258118315, "ReversePowerFlow": false} \ No newline at end of file +{"FeederHeadLine": "Line.sl_9", "FeederHeadLoading": 0.009731835918101209, "FeederHeadLoadKW": -0.5119653554699127, "FeederHeadLoadKVar": -0.07256290368325088, "ReversePowerFlow": false} \ No newline at end of file diff --git a/examples/monte_carlo/Exports/scenario_1/LoadsInfo.csv b/examples/monte_carlo/Exports/scenario_1/LoadsInfo.csv index 6b07bbfb..a4a9814e 100644 --- a/examples/monte_carlo/Exports/scenario_1/LoadsInfo.csv +++ b/examples/monte_carlo/Exports/scenario_1/LoadsInfo.csv @@ -1,5 +1,5 @@ ,phases,bus1,kV,kW,pf,model,yearly,daily,duty,growth,conn,kvar,Rneut,Xneut,status,class,Vminpu,Vmaxpu,Vminnorm,Vminemerg,xfkVA,allocationfactor,kVA,%mean,%stddev,CVRwatts,CVRvars,kwh,kwhdays,Cfactor,CVRcurve,NumCust,ZIPV,%SeriesRL,RelWeight,Vlowpu,puXharm,XRharm,spectrum,basefreq,enabled,like -Load.mpx000635970,1,110_2.1,0.24,0.289073511082329,0.987278887205994,1,lp_mpx000635970,,,,wye,0.0465543651466255,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.292798230397095,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, -Load.mpx000460267,1,111_2.1,0.24,0.134366837937276,0.985668177157945,1,lp_mpx000460267,,,,wye,0.0229966944600945,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.136320560053695,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, -Load.mpx000637601,1,112_2.1,0.24,0.581158184061614,0.891350249023867,1,lp_mpx000637601,,,,wye,0.295560447894192,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.651997556177329,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, -Load.mpx000594341,1,113_2.1,0.24,0.119701035811618,0.988083581678164,1,lp_mpx000594341,,,,wye,0.0186463750460353,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.1211446460919,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, +Load.mpx000635970,1,110_2.1,0.24,0.639107751171543,0.987278887205994,1,lp_mpx000635970,,,,wye,0.102926260883187,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.647342670296761,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, +Load.mpx000460267,1,111_2.1,0.24,0.145769003777463,0.985668177157945,1,lp_mpx000460267,,,,wye,0.0249481590330163,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.147888515786084,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, +Load.mpx000637601,1,112_2.1,0.24,0.0938369566047679,0.891350249023867,1,lp_mpx000637601,,,,wye,0.0477227950732819,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.10527506634741,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, +Load.mpx000594341,1,113_2.1,0.24,0.638416008632439,0.988083581678164,1,lp_mpx000594341,,,,wye,0.0994489667665681,-1,0,Variable,1,0.95,1.05,0,0,0,0.5,0.646115389902695,50,10,1,2,0,30,4,,1,['0 0 0 0 0 0 0'],50,1,0.5,0,6,defaultload,60,Yes, diff --git a/examples/monte_carlo/Exports/scenario_1/PVSystemsInfo.csv b/examples/monte_carlo/Exports/scenario_1/PVSystemsInfo.csv index 2de4ea77..043f60a7 100644 --- a/examples/monte_carlo/Exports/scenario_1/PVSystemsInfo.csv +++ b/examples/monte_carlo/Exports/scenario_1/PVSystemsInfo.csv @@ -1,6 +1,6 @@ ,phases,bus1,kv,irradiance,Pmpp,%Pmpp,Temperature,pf,conn,kvar,kVA,%Cutin,%Cutout,EffCurve,P-TCurve,%R,%X,model,Vminpu,Vmaxpu,Balanced,LimitCurrent,yearly,daily,duty,Tyearly,Tdaily,Tduty,class,UserModel,UserData,debugtrace,VarFollowInverter,DutyStart,WattPriority,PFPriority,%PminNoVars,%PminkvarMax,kvarMax,kvarMaxAbs,kVDC,Kp,PITol,SafeVoltage,SafeMode,DynamicEq,DynOut,ControlMode,AmpLimit,AmpLimitGain,spectrum,basefreq,enabled,like -PVSystem.pvgnem_mpx000635970,1,110-pv.1,0.24,1,20,100,25,0.95586704116733,wye,0,9.93237007021305,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000635970,,,,,,1,,,No,No,0,No,No,-1,-1,9.93237007021305,9.93237007021305,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, -PVSystem.pvgnem_mpx000460267,1,111-pv.1,0.24,1,8.5,100,25,0.954477228876004,wye,0,10.1429116337423,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000460267,,,,,,1,,,No,No,0,No,No,-1,-1,10.1429116337423,10.1429116337423,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, -PVSystem.pvgnem_mpx000594341,1,113-pv.1,0.24,1,4.6,100,25,0.95528911545434,wye,0,9.96875508261702,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000594341,,,,,,1,,,No,No,0,No,No,-1,-1,9.96875508261702,9.96875508261702,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, -PVSystem.pvgui_mpx000637601,1,112-pv.1,0.24,1.01069592198581,5.5,100,25,0.974558593129425,wye,0,10.1848767684801,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000637601,,,,,,1,,,No,No,0,No,No,-1,-1,10.1848767684801,10.1848767684801,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, -PVSystem.pvgui_mpx000460267,1,111-pv.1,0.24,0.925303188892118,3.9,100,25,0.957577343968793,wye,0,10.0437113791532,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000460267,,,,,,1,,,No,No,0,No,No,-1,-1,10.0437113791532,10.0437113791532,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, +PVSystem.pvgnem_mpx000635970,1,110-pv.1,0.24,1,20,100,25,0.978225122857338,wye,0,10.1050158704125,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000635970,,,,,,1,,,No,No,0,No,No,-1,-1,10.1050158704125,10.1050158704125,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, +PVSystem.pvgnem_mpx000460267,1,111-pv.1,0.24,1,8.5,100,25,0.97863402160381,wye,0,10.0184440366674,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000460267,,,,,,1,,,No,No,0,No,No,-1,-1,10.0184440366674,10.0184440366674,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, +PVSystem.pvgnem_mpx000594341,1,113-pv.1,0.24,1,4.6,100,25,0.970394608642052,wye,0,10.0166852892521,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000594341,,,,,,1,,,No,No,0,No,No,-1,-1,10.0166852892521,10.0166852892521,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, +PVSystem.pvgui_mpx000637601,1,112-pv.1,0.24,1.01069592198581,5.5,100,25,0.978864996577124,wye,0,9.90333326709121,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000637601,,,,,,1,,,No,No,0,No,No,-1,-1,9.90333326709121,9.90333326709121,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, +PVSystem.pvgui_mpx000460267,1,111-pv.1,0.24,0.925303188892118,3.9,100,25,0.979784945973254,wye,0,9.98060006049092,20,20,,,50,0,1,0.9,1.1,No,No,pv_mpx000460267,,,,,,1,,,No,No,0,No,No,-1,-1,9.98060006049092,9.98060006049092,8,0.01,0,80,No,,[],GFL,-1,0.8,,60,Yes, diff --git a/examples/monte_carlo/Exports/scenario_1/TransformersInfo.csv b/examples/monte_carlo/Exports/scenario_1/TransformersInfo.csv index e904be4e..f6639810 100644 --- a/examples/monte_carlo/Exports/scenario_1/TransformersInfo.csv +++ b/examples/monte_carlo/Exports/scenario_1/TransformersInfo.csv @@ -1,2 +1,2 @@ ,phases,windings,wdg,bus,conn,kV,kVA,tap,%R,Rneut,Xneut,buses,conns,kVs,kVAs,taps,XHL,XHT,XLT,Xscarray,thermal,n,m,flrise,hsrise,%loadloss,%noloadloss,normhkVA,emerghkVA,sub,MaxTap,MinTap,NumTaps,subname,%imag,ppm_antifloat,%Rs,bank,XfmrCode,XRConst,X12,X13,X23,LeadLag,WdgCurrents,Core,RdcOhms,Seasons,Ratings,normamps,emergamps,faultrate,pctperm,repair,basefreq,enabled,like -Transformer.tsf,1,2,2,tsf.1,wye,0.24,37.5,1,0.38195,-1,0,"['sourcebus.1', 'tsf.1']","['wye', 'wye']","['7.2', '0.24']","['37.5', '37.5']","['1', '1']",2.1,35,30,['2.1'],2,0.8,0.8,65,15,0.7639,0.0851,41.25,50,No,1.1,0.9,32,,0,1,"['0.38195', '0.38195']",,,No,2.1,35,30,Lag,"0.0648911, (-6.9312), 1.811422, (172.55), ",shell,0.0049867392,1,['1100'],5.72916666666667,6.94444444444444,0.007,0,0,60,Yes, +Transformer.tsf,1,2,2,tsf.1,wye,0.24,37.5,1,0.38195,-1,0,"['sourcebus.1', 'tsf.1']","['wye', 'wye']","['7.2', '0.24']","['37.5', '37.5']","['1', '1']",2.1,35,30,['2.1'],2,0.8,0.8,65,15,0.7639,0.0851,41.25,50,No,1.1,0.9,32,,0,1,"['0.38195', '0.38195']",,,No,2.1,35,30,Lag,"0.07002356, (-8.067), 1.965765, (171.38), ",shell,0.0049867392,1,['1100'],5.72916666666667,6.94444444444444,0.007,0,0,60,Yes, diff --git a/examples/monte_carlo/simulation.toml b/examples/monte_carlo/simulation.toml index 3714a10d..f4ce4db0 100644 --- a/examples/monte_carlo/simulation.toml +++ b/examples/monte_carlo/simulation.toml @@ -6,7 +6,7 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false +"OpenMDAO Reports" = true "Simulation Type" = "QSTS" "Project Path" = "./examples" "Active Project" = "monte_carlo" diff --git a/pyproject.toml b/pyproject.toml index d2573023..abbaadc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,14 @@ classifiers = [ "Programming Language :: Python :: 3", ] dependencies = [ - "click~=8.1.8", + "typer~=0.15.2", "h5py~=3.13.0", "helics~=3.6.1", "loguru~=0.7.3", + "matplotlib~=3.10.1", "networkx~=3.4.2", "numpy~=2.2.4", + "openmdao>=3.37,<4", "OpenDSSDirect.py==0.8.4", "pandas~=2.2.3", "pvder~=0.6.0", @@ -48,11 +50,12 @@ dev = [ "mock~=5.2.0", "pytest~=9.1.0", "pytest-cov~=7.1.0", + "pre-commit~=4.3.0", + "ruff~=0.15.5", "setuptools~=75.8.0", "sphinx~=7.4.7", "sphinx-rtd-theme~=3.1.0", "furo~=2024.8.6", - "sphinx-click~=6.0.0", "sphinxcontrib-openapi~=0.8.4", "sphinxcontrib-redoc~=1.6.0", "autodoc-pydantic~=2.0.1", @@ -71,6 +74,19 @@ pydss = "pydss.cli.pydss:cli" Homepage = "https://github.com/NatLabRockies/PyDSS" Documentation = "https://natlabrockies.github.io/PyDSS/" +[tool.ruff] +target-version = "py310" +line-length = 100 +extend-exclude = [ + "docs/build", + "examples/**/Exports", + "examples/**/Logs", + "examples/**/OpenMDAOReports", +] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "N"] + [tool.hatch.build.targets.sdist] include = [ diff --git a/src/pydss/Extensions/MonteCarlo.py b/src/pydss/Extensions/MonteCarlo.py deleted file mode 100644 index d2960b2d..00000000 --- a/src/pydss/Extensions/MonteCarlo.py +++ /dev/null @@ -1,59 +0,0 @@ -from ast import literal_eval -import os - -from loguru import logger -from scipy import stats -import numpy as np - -from pydss.simulation_input_models import SimulationSettingsModel -from pydss.utils import utils - -class MonteCarloSim: - - def __init__(self, settings: SimulationSettingsModel, dssPaths, dssObjects, dssObjectsByClass): - self.__dssPaths = dssPaths - self.__dssObjects = dssObjects - self._settings = settings - self.__dssObjectsByClass = dssObjectsByClass - - try: - MCfile = os.path.join(self._settings.project.active_scenario, 'Monte_Carlo', 'MonteCarloSettings.toml') - MCfilePath = os.path.join(self.__dssPaths['Import'], MCfile) - - logger.info('Reading monte carlo scenario settings file from ' + MCfilePath) - self.__MCsettingsDict = utils.load_data(MCfilePath) - except: - logger.error('Failed to read Monte Carlo scenario generation file %s', MCfilePath) - raise - return - - def Create_Scenario(self): - for key, Properties in self.__MCsettingsDict.items(): - if Properties['Class'] in self.__dssObjectsByClass: - Elements = self.__dssObjectsByClass[Properties['Class']] - ElmNames = list(Elements.keys()) - if Properties['useWildCard']: - ElmNames = [x for x in ElmNames if Properties['Wildcard'] in x] - NumElms = len(ElmNames) - distParams = literal_eval(Properties['Parameters']) - - dist = getattr(stats, Properties['Distribution'].replace(' ', '')) - if not Properties['isList']: - MCsamples = dist.rvs(*distParams, size=NumElms) - if Properties['isInteger']: - MCsamples = [int(round(x)) for x in MCsamples] - for ElmName, Value in zip(ElmNames,MCsamples): - Elements[ElmName].SetParameter(Properties['Property'], Value) - else: - MCsamples = dist.rvs(*distParams, size=NumElms * Properties['ListLength']) - if Properties['isInteger']: - MCsamples = [int(round(x)) for x in MCsamples] - MCsamples = np.reshape(MCsamples, (NumElms, Properties['ListLength'])) - for ElmName, Value in zip(ElmNames, MCsamples): - Value = str(Value).replace('\n', '').replace('\r', '').replace('[ ', '[').replace(' ]', ']') - Elements[ElmName].SetParameter(Properties['Property'], Value) - else: - logger.warning(Properties['Class'] + ' class not present in object dictionary.') - return - - diff --git a/src/pydss/ProfileManager/ProfileInterface.py b/src/pydss/ProfileManager/ProfileInterface.py deleted file mode 100644 index c2831a23..00000000 --- a/src/pydss/ProfileManager/ProfileInterface.py +++ /dev/null @@ -1,26 +0,0 @@ -from os.path import dirname, basename, isfile -from pydss.ProfileManager import hooks -import importlib -import glob - - -modules = glob.glob(hooks.__path__[0]+"/*.py") -pythonFiles = [(basename(f)[:-3], f) for f in modules if isfile(f) and not f.endswith('__init__.py')] - -ProfileInterfaces = {} -modules_instances = {} -for module, file in pythonFiles: - spec = importlib.util.spec_from_file_location(module, file) - modules_instances[module] = importlib.util.module_from_spec(spec) - spec.loader.exec_module(modules_instances[module]) - ProfileInterfaces[module] = modules_instances[module].ProfileManager - -def Create(sim_instance, solver, settings, logger, **kwargs): - source_type = settings.profiles.source_type.value - if source_type in ProfileInterfaces: - PostProcessor = ProfileInterfaces[source_type]( - sim_instance, solver, settings, logger, **kwargs - ) - else: - raise ModuleNotFoundError(f"{source_type} is not a valid source type. Valid values are {','.join(list(ProfileInterfaces.keys()))}") - return PostProcessor diff --git a/src/pydss/ProfileManager/__init__.py b/src/pydss/ProfileManager/__init__.py deleted file mode 100644 index a26f655d..00000000 --- a/src/pydss/ProfileManager/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pydss.ProfileManager import ProfileInterface diff --git a/src/pydss/ProfileManager/hooks/__init__.py b/src/pydss/ProfileManager/hooks/__init__.py deleted file mode 100644 index a26f655d..00000000 --- a/src/pydss/ProfileManager/hooks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pydss.ProfileManager import ProfileInterface diff --git a/src/pydss/ProfileManager/hooks/h5.py b/src/pydss/ProfileManager/hooks/h5.py deleted file mode 100644 index 9e427727..00000000 --- a/src/pydss/ProfileManager/hooks/h5.py +++ /dev/null @@ -1,185 +0,0 @@ -from pydss.ProfileManager.base_definitions import BaseProfileManager, BaseProfile -from pydss.ProfileManager.common import PROFILE_TYPES -from pydss.exceptions import InvalidParameter -from pydss.common import DATE_FORMAT -from datetime import datetime -import pandas as pd -import numpy as np -import datetime -import h5py -import copy -import os - -class ProfileManager(BaseProfileManager): - - def __init__(self, sim_instance, solver, options, logger, **kwargs): - super(ProfileManager, self).__init__(sim_instance, solver, options, logger, **kwargs) - self.Objects = kwargs["objects"] - if os.path.exists(self.basepath): - self.logger.info("Loading existing h5 store") - self.store = h5py.File(self.basepath, "r+") - else: - self.logger.info("Creating new h5 store") - self.store = h5py.File(self.basepath, "w") - for profileGroup in PROFILE_TYPES.names(): - self.store.create_group(profileGroup) - self.setup_profiles() - return - - def setup_profiles(self): - self.Profiles = {} - for group, profileMap in self.mapping.items(): - if group in self.store: - grp = self.store[group] - for profileName, mappingDict in profileMap.items(): - if profileName in grp: - objects = {x['object']: self.Objects[x['object']] for x in mappingDict} - self.Profiles[f"{group}/{profileName}"] = Profile( - self.sim_instance, - grp[profileName], - objects, - self.solver, - mappingDict, - self.logger, - **self.kwargs - ) - else: - self.logger.warning("Group {} / data set {} not found in the h5 store".format( - group, profileName - )) - else: - self.logger.warning("Group {} not found in the h5 store".format(group)) - return - - def create_dataset(self, dname, pType, data ,startTime, resolution, units, info): - grp = self.store[pType] - if dname not in grp: - dset = grp.create_dataset( - dname, - data=data, - shape=(len(data),), - maxshape=(None,), - chunks=True, - compression="gzip", - compression_opts=4, - shuffle=True - ) - self.createMetadata( - dset, startTime, resolution, data, units, info - ) - else: - self.logger.error('Dataset "{}" already exists in group "{}".'.format(dname, pType)) - raise Exception('Dataset "{}" already exists in group "{}".'.format(dname, pType)) - - def add_from_arrays(self, data, name, pType, startTime, resolution, units="", info=""): - r, c = data.shape - if r > c: - for i in range(c): - d = data[:, i] - dname = name if i==0 else "{}_{}".format(name, i) - self.create_dataset(dname=dname, pType=pType, data=d, startTime=startTime, resolution=resolution, - units=units, info=info) - else: - for i in range(r): - d = data[i, :] - dname = name if i==0 else "{}_{}".format(name, i) - self.create_dataset(dname=dname, pType=pType, data=d, startTime=startTime, resolution=resolution, - units=units, info=info) - return - - def add_profiles_from_csv(self, csv_file, name, pType, startTime, resolution_sec=900, units="", - info=""): - data = pd.read_csv(csv_file).values - self.add_profiles(data, name, pType, startTime, resolution_sec=resolution_sec, units=units, info=info) - - - def add_profiles(self, data, name, pType, startTime, resolution_sec=900, units="", info=""): - if type(startTime) is not datetime.datetime: - raise InvalidParameter("startTime should be a python datetime object") - if pType not in PROFILE_TYPES.names(): - raise InvalidParameter("Valid values for pType are {}".format(PROFILE_TYPES.names())) - if data: - self.add_from_arrays(data, name, pType, startTime, resolution_sec, units=units, info=info) - self.store.flush() - return - - def createMetadata(self, dSet, startTime, resolution, data, units, info): - metadata = { - "sTime": str(startTime), - "eTime": str(startTime + datetime.timedelta(seconds=resolution*len(data))), - "resTime": resolution, - "npts": len(data), - "min": min(data), - "max": max(data), - "mean": np.mean(data), - "units": units, - "info": info, - } - for key, value in metadata.items(): - if isinstance(value, str): - value = np.string_(value) - dSet.attrs[key] = value - return - - def remove_profile(self, profile_type, profile_name): - return - - def update(self): - results = {} - for profileaName, profileObj in self.Profiles.items(): - result = profileObj.update() - results[profileaName] = result - return results - -class Profile(BaseProfile): - - DEFAULT_SETTINGS = { - "multiplier": 1, - "normalize": False, - "interpolate": False - } - - def __init__(self, sim_instance, dataset, devices, solver, mapping_dict, logger, **kwargs): - super(Profile, self).__init__(sim_instance, dataset, devices, solver, mapping_dict, logger, **kwargs) - self.valueSettings = {x['object']: {**self.DEFAULT_SETTINGS, **x} for x in mapping_dict} - - self.bufferSize = kwargs["bufferSize"] - self.buffer = np.zeros(self.bufferSize) - self.profile = dataset - self.neglectYear = kwargs["neglectYear"] - self.Objects = devices - - self.attrs = self.profile.attrs - self.sTime = datetime.datetime.strptime(self.attrs["sTime"].decode(), DATE_FORMAT) - self.eTime = datetime.datetime.strptime(self.attrs["eTime"].decode(), '%Y-%m-%d %H:%M:%S.%f') - self.simRes = solver.GetStepSizeSec() - self.Time = copy.deepcopy(solver.GetDateTime()) - return - - def update_profile_settings(self): - return - - def update(self, updateObjectProperties=True): - self.Time = copy.deepcopy(self.solver.GetDateTime()) - if self.Time < self.sTime or self.Time > self.eTime: - value = 0 - value1 = 0 - else: - dT = (self.Time - self.sTime).total_seconds() - n = int(dT / self.attrs["resTime"]) - value = self.profile[n] - dT2 = (self.Time - (self.sTime + datetime.timedelta(seconds=int(n * self.attrs["resTime"])))).total_seconds() - value1 = self.profile[n] + (self.profile[n+1] - self.profile[n]) * dT2 / self.attrs["resTime"] - if updateObjectProperties: - for objName, obj in self.Objects.items(): - if self.valueSettings[objName]['interpolate']: - value = value1 - mult = self.valueSettings[objName]['multiplier'] - if self.valueSettings[objName]['normalize']: - valueF = value / self.attrs["max"] * mult - else: - valueF = value * mult - obj.SetParameter(self.attrs["units"].decode(), valueF) - return value - - diff --git a/src/pydss/__init__.py b/src/pydss/__init__.py index 2cc09cac..92585c2c 100644 --- a/src/pydss/__init__.py +++ b/src/pydss/__init__.py @@ -1,4 +1,5 @@ __version__ = "3.1.8" from pydss.utils.timing_utils import timer_stats_collector -from . import * + +__all__ = ["timer_stats_collector"] diff --git a/src/pydss/api/server.py b/src/pydss/api/server.py index 7e1fca5c..cee18309 100644 --- a/src/pydss/api/server.py +++ b/src/pydss/api/server.py @@ -6,7 +6,7 @@ import io import re -from aiohttp_swagger3 import * +from aiohttp_swagger3 import SwaggerDocs, SwaggerUiSettings from loguru import logger from aiohttp import web @@ -14,36 +14,39 @@ import pydss.api.schema as schema import pydss + def read(*names, **kwargs): with io.open( os.path.join(os.path.dirname(pydss.__file__), *names), - encoding=kwargs.get("encoding", "utf8") + encoding=kwargs.get("encoding", "utf8"), ) as fp: return fp.read() + def find_version(*file_paths): version_file = read(*file_paths) - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") -def getJSONschema(host, port): + +def get_json_schema(host, port): base_path = schema.__path__._path[0] - path = os.path.join(base_path, 'pydss.v1.json') + path = os.path.join(base_path, "pydss.v1.json") base_url = f"http://{host}:{port}" url = base_url + "/docs/swagger.json" - isValid = False - while not isValid: + is_valid = False + while not is_valid: time.sleep(3) response = requests.get(url) - isValid = response.status_code == 200 - with open(path, 'w') as outfile: + is_valid = response.status_code == 200 + with open(path, "w") as outfile: json.dump(response.json(), outfile, indent=4, sort_keys=True) logger.info(f"Export the schema file to {path}") -class pydss_server(): + +class PydssServer: def __init__(self, host, port): self.handler = Handler() self.app = web.Application() @@ -51,32 +54,43 @@ def __init__(self, host, port): self.app, title="Pydss RESTful API documentation", version=find_version("__init__.py"), - description = "The API enables creating pydss instances, running simulations and creation of new projects.", + description="The API enables creating pydss instances, running simulations and creation of new projects.", swagger_ui_settings=SwaggerUiSettings(path="/docs/"), # components="components.yaml" ) self.register_media_handlers() self.add_routes() - t = threading.Thread(name='child procs', target=getJSONschema, args=(host, port,)) + t = threading.Thread( + name="child procs", + target=get_json_schema, + args=( + host, + port, + ), + ) t.start() def register_media_handlers(self): - self.swagger.register_media_type_handler("multipart/form-data", self.handler.post_pydss_create) + self.swagger.register_media_type_handler( + "multipart/form-data", self.handler.post_pydss_create + ) return def add_routes(self): - self.swagger.add_routes([ - web.get('/simulators/pydss/instances', self.handler.get_instance_uuids), - web.get('/simulators/pydss/status/uuid/{uuid}', self.handler.get_instance_status), - web.get('/simulators/pydss/info', self.handler.get_pydss_project_info), - - web.put('/simulators/pydss', self.handler.put_pydss), - - web.post('/simulators/pydss', self.handler.post_pydss), - web.post('/simulators/pydss/create', self.handler.post_pydss_create, validate=False), + self.swagger.add_routes( + [ + web.get("/simulators/pydss/instances", self.handler.get_instance_uuids), + web.get("/simulators/pydss/status/uuid/{uuid}", self.handler.get_instance_status), + web.get("/simulators/pydss/info", self.handler.get_pydss_project_info), + web.put("/simulators/pydss", self.handler.put_pydss), + web.post("/simulators/pydss", self.handler.post_pydss), + web.post( + "/simulators/pydss/create", self.handler.post_pydss_create, validate=False + ), + web.delete("/simulators/pydss", self.handler.delete_pydss), + ] + ) - web.delete('/simulators/pydss', self.handler.delete_pydss) - ]) async def clean_background_tasks(self, app): logger.info("cleanup_background_tasks") - self.handler.event.set() \ No newline at end of file + self.handler.event.set() diff --git a/src/pydss/api/src/app/DataWriter.py b/src/pydss/api/src/app/DataWriter.py deleted file mode 100644 index 88b72e7d..00000000 --- a/src/pydss/api/src/app/DataWriter.py +++ /dev/null @@ -1,16 +0,0 @@ -from pydss.api.src.app.HDF5 import hdf5Writer -from pydss.api.src.app.JSON_writer import JSONwriter - -class DataWriter: - modes = { - 'h5': hdf5Writer, - 'json': JSONwriter, - # 'parquet': parquetWriter, - # "pickle": pickleWriter, - } - - def __init__(self, log_dir, format, columnLength): - self.writer = self.modes[format](log_dir, columnLength) - - def write(self, fed_name, currenttime, powerflow_output, index): - self.writer.write(fed_name, currenttime, powerflow_output, index) \ No newline at end of file diff --git a/src/pydss/api/src/app/JSON_writer.py b/src/pydss/api/src/app/JSON_writer.py deleted file mode 100644 index 40f4a92e..00000000 --- a/src/pydss/api/src/app/JSON_writer.py +++ /dev/null @@ -1,127 +0,0 @@ -import json -import os - -class JSONwriter: - def __init__(self, log_dir, columnLength=None): - self.log_dir = log_dir - self.metadata = False - self.payload = False - return - - def parse_metadata(self, cName, LFresults, results): - for k, v in LFresults[cName].items(): - data = k.split("__") - name = data[0] - if len(data) == 3: - ppty = f"{data[1]}__{data[2]}" - else: - ppty = data[1] - asset = self.check_asset(cName, None, results) - if not asset: - asset = { - "assetType": cName, - "keyColumns": "Name", - "keyValues": [name], - "measurementColumns": [ - { - "name": ppty, - "type": "numeric" if not isinstance(v, str) else "string", - "mappings": {} - } - ] - } - results["assets"].append(asset) - else: - if name not in asset["keyValues"]: - asset["keyValues"].append(name) - colExists = False - for cols in asset["measurementColumns"]: - if cols["name"] == ppty: - colExists = True - if not colExists: - res = { - "name": ppty, - "type": "numeric" if not isinstance(v, str) else "string", - "mappings": {}, - } - asset["measurementColumns"].append(res) - return results - - def create_meta_data(self, LFresults, fed_name, fed_uuid, cosim_uuid, circuit, currenttime): - results = {} - results["cosimUUID"] = cosim_uuid - results["federateUUID"] = fed_uuid - results["interconnect"] = "distribution" - results["assets"] = [] - for key in LFresults: - results = self.parse_metadata(key, LFresults, results) - return results - - def update_payload(self, timestep, fed_uuid, cosim_uuid): - if not self.payload: - self.payload = { - "cosimUUID": cosim_uuid, - "federateUUID": fed_uuid, - "timeSteps": [timestep] - } - else: - self.payload["timeSteps"].append(timestep) - return - - def write(self, fed_name, currenttime, LFresults, index=None, circuit=None, fed_uuid=None, cosim_uuid=None): - jFile = open(os.path.join(self.log_dir, f"Results_{int(currenttime)}.json"), "w") - pFile = open(os.path.join(self.log_dir, f"payload.json"), "w") - if not self.metadata: - mFile = open(os.path.join(self.log_dir, f"metadata.json"), "w") - self.metadata = self.create_meta_data(LFresults, fed_name, fed_uuid, cosim_uuid, circuit, currenttime) - json.dump(self.metadata, mFile, indent=4, sort_keys=True) - mFile.close() - results = self.remap(LFresults, fed_name, fed_uuid, cosim_uuid, circuit, currenttime) - self.update_payload(currenttime, fed_uuid, cosim_uuid) - json.dump(results, jFile, indent=4, sort_keys=True) - json.dump(self.payload, pFile, indent=4, sort_keys=True) - jFile.close() - pFile.close() - return - - def __del__(self): - return - - def check_asset(self, className, ppty, results): - for asset in results["assets"]: - if ppty is not None: - if "assetType" in asset and asset["assetType"] == className and ppty in asset: - return asset - else: - if "assetType" in asset and asset["assetType"] == className: - return asset - return False - - def parse_class(self, cName, LFresults, results): - for k, v in LFresults[cName].items(): - data = k.split("__") - name = data[0] - if len(data) == 3: - ppty = f"{data[1]}__{data[2]}" - else: - ppty = data[1] - asset = self.check_asset(cName, ppty, results) - if not asset: - asset = { - "assetType": cName, - ppty: [v] - } - results["assets"].append(asset) - else: - asset[ppty].append(v) - return results - - def remap(self, LFresults, fed_name, fed_uuid, cosim_uuid, circuit, currenttime): - results = {} - results["cosimUUID"] = cosim_uuid - results["federateUUID"] = fed_uuid - results["timeStep"] = currenttime - results["assets"] = [] - for key in LFresults: - results = self.parse_class(key, LFresults, results) - return results \ No newline at end of file diff --git a/src/pydss/api/src/app/data_writer.py b/src/pydss/api/src/app/data_writer.py new file mode 100644 index 00000000..b5834791 --- /dev/null +++ b/src/pydss/api/src/app/data_writer.py @@ -0,0 +1,17 @@ +from pydss.api.src.app.hdf5 import Hdf5Writer +from pydss.api.src.app.json_writer import JsonWriter + + +class DataWriter: + modes = { + "h5": Hdf5Writer, + "json": JsonWriter, + # 'parquet': parquetWriter, + # "pickle": pickleWriter, + } + + def __init__(self, log_dir, format, column_length): + self.writer = self.modes[format](log_dir, column_length) + + def write(self, fed_name, currenttime, powerflow_output, index): + self.writer.write(fed_name, currenttime, powerflow_output, index) diff --git a/src/pydss/api/src/app/HDF5.py b/src/pydss/api/src/app/hdf5.py similarity index 52% rename from src/pydss/api/src/app/HDF5.py rename to src/pydss/api/src/app/hdf5.py index bc7a0250..2530751d 100644 --- a/src/pydss/api/src/app/HDF5.py +++ b/src/pydss/api/src/app/hdf5.py @@ -6,20 +6,21 @@ import pandas as pd import h5py -class hdf5Writer: - """ Class that handles writing simulation results to arrow - files. + +class Hdf5Writer: + """Class that handles writing simulation results to arrow + files. """ - def __init__(self, log_dir, columnLength): - """ Constructor """ + def __init__(self, log_dir, column_length): + """Constructor""" self.log_dir = log_dir - self.store = h5py.File(os.path.join(log_dir, 'groups_dict.hdf5'), 'w') + self.store = h5py.File(os.path.join(log_dir, "groups_dict.hdf5"), "w") self.store_groups = {} self.store_datasets = {} self.row = {} - self.columnLength = columnLength - self.chunkRows = 24 + self.column_length = column_length + self.chunk_rows = 24 self.step = 0 self.dfs = {} # Create arrow writer for each object type @@ -38,37 +39,44 @@ def write(self, fed_name, currenttime, powerflow_output, index): # Iterate through each object type for obj_type in powerflow_output: - Data = pd.DataFrame(powerflow_output[obj_type], index=[self.step]) + data = pd.DataFrame(powerflow_output[obj_type], index=[self.step]) if obj_type not in self.row: self.row[obj_type] = 0 self.store_groups[obj_type] = self.store.create_group(obj_type) self.store_datasets[obj_type] = {} - for colName in powerflow_output[obj_type].keys(): - self.store_datasets[obj_type][colName] = self.store_groups[obj_type].create_dataset( - colName, - shape=(self.columnLength, ), - maxshape=(None, ), + for column_name in powerflow_output[obj_type].keys(): + self.store_datasets[obj_type][column_name] = self.store_groups[ + obj_type + ].create_dataset( + column_name, + shape=(self.column_length,), + maxshape=(None,), chunks=True, compression="gzip", - compression_opts=4 + compression_opts=4, ) if obj_type not in self.dfs: - self.dfs[obj_type] = Data + self.dfs[obj_type] = data else: if self.dfs[obj_type] is None: - self.dfs[obj_type] = Data + self.dfs[obj_type] = data else: - self.dfs[obj_type] = self.dfs[obj_type].append(Data, ignore_index=True) + self.dfs[obj_type] = self.dfs[obj_type].append(data, ignore_index=True) - if self.step % self.chunkRows == self.chunkRows - 1: - si = int(self.step / self.chunkRows) * self.chunkRows - ei = si + self.chunkRows - for colName in powerflow_output[obj_type].keys(): - self.store_datasets[obj_type][colName][si:ei] = self.dfs[obj_type][colName] + if self.step % self.chunk_rows == self.chunk_rows - 1: + si = int(self.step / self.chunk_rows) * self.chunk_rows + ei = si + self.chunk_rows + for column_name in powerflow_output[obj_type].keys(): + self.store_datasets[obj_type][column_name][si:ei] = self.dfs[obj_type][ + column_name + ] self.dfs[obj_type] = None # Add object status data to a DataFrame self.step += 1 def __del__(self): self.store.flush() - self.store.close() \ No newline at end of file + self.store.close() + + +globals()["hdf5Writer"] = Hdf5Writer diff --git a/src/pydss/api/src/app/json_writer.py b/src/pydss/api/src/app/json_writer.py new file mode 100644 index 00000000..1fe976f4 --- /dev/null +++ b/src/pydss/api/src/app/json_writer.py @@ -0,0 +1,146 @@ +import json +import os + + +class JsonWriter: + def __init__(self, log_dir, column_length=None): + self.log_dir = log_dir + self.metadata = False + self.payload = False + return + + def parse_metadata(self, class_name, load_flow_results, results): + for key, value in load_flow_results[class_name].items(): + data = key.split("__") + name = data[0] + if len(data) == 3: + ppty = f"{data[1]}__{data[2]}" + else: + ppty = data[1] + asset = self.check_asset(class_name, None, results) + if not asset: + asset = { + "assetType": class_name, + "keyColumns": "Name", + "keyValues": [name], + "measurementColumns": [ + { + "name": ppty, + "type": "numeric" if not isinstance(value, str) else "string", + "mappings": {}, + } + ], + } + results["assets"].append(asset) + else: + if name not in asset["keyValues"]: + asset["keyValues"].append(name) + column_exists = False + for column in asset["measurementColumns"]: + if column["name"] == ppty: + column_exists = True + if not column_exists: + res = { + "name": ppty, + "type": "numeric" if not isinstance(value, str) else "string", + "mappings": {}, + } + asset["measurementColumns"].append(res) + return results + + def create_meta_data( + self, load_flow_results, fed_name, fed_uuid, cosim_uuid, circuit, current_time + ): + results = {} + results["cosimUUID"] = cosim_uuid + results["federateUUID"] = fed_uuid + results["interconnect"] = "distribution" + results["assets"] = [] + for key in load_flow_results: + results = self.parse_metadata(key, load_flow_results, results) + return results + + def update_payload(self, timestep, fed_uuid, cosim_uuid): + if not self.payload: + self.payload = { + "cosimUUID": cosim_uuid, + "federateUUID": fed_uuid, + "timeSteps": [timestep], + } + else: + self.payload["timeSteps"].append(timestep) + return + + def write( + self, + fed_name, + current_time, + load_flow_results, + index=None, + circuit=None, + fed_uuid=None, + cosim_uuid=None, + ): + json_file = open(os.path.join(self.log_dir, f"Results_{int(current_time)}.json"), "w") + payload_file = open(os.path.join(self.log_dir, "payload.json"), "w") + if not self.metadata: + metadata_file = open(os.path.join(self.log_dir, "metadata.json"), "w") + self.metadata = self.create_meta_data( + load_flow_results, fed_name, fed_uuid, cosim_uuid, circuit, current_time + ) + json.dump(self.metadata, metadata_file, indent=4, sort_keys=True) + metadata_file.close() + results = self.remap( + load_flow_results, fed_name, fed_uuid, cosim_uuid, circuit, current_time + ) + self.update_payload(current_time, fed_uuid, cosim_uuid) + json.dump(results, json_file, indent=4, sort_keys=True) + json.dump(self.payload, payload_file, indent=4, sort_keys=True) + json_file.close() + payload_file.close() + return + + def __del__(self): + return + + def check_asset(self, class_name, property_name, results): + for asset in results["assets"]: + if property_name is not None: + if ( + "assetType" in asset + and asset["assetType"] == class_name + and property_name in asset + ): + return asset + else: + if "assetType" in asset and asset["assetType"] == class_name: + return asset + return False + + def parse_class(self, class_name, load_flow_results, results): + for key, value in load_flow_results[class_name].items(): + data = key.split("__") + if len(data) == 3: + ppty = f"{data[1]}__{data[2]}" + else: + ppty = data[1] + asset = self.check_asset(class_name, ppty, results) + if not asset: + asset = {"assetType": class_name, ppty: [value]} + results["assets"].append(asset) + else: + asset[ppty].append(value) + return results + + def remap(self, load_flow_results, fed_name, fed_uuid, cosim_uuid, circuit, current_time): + results = {} + results["cosimUUID"] = cosim_uuid + results["federateUUID"] = fed_uuid + results["timeStep"] = current_time + results["assets"] = [] + for key in load_flow_results: + results = self.parse_class(key, load_flow_results, results) + return results + + +globals()["JSONwriter"] = JsonWriter diff --git a/src/pydss/api/src/app/pydss.py b/src/pydss/api/src/app/pydss.py index 1141b1f7..24e31acf 100644 --- a/src/pydss/api/src/app/pydss.py +++ b/src/pydss/api/src/app/pydss.py @@ -5,21 +5,19 @@ from loguru import logger from pydss.simulation_input_models import SimulationSettingsModel -from pydss.api.src.app.JSON_writer import JSONwriter -from pydss.dssInstance import OpenDSS +from pydss.api.src.app.json_writer import JsonWriter +from pydss.dss_instance import OpenDSS class PyDSS: - commands = { - "run": None - } + commands = {"run": None} def __init__(self, event=None, queue=None, parameters=None): self.initalized = False self.uuid = current_process().name - ''' TODO: work on logging.yaml file''' + """ TODO: work on logging.yaml file""" logger.info("{} - initialized ".format({self.uuid})) @@ -27,27 +25,30 @@ def __init__(self, event=None, queue=None, parameters=None): self.queue = queue try: - settings = SimulationSettingsModel(**parameters['parameters']) + settings = SimulationSettingsModel(**parameters["parameters"]) self.pydss_obj = OpenDSS(settings) - export_path = os.path.join(self.pydss_obj._dssPath['Export'], settings.project.active_scenario) - Steps, sTime, eTime = self.pydss_obj._dssSolver.SimulationSteps() - self.a_writer = JSONwriter(export_path, Steps) + export_path = os.path.join( + self.pydss_obj._dssPath["Export"], settings.project.active_scenario + ) + steps, start_time, end_time = self.pydss_obj._dssSolver.SimulationSteps() + self.a_writer = JsonWriter(export_path, steps) self.initalized = True - except: - result = {"Status": 500, "Message": f"Failed to create a pydss instance"} + except Exception: + result = {"Status": 500, "Message": "Failed to create a pydss instance"} self.queue.put(result) return - # self.RunSimulation() + # self.run_simulation() logger.info("{} - pydss dispatched".format(self.uuid)) result = { "Status": 200, "Message": "Pydss {} successfully initialized.".format(self.uuid), - "UUID": self.uuid + "UUID": self.uuid, } - if self.queue != None: self.queue.put(result) + if self.queue is not None: + self.queue.put(result) self.run_process() @@ -56,13 +57,10 @@ def run_process(self): while not self.shutdownevent.is_set(): try: task = self.queue.get() - if task == 'END': + if task == "END": break elif "parameters" not in task: - result = { - "Status": 500, - "Message": "No parameters passed" - } + result = {"Status": 500, "Message": "No parameters passed"} else: command = task["command"] parameters = task["parameters"] @@ -72,7 +70,10 @@ def run_process(self): result = {"Status": status, "Message": msg, "UUID": self.uuid} else: logger.info(f"{command} is not a valid pydss command") - result = {"Status": 500, "Message": f"{command} is not a valid pydss command"} + result = { + "Status": 500, + "Message": f"{command} is not a valid pydss command", + } self.queue.put(result) except Empty: @@ -84,14 +85,14 @@ def run_process(self): def close_instance(self): del self.pydss_obj - logger.info(f'Pydss case {self.uuid} closed.') + logger.info(f"Pydss case {self.uuid} closed.") def run(self, params): if self.initalized: try: - Steps, sTime, eTime = self.pydss_obj._dssSolver.SimulationSteps() - for i in range(Steps): - results = self.pydss_obj.RunStep(i) + steps, start_time, end_time = self.pydss_obj._dssSolver.SimulationSteps() + for i in range(steps): + results = self.pydss_obj.run_step(i) restructured_results = {} for k, val in results.items(): if "." not in k: @@ -107,26 +108,32 @@ def run(self, params): self.pydss_obj._Options["Helics"]["Federate name"], self.pydss_obj._dssSolver.GetTotalSeconds(), restructured_results, - i + i, ) self.initalized = False - return 200, f"Simulation complete..." + return 200, "Simulation complete..." except Exception as e: self.initalized = False - return 500, f"Simulation crashed at at simulation time step: {self.pydss_obj._dssSolver.GetDateTime()}, {e}" + return ( + 500, + f"Simulation crashed at at simulation time step: {self.pydss_obj._dssSolver.GetDateTime()}, {e}", + ) else: - return 500, f"No project initialized. Load a project first using the 'init' command" + return 500, "No project initialized. Load a project first using the 'init' command" - def registerPubSubs(self, params): + def register_pub_subs(self, params): subs = params["Subscriptions"] pubs = params["Publications"] - self.pydss_obj._HI.registerPubSubTags(pubs, subs) - return 200, f"Publications and subscriptions have been registered; Federate has entered execution mode" + self.pydss_obj._HI.register_pub_sub_tags(pubs, subs) + return ( + 200, + "Publications and subscriptions have been registered; Federate has entered execution mode", + ) -if __name__ == '__main__': - FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +if __name__ == "__main__": + FORMAT = "%(asctime)s - %(levelname)s - %(message)s" logger.basicConfig(level=logger.INFO, format=FORMAT) a = PyDSS() del a diff --git a/src/pydss/api/src/app/Tester.py b/src/pydss/api/src/app/tester.py similarity index 61% rename from src/pydss/api/src/app/Tester.py rename to src/pydss/api/src/app/tester.py index 21f5c454..121bc753 100644 --- a/src/pydss/api/src/app/Tester.py +++ b/src/pydss/api/src/app/tester.py @@ -1,4 +1,3 @@ - from pathlib import Path import time import os @@ -6,25 +5,25 @@ from loguru import logger from pydss.simulation_input_models import load_simulation_settings -from pydss.api.src.app.DataWriter import DataWriter -from pydss.dssInstance import OpenDSS +from pydss.api.src.app.data_writer import DataWriter +from pydss.dss_instance import OpenDSS def run_test(tomlpath): try: settings = load_simulation_settings(Path(tomlpath)) - except Exception as e: - logger.error(f"Invalid simulation settings passed, {e}") + except Exception as error: + logger.error(f"Invalid simulation settings passed, {error}") return pydss_obj = OpenDSS(settings) - export_path = os.path.join(pydss_obj._dssPath['Export'], settings.project.active_scenario) - Steps, sTime, eTime = pydss_obj._dssSolver.SimulationSteps() - writer = DataWriter(export_path, format="json", columnLength=Steps) + export_path = os.path.join(pydss_obj._dssPath["Export"], settings.project.active_scenario) + steps, start_time, end_time = pydss_obj._dssSolver.SimulationSteps() + writer = DataWriter(export_path, format="json", column_length=steps) - st = time.time() - for i in range(Steps): - results = pydss_obj.RunStep(i) + start_timestamp = time.time() + for step in range(steps): + results = pydss_obj.RunStep(step) restructured_results = {} for k, val in results.items(): if "." not in k: @@ -41,6 +40,6 @@ def run_test(tomlpath): pydss_obj._Options["Helics"]["Federate name"], pydss_obj._dssSolver.GetTotalSeconds(), restructured_results, - i + step, ) - logger.debug("{} seconds".format(time.time() - st)) + logger.debug("{} seconds".format(time.time() - start_timestamp)) diff --git a/src/pydss/api/src/web/create_schema.py b/src/pydss/api/src/web/create_schema.py index 12164ea3..f677eccf 100644 --- a/src/pydss/api/src/web/create_schema.py +++ b/src/pydss/api/src/web/create_schema.py @@ -1,10 +1,11 @@ import requests import json -def getJSONschema(ip, port, path): + +def get_json_schema(ip, port, path): base_url = f"http://{ip}:{port}" url = base_url + "/docs/swagger.json" response = requests.get(url) - with open(path, 'w') as outfile: + with open(path, "w") as outfile: json.dump(response.json(), outfile) diff --git a/src/pydss/api/src/web/handler.py b/src/pydss/api/src/web/handler.py index 15d5df4b..c107df6e 100644 --- a/src/pydss/api/src/web/handler.py +++ b/src/pydss/api/src/web/handler.py @@ -14,10 +14,10 @@ class Handler: - """ Handlers for web server. """ + """Handlers for web server.""" def __init__(self): - """ Constructor for pydss handler. """ + """Constructor for pydss handler.""" logger.info("Initializing Handler ....") @@ -73,11 +73,9 @@ async def get_pydss_project_info(self, request, path): logger.info(f"Exploring {path} for valid projects") if not os.path.exists(path): - return web.json_response({ - "Status": 404, - "Message": f"Provided path does not exist", - "UUID": None - }) + return web.json_response( + {"Status": 404, "Message": "Provided path does not exist", "UUID": None} + ) subfolders = [f.path for f in os.scandir(path) if f.is_dir()] projects = {} @@ -85,19 +83,27 @@ async def get_pydss_project_info(self, request, path): try: pydss_project = PyDssProject.load_project(folder) projects[pydss_project._name] = [x.name for x in pydss_project.scenarios] - except: + except Exception: pass n = len(projects) if n > 0: - return web.json_response({"Status": 200, - "Message": f"{n} valid projects found", - "UUID": None, - "Data": projects}) + return web.json_response( + { + "Status": 200, + "Message": f"{n} valid projects found", + "UUID": None, + "Data": projects, + } + ) else: - web.json_response({"Status": 404, - "Message": f"No valid pydss project in provided base path", - "UUID": None}) + web.json_response( + { + "Status": 404, + "Message": "No valid pydss project in provided base path", + "UUID": None, + } + ) async def post_pydss_create(self, request): """ @@ -156,14 +162,14 @@ async def post_pydss_create(self, request): """ from zipfile import ZipFile - examples_path = os.path.join("C:/Users/alatif/Desktop/Pydss_2.0/pydss/", 'examples') + + examples_path = os.path.join("C:/Users/alatif/Desktop/Pydss_2.0/pydss/", "examples") unzip_path = os.path.join(examples_path, "uploaded_opendss_project") zip_path = os.path.join(examples_path, "uploaded_opendss_project.zip") data = None - with open(zip_path, 'wb') as fd: + with open(zip_path, "wb") as fd: while True: - chunk = await request.content.read(1024) if data is None: data = chunk @@ -175,35 +181,41 @@ async def post_pydss_create(self, request): data = bytestream_decode(data) os.makedirs(unzip_path, exist_ok=True) - with ZipFile(zip_path, 'r') as zipObj: - zipObj.extractall(path=unzip_path) + with ZipFile(zip_path, "r") as zip_object: + zip_object.extractall(path=unzip_path) - controller_types = [ControllerType(x) for x in data['controller_types'].split(",")] + controller_types = [ControllerType(x) for x in data["controller_types"].split(",")] scenarios = [ PyDssScenario( name=x.strip(), controller_types=controller_types, - ) for x in data['scenarios'].split(",") + ) + for x in data["scenarios"].split(",") ] - PyDssProject.create_project(path=examples_path, name=data['project'], scenarios=scenarios, - opendss_project_folder=unzip_path, master_dss_file=data['master_file']) + PyDssProject.create_project( + path=examples_path, + name=data["project"], + scenarios=scenarios, + opendss_project_folder=unzip_path, + master_dss_file=data["master_file"], + ) try: shutil.rmtree(unzip_path) if os.path.exists(zip_path): os.remove(zip_path) - except: - return web.json_response({ - 'Status': 403, - 'Message': 'User does not have access to delete folders', - 'UUID': None - }) + except Exception: + return web.json_response( + { + "Status": 403, + "Message": "User does not have access to delete folders", + "UUID": None, + } + ) - result = {'Status': 200, - 'Message': 'Pydss project created', - 'UUID': None} + result = {"Status": 200, "Message": "Pydss project created", "UUID": None} # name, scenarios, simulation_config = None, options = None, # simulation_file = SIMULATION_SETTINGS_FILENAME, opendss_project_folder = None, @@ -219,68 +231,68 @@ async def get_pydss_project(self, request): async def post_pydss(self, request): """ - --- - summary: Creates an instance of pydss and runs the simulation - tags: - - Simulation - requestBody: - content: - application/json: - schema: - type: object - properties: - parameters: - type: object - examples: - Example 1: - value: - parameters: - Start Year: 2017 - Start Day: 1 - Start Time (min): 0 - End Day: 1 - End Time (min): 1439 - Date offset: 0 - Step resolution (sec): 900 - Max Control Iterations: 50 - Error tolerance: 0.001 - Control mode: Static - Simulation Type: QSTS - Project Path: "C:/Users/alatif/Desktop/Pydss_2.0/pydss/examples" - Active Project: custom_contols - Active Scenario: base_case - DSS File: Master_Spohn_existing_VV.dss - Co-simulation Mode: false - Log Results: false - Export Data Tables: true - Export Data In Memory: true - Federate name: Pydss_x - responses: - '200': - description: Successfully retrieved project information - content: - application/json: - schema: - type: object - examples: - get_instance_status: - value: - Status: 200 - Message: Starting a pydss instance - UUID: 96c21e00-cd3c-4943-a914-14451f5f7ab6 - '500': - description: Provided path does not exist - content: - application/json: - schema: - type: object - examples: - get_instance_status: + --- + summary: Creates an instance of pydss and runs the simulation + tags: + - Simulation + requestBody: + content: + application/json: + schema: + type: object + properties: + parameters: + type: object + examples: + Example 1: value: - Status: 500 - Message: Failed to create a pydss instance - UUID: None - """ + parameters: + Start Year: 2017 + Start Day: 1 + Start Time (min): 0 + End Day: 1 + End Time (min): 1439 + Date offset: 0 + Step resolution (sec): 900 + Max Control Iterations: 50 + Error tolerance: 0.001 + Control mode: Static + Simulation Type: QSTS + Project Path: "C:/Users/alatif/Desktop/Pydss_2.0/pydss/examples" + Active Project: custom_contols + Active Scenario: base_case + DSS File: Master_Spohn_existing_VV.dss + Co-simulation Mode: false + Log Results: false + Export Data Tables: true + Export Data In Memory: true + Federate name: Pydss_x + responses: + '200': + description: Successfully retrieved project information + content: + application/json: + schema: + type: object + examples: + get_instance_status: + value: + Status: 200 + Message: Starting a pydss instance + UUID: 96c21e00-cd3c-4943-a914-14451f5f7ab6 + '500': + description: Provided path does not exist + content: + application/json: + schema: + type: object + examples: + get_instance_status: + value: + Status: 500 + Message: Failed to create a pydss instance + UUID: None + """ data = await request.json() logger.info(f"Running command :{data}") @@ -298,15 +310,14 @@ async def post_pydss(self, request): # Start process for pydss p.start() # Return a message to webclient - result = {'Status': 200, - 'Message': 'Starting a pydss instance', - 'UUID': pydss_uuid} + result = {"Status": 200, "Message": "Starting a pydss instance", "UUID": pydss_uuid} return web.json_response(result) async def put_pydss(self, request): - - """ Running pydss app"""""" + ( + """ Running pydss app""" + """ --- summary: Run a command on an active instance of Pydss tags: @@ -371,6 +382,7 @@ async def put_pydss(self, request): Message: Provided UUID is not valid pydss instance id UUID: None """ + ) data = await request.json() logger.info(f"Running command :{data}") @@ -378,31 +390,33 @@ async def put_pydss(self, request): if "command" not in data or "parameters" not in data: msg = "Please provide a command and parameters" logger.error(msg) - return web.json_response({"Status": 401, - "Message": msg, - "UUID": None}) + return web.json_response({"Status": 401, "Message": msg, "UUID": None}) pydss_uuid = await self._get_uuid(data=data) if pydss_uuid: logger.info(f"Running command {data['command']} on pydss instance {pydss_uuid}") - pydss_t = self.loop.run_in_executor(self.pool, self._post_put_background_task, pydss_uuid) + pydss_t = self.loop.run_in_executor( + self.pool, self._post_put_background_task, pydss_uuid + ) pydss_t.add_done_callback(self._post_put_callback) - self.pydss_instances[pydss_uuid]['queue'].put(data) + self.pydss_instances[pydss_uuid]["queue"].put(data) - result = {"Status": 200, - "Message": f"{data['command']} command submitted, awaiting response ", - "UUID": pydss_uuid - } + result = { + "Status": 200, + "Message": f"{data['command']} command submitted, awaiting response ", + "UUID": pydss_uuid, + } return web.json_response(result) else: logger.error(f"UUID={pydss_uuid} not found.") - result = {"Status": 403, - "Message": f"{pydss_uuid} is not valid pydss instance id ", - "UUID": pydss_uuid - } + result = { + "Status": 403, + "Message": f"{pydss_uuid} is not valid pydss instance id ", + "UUID": pydss_uuid, + } return web.json_response(result) async def delete_pydss(self, request): @@ -456,26 +470,26 @@ async def delete_pydss(self, request): logger.error(f"UUID={pydss_uuid} not found.") try: - pydss_t = self.loop.run_in_executor(self.pool, self._delete_background_task, pydss_uuid) + pydss_t = self.loop.run_in_executor( + self.pool, self._delete_background_task, pydss_uuid + ) pydss_t.add_done_callback(self._delete_callback) self.pydss_instances[pydss_uuid]["queue"].put("END") - return web.json_response({ - "Status": 200, - "Message": f"Successfully deleted a pydss instance", - "UUID": pydss_uuid - }) - except Exception as e: - + return web.json_response( + { + "Status": 200, + "Message": "Successfully deleted a pydss instance", + "UUID": pydss_uuid, + } + ) + except Exception: logger.error(f"Error closing pydss instance {pydss_uuid}") else: - - return web.json_response({ - "Status": 403, - "Message": f"Error closing pydss instance", - "UUID": None - }) + return web.json_response( + {"Status": 403, "Message": "Error closing pydss instance", "UUID": None} + ) async def get_instance_uuids(self, request): """ @@ -511,17 +525,21 @@ async def get_instance_uuids(self, request): """ uuids = [str(k) for k in self.pydss_instances.keys()] if len(uuids) > 0: - return web.json_response({ - "Status": 200, - "Message": f"{len(uuids)} instances currently running", - "Instances": uuids - }) + return web.json_response( + { + "Status": 200, + "Message": f"{len(uuids)} instances currently running", + "Instances": uuids, + } + ) else: - return web.json_response({ - "Status": 204, - "Message": "No pydss instance currently running", - "Instances": uuids - }) + return web.json_response( + { + "Status": 204, + "Message": "No pydss instance currently running", + "Instances": uuids, + } + ) async def get_instance_status(self, request, uuid: str): """ @@ -571,11 +589,7 @@ async def get_instance_status(self, request, uuid: str): status = "200" msg = "Pydss instance with the provided UUID is currently running" - return web.json_response({ - "Status": status, - "Message": msg, - "UUID": uuid - }) + return web.json_response({"Status": status, "Message": msg, "UUID": uuid}) def _post_put_background_task(self, pydss_uuid): @@ -591,7 +605,7 @@ async def _get_uuid(self, data): if "UUID" not in data: return None - pydss_uuid = data['UUID'] + pydss_uuid = data["UUID"] if pydss_uuid not in self.pydss_instances.keys(): return None @@ -604,11 +618,7 @@ def _delete_background_task(self, pydss_uuid): del self.pydss_instances[pydss_uuid] - return { - "Status": "Success", - "Message": "Pydss instance closed", - "UUID": pydss_uuid - } + return {"Status": "Success", "Message": "Pydss instance closed", "UUID": pydss_uuid} def _delete_callback(self, return_value): - logger.info(f"{return_value.result()}") \ No newline at end of file + logger.info(f"{return_value.result()}") diff --git a/src/pydss/api/src/web/parser.py b/src/pydss/api/src/web/parser.py index 85e847d9..531f3e9d 100644 --- a/src/pydss/api/src/web/parser.py +++ b/src/pydss/api/src/web/parser.py @@ -17,53 +17,96 @@ ``a_1=something`` as a list, you would use a ``list_char='_'``. """ -import pydss.defaults as defaultSettings +import pydss.defaults as default_settings from pydss.utils.utils import load_data -import ast import os -defaultSettings.__file__ -__all__ = ['variable_decode'] +default_settings.__file__ +__all__ = ["variable_decode"] master_dict = { - "Project" : ["Start Year", "Start Day", "Start Time (min)", "End Day", "End Time (min)", "Date offset", - "Step resolution (sec)", "Max Control Iterations", "Error tolerance", "Control mode", - "Disable pydss controllers", "Simulation Type", "Project Path", "Active Project", "Active Scenario", - "DSS File", "DSS File Absolute Path", "Return Results"], - "Exports" : ["Export Mode", "Export Style", "Export Format", "Export Compression", "Export Iteration Order", - "Export Elements", "Export Data Tables", "Export Data In Memory", "HDF Max Chunk Bytes", - "Export Event Log", "Log Results"], - "Frequency" : ["Enable frequency sweep", "Fundamental frequency", "Start frequency", "End frequency", - "frequency increment", "Neglect shunt admittance", "Percentage load in series"], - "Helics" : ["Co-simulation Mode", "Federate name", "Time delta", "Core type", "Uninterruptible", - "Helics logging level", "Iterative Mode", 'Max co-iterations', "Error tolerance", 'Broker', - "Broker port"], - "Logging" : ["Logging Level", "Log to external file", "Display on screen", "Clear old log file"], - "MonteCarlo" : ["Number of Monte Carlo scenarios"], - "Plots" : ["Create dynamic plots", "Open plots in browser"], + "Project": [ + "Start Year", + "Start Day", + "Start Time (min)", + "End Day", + "End Time (min)", + "Date offset", + "Step resolution (sec)", + "Max Control Iterations", + "Error tolerance", + "Control mode", + "Simulation Type", + "Project Path", + "Active Project", + "Active Scenario", + "DSS File", + "DSS File Absolute Path", + "Return Results", + ], + "Exports": [ + "Export Mode", + "Export Style", + "Export Format", + "Export Compression", + "Export Iteration Order", + "Export Elements", + "Export Data Tables", + "Export Data In Memory", + "HDF Max Chunk Bytes", + "Export Event Log", + "Log Results", + ], + "Frequency": [ + "Enable frequency sweep", + "Fundamental frequency", + "Start frequency", + "End frequency", + "frequency increment", + "Neglect shunt admittance", + "Percentage load in series", + ], + "Helics": [ + "Co-simulation Mode", + "Federate name", + "Time delta", + "Core type", + "Uninterruptible", + "Helics logging level", + "Iterative Mode", + "Max co-iterations", + "Error tolerance", + "Broker", + "Broker port", + ], + "Logging": ["Logging Level", "Log to external file", "Display on screen", "Clear old log file"], + "MonteCarlo": ["Number of Monte Carlo scenarios"], + "Plots": ["Create dynamic plots", "Open plots in browser"], } + def bytestream_decode(s): lines = s.splitlines() - filter = b'Content-Disposition: form-data; name=' + filter = b"Content-Disposition: form-data; name=" result = {} - for i, l in enumerate(lines): - if l.startswith(filter): - l = l.replace(filter, b"").replace(b'"', b'').decode('ascii') - if '; filename=' in l: - data = l.replace("; filename=", ":").split(":") + for i, line in enumerate(lines): + if line.startswith(filter): + line = line.replace(filter, b"").replace(b'"', b"").decode("ascii") + if "; filename=" in line: + data = line.replace("; filename=", ":").split(":") result[data[0]] = data[1] else: - val = lines[i + 2].decode('ascii') - result[l] = val + val = lines[i + 2].decode("ascii") + result[line] = val return result + # TODO: It does not appear that this function is used. Can it be deleted? # If not, it needs to be updated. def restructure_dictionary(d): global master_dict pydss_settings = {} - path = defaultSettings.__path__._path[0] + path = default_settings.__path__._path[0] complete_path = os.path.join(path, "simulation.toml") defaults = load_data(complete_path) @@ -72,17 +115,17 @@ def restructure_dictionary(d): if k in valid_entries: if key not in pydss_settings: pydss_settings[key] = {} - if i == "False" or i == "false" or i == False: + if i == "False" or i == "false" or not i: i = False - elif i == "True" or i == "true" or i == True: + elif i == "True" or i == "true" or i: i = True else: try: i = int(i) - except: + except (TypeError, ValueError): try: i = float(i) - except: + except (TypeError, ValueError): pass pydss_settings[key][k] = i @@ -90,17 +133,12 @@ def restructure_dictionary(d): if key in pydss_settings: defaults[key].update(pydss_settings[key]) - defaults["Project"]["Scenarios"] =[ - { - "name": d["Active Scenario"], - "post_process_infos": [] - } - ] + defaults["Project"]["Scenarios"] = [{"name": d["Active Scenario"], "post_process_infos": []}] return defaults -def variable_decode(d, dict_char='.', list_char='-'): +def variable_decode(d, dict_char=".", list_char="-"): """ Decode the flat dictionary d into a nested structure. """ diff --git a/src/pydss/apps/data_viewer.py b/src/pydss/apps/data_viewer.py index c02e63a7..9a428511 100644 --- a/src/pydss/apps/data_viewer.py +++ b/src/pydss/apps/data_viewer.py @@ -3,15 +3,13 @@ import os import re import sys -from collections import defaultdict +from importlib.util import find_spec import ipywidgets as widgets import pandas as pd from IPython.display import display -try: - import plotly -except ImportError: +if find_spec("plotly") is None: print("plotly is required to run the DataViewer. Please run 'pip install plotly'") sys.exit(1) diff --git a/src/pydss/cli/add_post_process.py b/src/pydss/cli/add_post_process.py index 60a207f0..39e3ff89 100644 --- a/src/pydss/cli/add_post_process.py +++ b/src/pydss/cli/add_post_process.py @@ -1,17 +1,14 @@ """CLI to create a new pydss project""" - -import click - from pydss.pydss_project import PyDssProject -@click.argument("config-file") -@click.argument("script") -@click.argument("scenario-name") -@click.argument("project-path") -@click.command() -def add_post_process(project_path, scenario_name, script, config_file): +def add_post_process( + project_path: str, + scenario_name: str, + script: str, + config_file: str, +): """Add post-process script to pydss scenario.""" project = PyDssProject.load_project(project_path) scenario = project.get_scenario(scenario_name) diff --git a/src/pydss/cli/add_scenario.py b/src/pydss/cli/add_scenario.py index 5b9167a6..54bac5a3 100644 --- a/src/pydss/cli/add_scenario.py +++ b/src/pydss/cli/add_scenario.py @@ -1,58 +1,64 @@ from pathlib import Path import toml -import click -import os +import typer from pydss.simulation_input_models import MappedControllers from pydss.pydss_project import PyDssScenario from pydss.common import CONTROLLER_TYPES, ControllerType -def build_scenario(project_path:str, scenario_name:str, controller_mapping:str): + +def build_scenario(project_path: str, scenario_name: str, controller_mapping: str): project_path = Path(project_path) - controller_mapping_path = Path(controller_mapping) + controller_mapping_path = Path(controller_mapping) assert project_path.exists(), "Provided project path does not exist" assert (project_path / "Scenarios").exists(), "provided project is not a valid pydss project" assert controller_mapping_path.exists(), "rovided controller_mapping file does not exist" - assert controller_mapping_path.suffix.lower() == ".toml", "controller_mapping should be a TOML file" - + assert controller_mapping_path.suffix.lower() == ".toml", ( + "controller_mapping should be a TOML file" + ) + controller_map = toml.load(controller_mapping_path) mapped_controllers = MappedControllers(**controller_map) acceptable_controller_types = CONTROLLER_TYPES controllers = {} for controller in mapped_controllers.mapping: settings_path_obj = Path(controller.controller_file) - assert controller.controller_type in ControllerType, \ - f"{controller.controller_type} is not a valid contoller. Options are {acceptable_controller_types}" - assert settings_path_obj.exists(), \ + assert controller.controller_type in ControllerType, ( + f"{controller.controller_type} is not a valid contoller. Options are {acceptable_controller_types}" + ) + assert settings_path_obj.exists(), ( f"file for controller type {controller.controller_type} does not exist" + ) controller_data = toml.load(settings_path_obj) if controller_data: if controller.controller_type in controllers: - msg= f"Multiple keys files for the same controller type {controller.controller_type}." \ - "Each controller type can only be attached to a single file." + msg = ( + f"Multiple keys files for the same controller type {controller.controller_type}." + "Each controller type can only be attached to a single file." + ) raise ValueError(msg) controllers[controller.controller_type] = toml.load(settings_path_obj) scenario_dir = project_path / "Scenarios" / scenario_name - scenario_obj = PyDssScenario( - [scenario_name], controllers=controllers, export_modes=None - ) + scenario_obj = PyDssScenario([scenario_name], controllers=controllers, export_modes=None) scenario_obj.serialize(str(scenario_dir)) -@click.argument("project-path", type=click.Path(exists=True)) -@click.option( - "-s", "--scenario_name", - required=True, - help="name of the new scenario", -) -@click.option( - "-c", "--controller-mapping", - required=True, - default=None, - type=click.Path(exists=True), - help="JSON file that maps controller type to controller definition files", -) -@click.command() -def add_scenario(project_path:str, scenario_name:str, controller_mapping:str): + +def add_scenario( + project_path: str = typer.Argument(..., exists=True), + scenario_name: str = typer.Option( + ..., + "-s", + "--scenario_name", + "--scenario-name", + help="name of the new scenario", + ), + controller_mapping: str = typer.Option( + ..., + "-c", + "--controller-mapping", + exists=True, + help="JSON file that maps controller type to controller definition files", + ), +): """Add a new scenario to an existing project""" build_scenario(project_path, scenario_name, controller_mapping) - diff --git a/src/pydss/cli/controllers.py b/src/pydss/cli/controllers.py index 3479916a..5df9a24f 100644 --- a/src/pydss/cli/controllers.py +++ b/src/pydss/cli/controllers.py @@ -5,22 +5,18 @@ import sys import os -import click +import typer from pydss.common import CONTROLLER_TYPES from pydss.utils.utils import load_data from pydss.registry import Registry -@click.group() -def controllers(): - """Manage registered pydss controllers.""" +controllers = typer.Typer(help="Manage registered pydss controllers.") -@click.argument("filename") -@click.argument("controller_type") -@click.command() -def register(controller_type, filename): +@controllers.command() +def register(controller_type: str, filename: str): """Register a controller in the local registry.""" if controller_type not in CONTROLLER_TYPES: print(f"controller_type must be one of {CONTROLLER_TYPES}") @@ -36,29 +32,21 @@ def register(controller_type, filename): print(f"Registered {controller_type} {name}") -@click.argument("name") -@click.argument("controller_type") -@click.command() -def unregister(controller_type, name): +@controllers.command() +def unregister(controller_type: str, name: str): """Unregister a controller.""" Registry().unregister_controller(controller_type, name) print(f"Unregistered {controller_type} {name}") -@click.command() +@controllers.command() def show(): """Show the registered controllers.""" Registry().show_controllers() -@click.command() +@controllers.command() def reset_defaults(): """Reset defaults.""" Registry().reset_defaults(controllers_only=True) print("Reset pydss defaults") - - -controllers.add_command(register) -controllers.add_command(unregister) -controllers.add_command(show) -controllers.add_command(reset_defaults) diff --git a/src/pydss/cli/convert.py b/src/pydss/cli/convert.py index 2fe8981f..796660e8 100644 --- a/src/pydss/cli/convert.py +++ b/src/pydss/cli/convert.py @@ -2,43 +2,41 @@ import os -import click +import typer from pydss.config_data import convert_config_data_to_toml from pydss.export_list_reader import ExportListReader from pydss.utils.utils import dump_data -@click.group("convert") -def convert(): - """Convert input files to new formats.""" +convert = typer.Typer(help="Convert input files to new formats.") -@click.argument( - "filenames", - nargs=-1, -) -@click.option( - "-n", "--name", - help="new filename; default is to use the basename of the XLSX file", -) -@click.command() -def excel_to_toml(filenames, name=None): +@convert.command() +def excel_to_toml( + filenames: list[str] = typer.Argument(...), + name: str | None = typer.Option( + None, + "-n", + "--name", + help="new filename; default is to use the basename of the XLSX file", + ), +): """Convert an Excel configuration file to TOML.""" for filename in filenames: convert_config_data_to_toml(filename, name) -@click.argument( - "filenames", - nargs=-1, -) -@click.option( - "-n", "--name", - help="new filename; default is Exports.toml", -) -@click.command() -def simulation_file(filenames, name=None): +@convert.command() +def simulation_file( + filenames: list[str] = typer.Argument(...), + name: str | None = typer.Option( + None, + "-n", + "--name", + help="new filename; default is Exports.toml", + ), +): """Convert a legacy simulation TOML file to the new format.""" for filename in filenames: dirname = os.path.dirname(filename) @@ -49,7 +47,3 @@ def simulation_file(filenames, name=None): reader = ExportListReader(filename) dump_data(reader.serialize(), new_filename) print(f"Converted {filename} to {new_filename}") - - -convert.add_command(excel_to_toml) -convert.add_command(simulation_file) diff --git a/src/pydss/cli/create_project.py b/src/pydss/cli/create_project.py index eece6b69..42659328 100644 --- a/src/pydss/cli/create_project.py +++ b/src/pydss/cli/create_project.py @@ -1,6 +1,6 @@ """CLI to create a new pydss project""" -import click +import typer import ast import sys @@ -9,80 +9,82 @@ from pydss.pydss_project import PyDssProject, PyDssScenario, ControllerType, ExportMode -@click.option( - "-P", "--path", - required=True, - help="path in which to create project", -) -@click.option( - "-p", "--project", - required=True, - help="project name", -) -@click.option( - "-s", "--scenarios", - required=True, - help="comma-delimited scenario names", -) -@click.option( - "-f", "--simulation-file", - required=False, - show_default=True, - default="simulation.toml", - help="simulation file name", -) -@click.option( - "-F", "--opendss-project-folder", - default=None, - required=False, - type=click.Path(exists=True), - help="simulation file name", -) -@click.option( - "-m", "--master-dss-file", - required=False, - show_default=True, - default=None, - help="simulation file name", -) -@click.option( - "-S", "--simulation-config", - default=None, - type=click.Path(exists=True), - help="simulation configuration settings", -) -@click.option( - "-c", "--controller-types", - default=None, - help="comma-delimited list of controller types", -) -@click.option( - "-e", "--export-modes", - default=None, - help="comma-delimited list of export modes", -) -@click.option( - "-o", "--options", - help="dict-formatted simulation settings that override the config file. " \ - "Example: pydss run ./project --options \"{\\\"Simulation Type\\\": \\\"QSTS\\\"}\"", -) -@click.option( - "--force", - is_flag=True, - default=False, - show_default=True, - help="Overwrite directory if it already exists.", -) -@click.command() -def create_project(path=None, project=None, scenarios=None, simulation_file=None, simulation_config=None, - controller_types=None, export_modes=None, options=None, - opendss_project_folder=None, master_dss_file=None, force=False): +def create_project( + path: str = typer.Option( + ..., + "-P", + "--path", + help="path in which to create project", + ), + project: str = typer.Option( + ..., + "-p", + "--project", + help="project name", + ), + scenarios: str = typer.Option( + ..., + "-s", + "--scenarios", + help="comma-delimited scenario names", + ), + simulation_file: str = typer.Option( + "simulation.toml", + "-f", + "--simulation-file", + help="simulation file name", + ), + opendss_project_folder: str | None = typer.Option( + None, + "-F", + "--opendss-project-folder", + exists=True, + help="simulation file name", + ), + master_dss_file: str | None = typer.Option( + None, + "-m", + "--master-dss-file", + help="simulation file name", + ), + simulation_config: str | None = typer.Option( + None, + "-S", + "--simulation-config", + exists=True, + help="simulation configuration settings", + ), + controller_types: str | None = typer.Option( + None, + "-c", + "--controller-types", + help="comma-delimited list of controller types", + ), + export_modes: str | None = typer.Option( + None, + "-e", + "--export-modes", + help="comma-delimited list of export modes", + ), + options: str | None = typer.Option( + None, + "-o", + "--options", + help="dict-formatted simulation settings that override the config file. " + 'Example: pydss run ./project --options "{\\"Simulation Type\\": \\"QSTS\\"}"', + ), + force: bool = typer.Option( + False, + "--force", + help="Overwrite directory if it already exists.", + ), +): """Create pydss project.""" if controller_types is not None: controller_types = [ControllerType(x) for x in controller_types.split(",")] if export_modes is not None: export_modes = [ExportMode(x) for x in export_modes.split(",")] - + if options is not None: options = ast.literal_eval(options) if not isinstance(options, dict): @@ -94,8 +96,8 @@ def create_project(path=None, project=None, scenarios=None, simulation_file=None name=x.strip(), controller_types=controller_types, export_modes=export_modes, - - ) for x in scenarios.split(",") + ) + for x in scenarios.split(",") ] PyDssProject.create_project( path, @@ -105,6 +107,6 @@ def create_project(path=None, project=None, scenarios=None, simulation_file=None options=options, simulation_file=simulation_file, master_dss_file=master_dss_file, - opendss_project_folder = opendss_project_folder, + opendss_project_folder=opendss_project_folder, force=force, ) diff --git a/src/pydss/cli/edit_scenario.py b/src/pydss/cli/edit_scenario.py index e592dc60..86832a38 100644 --- a/src/pydss/cli/edit_scenario.py +++ b/src/pydss/cli/edit_scenario.py @@ -1,46 +1,57 @@ """CLI to edit an existing project or scenario""" -import click +import typer from pydss.common import CONTROLLER_TYPES from pydss.pydss_project import update_pydss_controllers -@click.option( - "-p", "--project-path", - required=True, - help="project path", -) -@click.option( - "-s", "--scenario", - required=True, - help="Project name (should exist)", -) -@click.group() -def edit_scenario(project_path=None, scenario=None): +edit_scenario = typer.Typer(help="Edit scenario in a pydss project.") + + +@edit_scenario.callback() +def main( + project_path: str = typer.Option( + ..., + "-p", + "--project-path", + help="project path", + ), + scenario: str = typer.Option( + ..., + "-s", + "--scenario", + help="Project name (should exist)", + ), +): """Edit scenario in a pydss project.""" -@click.option( - "-c", "--controller", - required=True, - help="controller name", -) -@click.option( - "-f", "--dss-file", - required=True, - help="OpenDSS file containing elements", -) -@click.option( - "-t", "--controller-type", - required=True, - type=click.Choice(CONTROLLER_TYPES), - help="controller type", -) -@click.command() -@click.pass_context -def update_controllers(ctx, controller_type=None, controller=None, dss_file=None): +@edit_scenario.command() +def update_controllers( + ctx: typer.Context, + controller: str = typer.Option( + ..., + "-c", + "--controller", + help="controller name", + ), + dss_file: str = typer.Option( + ..., + "-f", + "--dss-file", + help="OpenDSS file containing elements", + ), + controller_type: str = typer.Option( + ..., + "-t", + "--controller-type", + help="controller type", + ), +): """Update a scenario's controllers from an OpenDSS file.""" + if controller_type not in CONTROLLER_TYPES: + raise typer.BadParameter(f"must be one of {CONTROLLER_TYPES}") project_path = ctx.parent.params["project_path"] scenario = ctx.parent.params["scenario"] update_pydss_controllers( @@ -48,8 +59,5 @@ def update_controllers(ctx, controller_type=None, controller=None, dss_file=None scenario=scenario, controller_type=controller_type, controller=controller, - dss_file=dss_file + dss_file=dss_file, ) - - -edit_scenario.add_command(update_controllers) diff --git a/src/pydss/cli/export.py b/src/pydss/cli/export.py index da0e5cb1..d936ae32 100644 --- a/src/pydss/cli/export.py +++ b/src/pydss/cli/export.py @@ -6,7 +6,7 @@ import os from loguru import logger -import click +import typer from pydss.pydss_results import PyDssResults @@ -15,44 +15,32 @@ # TODO Make command to list scenarios. -@click.argument( - "project-path", -) -@click.option( - "-f", "--fmt", - default="csv", - help="Output file format (csv or h5)." -) -@click.option( - "-c", "--compress", - is_flag=True, - default=False, - show_default=True, - help="Compress output files.", -) -@click.option( - "-o", "--output-dir", - help="Output directory. Default is project exports directory.", -) -@click.option( - "--verbose", - is_flag=True, - default=False, - show_default=True, - help="Enable verbose log output." -) -@click.command() -def export(project_path, fmt="csv", compress=False, output_dir=None, verbose=False): + +def export( + project_path: str, + fmt: str = typer.Option("csv", "-f", "--fmt", help="Output file format (csv or h5)."), + compress: bool = typer.Option( + False, + "-c", + "--compress", + help="Compress output files.", + ), + output_dir: str | None = typer.Option( + None, + "-o", + "--output-dir", + help="Output directory. Default is project exports directory.", + ), + verbose: bool = typer.Option(False, "--verbose", help="Enable verbose log output."), +): """Export data from a pydss project.""" if not os.path.exists(project_path): sys.exit(1) filename = "pydss_export.log" console_level = "INFO" - file_level = "INFO" if verbose: console_level = "DEBUG" - file_level = "DEBUG" logger.level(console_level) if filename: diff --git a/src/pydss/cli/extract.py b/src/pydss/cli/extract.py index 2e5c2727..b611cba2 100644 --- a/src/pydss/cli/extract.py +++ b/src/pydss/cli/extract.py @@ -6,31 +6,24 @@ import os from loguru import logger -import click +import typer from pydss.pydss_project import PyDssProject from pydss.pydss_results import PyDssResults from pydss.utils.utils import get_cli_string -@click.argument( - "file-path", -) -@click.argument( - "project-path", -) -@click.option( - "-o", "--output-dir", - help="Output directory. Default is the project path.", -) -@click.option( - "--verbose", - is_flag=True, - default=False, - show_default=True, - help="Enable verbose log output." -) -@click.command() -def extract(project_path, file_path, output_dir=None, verbose=False): + +def extract( + project_path: str, + file_path: str, + output_dir: str | None = typer.Option( + None, + "-o", + "--output-dir", + help="Output directory. Default is the project path.", + ), + verbose: bool = typer.Option(False, "--verbose", help="Enable verbose log output."), +): """Extract a file from an archived pydss project.""" if not os.path.exists(project_path): logger.error(f"project-path={project_path} does not exist") @@ -38,15 +31,13 @@ def extract(project_path, file_path, output_dir=None, verbose=False): filename = "pydss_extract.log" console_level = "INFO" - file_level = "INFO" if verbose: console_level = "DEBUG" - file_level = "DEBUG" logger.level(console_level) if filename: logger.add(filename) - + logger.info("CLI: [%s]", get_cli_string()) project = PyDssProject.load_project(project_path) @@ -66,22 +57,16 @@ def extract(project_path, file_path, output_dir=None, verbose=False): logger.info(f"Extracted {file_path} to {path}") -@click.argument( - "project-path", -) -@click.option( - "-o", "--output-dir", - help="Output directory. Default is the project path.", -) -@click.option( - "--verbose", - is_flag=True, - default=False, - show_default=True, - help="Enable verbose log output." -) -@click.command() -def extract_element_files(project_path, output_dir=None, verbose=False): +def extract_element_files( + project_path: str, + output_dir: str | None = typer.Option( + None, + "-o", + "--output-dir", + help="Output directory. Default is the project path.", + ), + verbose: bool = typer.Option(False, "--verbose", help="Enable verbose log output."), +): """Extract the element info files from an archived pydss project.""" if not os.path.exists(project_path): logger.error(f"project-path={project_path} does not exist") @@ -89,15 +74,13 @@ def extract_element_files(project_path, output_dir=None, verbose=False): filename = "pydss_extract.log" console_level = "INFO" - file_level = "INFO" if verbose: console_level = "DEBUG" - file_level = "DEBUG" logger.level(console_level) if filename: logger.add(filename) - + logger.info("CLI: [%s]", get_cli_string()) project = PyDssProject.load_project(project_path) diff --git a/src/pydss/cli/pydss.py b/src/pydss/cli/pydss.py index 48a5fa6d..56ff7415 100644 --- a/src/pydss/cli/pydss.py +++ b/src/pydss/cli/pydss.py @@ -1,7 +1,7 @@ """Main CLI command for pydss.""" from loguru import logger -import click +import typer from pydss.cli.create_project import create_project from pydss.cli.add_post_process import add_post_process @@ -25,20 +25,24 @@ "Server dependencies not installed. Use 'pip install NREL-pydss[server]' to install additional dependencies" ) -@click.group() -def cli(): +cli = typer.Typer(help="Pydss commands") + + +@cli.callback() +def main(): """Pydss commands""" -cli.add_command(create_project) -cli.add_command(add_post_process) -cli.add_command(export) -cli.add_command(extract) -cli.add_command(extract_element_files) -cli.add_command(run) -cli.add_command(add_scenario) -cli.add_command(edit_scenario) -cli.add_command(convert) -cli.add_command(controllers) -cli.add_command(reports) + +cli.command()(create_project) +cli.command()(add_post_process) +cli.command()(export) +cli.command()(extract) +cli.command()(extract_element_files) +cli.command()(run) +cli.command()(add_scenario) +cli.add_typer(edit_scenario, name="edit-scenario") +cli.add_typer(convert, name="convert") +cli.add_typer(controllers, name="controllers") +cli.command()(reports) if server_dependencies_installed: - cli.add_command(serve) \ No newline at end of file + cli.command()(serve) diff --git a/src/pydss/cli/reports.py b/src/pydss/cli/reports.py index 82ac4275..9bd35782 100644 --- a/src/pydss/cli/reports.py +++ b/src/pydss/cli/reports.py @@ -2,128 +2,129 @@ CLI to run a pydss project """ -import click +import typer import json import os from terminaltables import SingleTable from os.path import normpath, basename -@click.argument( - "project-path", -) -@click.option( - "-l", "--list-reports", - help="List all reports for a given project path", - is_flag=True, - default=False, - show_default=True, -) - -@click.option( - "-i", "--index", - help="View report by index (use -l flag to see list of available reports)", - default=0, - show_default=True, -) - -@click.option( - "-s", "--scenario", - required=False, - help="Pydss scenario name.", -) - -@click.option( - "-r", "--report", - required=False, - help="Pydss report name.", -) -@click.command() - -def reports(project_path, list_reports=False, scenario=None, report=None, index=0): +def reports( + project_path: str, + list_reports: bool = typer.Option( + False, + "-l", + "--list-reports", + help="List all reports for a given project path", + ), + index: int = typer.Option( + 0, + "-i", + "--index", + help="View report by index (use -l flag to see list of available reports)", + ), + scenario: str | None = typer.Option( + None, + "-s", + "--scenario", + help="Pydss scenario name.", + ), + report: str | None = typer.Option( + None, + "-r", + "--report", + help="Pydss report name.", + ), +): """Explore and print pydss reports.""" - assert not (list_reports and index), "Both 'list' and 'index' options cannot be set to true at the same time" - assert os.path.exists(project_path), "The provided project path {} does not exist".format(project_path) - logsPath = os.path.join(project_path, "Logs") - assert os.path.exists(logsPath), "No Logs folder in the provided project path.".format(project_path) - print(logsPath) - reportList = getAvailableReports(logsPath) + assert not (list_reports and index), ( + "Both 'list' and 'index' options cannot be set to true at the same time" + ) + assert os.path.exists(project_path), "The provided project path {} does not exist".format( + project_path + ) + logs_path = os.path.join(project_path, "Logs") + assert os.path.exists(logs_path), "No Logs folder in the provided project path." + print(logs_path) + report_list = get_available_reports(logs_path) project = basename(normpath(project_path)) if list_reports: - Table = SingleTable(reportList, title="Available pydss reports") + table = SingleTable(report_list, title="Available pydss reports") print("") - print(Table.table) + print(table.table) elif index: - idx, projectName, ScenarioName, ReportName = reportList[index] - printReport(logsPath, projectName, ScenarioName, ReportName) + idx, project_name, scenario_name, report_name = report_list[index] + print_report(logs_path, project_name, scenario_name, report_name) elif project: - for dx, projectName, ScenarioName, ReportName in reportList[1:]: - if projectName == project: - if scenario is None or scenario == ScenarioName: - if report is None or report == ReportName: - printReport(logsPath, projectName, ScenarioName, ReportName) + for dx, project_name, scenario_name, report_name in report_list[1:]: + if project_name == project: + if scenario is None or scenario == scenario_name: + if report is None or report == report_name: + print_report(logs_path, project_name, scenario_name, report_name) + -def printReport(logsPath, project, scenario, report): - fileName = "{}__{}__reports.log".format(project, scenario) - filePath = os.path.join(logsPath, fileName) - assert os.path.exists(filePath), "Report {} for project: {} / scenario: {} does not exist".format( - report, project, scenario +def print_report(logs_path, project, scenario, report): + file_name = "{}__{}__reports.log".format(project, scenario) + file_path = os.path.join(logs_path, file_name) + assert os.path.exists(file_path), ( + "Report {} for project: {} / scenario: {} does not exist".format(report, project, scenario) ) - tableData = [] - Keys = {} - with open(os.path.join(logsPath, fileName), "r") as f: - for l in f: - data = json.loads(l.strip()) + table_data = [] + keys = {} + with open(os.path.join(logs_path, file_name), "r") as f: + for line in f: + data = json.loads(line.strip()) if "Report" not in data: - print("Skipping {}. Not a valid pydss report.".format(fileName)) + print("Skipping {}. Not a valid pydss report.".format(file_name)) return None elif data["Report"] == report: - if report not in Keys: - Keys[report] = list(data.keys()) - Keys[report] = [x for x in Keys[report] if x != "Report"] + if report not in keys: + keys[report] = list(data.keys()) + keys[report] = [x for x in keys[report] if x != "Report"] values = [] - for k in Keys[report]: + for k in keys[report]: values.append(data[k]) - tableData.append(values) - tableData.insert(0, Keys[report]) - Table = SingleTable(tableData, title="{} report (Project: {}, Scenario: {})".format( - report, project, scenario - )) + table_data.append(values) + table_data.insert(0, keys[report]) + table = SingleTable( + table_data, title="{} report (Project: {}, Scenario: {})".format(report, project, scenario) + ) print("") - print(Table.table) + print(table.table) return -def getAvailableReports(logsPath): - logFiles = list(filter(lambda x: '.log' in x, os.listdir(logsPath))) - reportFiles = [x for x in logFiles if "__reports" in x] + +def get_available_reports(logs_path): + log_files = list(filter(lambda x: ".log" in x, os.listdir(logs_path))) + report_files = [x for x in log_files if "__reports" in x] headings = ["#", "Project", "Scenario", "Report"] - reportList = [headings] - reportNumber = 0 - for report in reportFiles: + report_list = [headings] + report_number = 0 + for report in report_files: project, scenario, _ = report.split("__") - print(reportNumber, project, scenario, report) - reportTypes = getReportTypes(logsPath, report) - if reportTypes is not None: - for reportTypes in reportTypes: - reportNumber += 1 - reportList.append([reportNumber, project, scenario, reportTypes]) - return reportList + print(report_number, project, scenario, report) + report_types = get_report_types(logs_path, report) + if report_types is not None: + for report_type in report_types: + report_number += 1 + report_list.append([report_number, project, scenario, report_type]) + return report_list -def getReportTypes(logsPath, reportFile): - fileName = os.path.join(logsPath, reportFile) - print(fileName) - f = open(fileName, "r") +def get_report_types(logs_path, report_file): + file_name = os.path.join(logs_path, report_file) + print(file_name) + f = open(file_name, "r") lines = f.readlines() - reportTypes = [] - for l in lines: - data = json.loads(l.strip()) + report_types = [] + for line in lines: + data = json.loads(line.strip()) if "Report" not in data: - print("Skipping {}. Not a valid pydss report.".format(fileName)) + print("Skipping {}. Not a valid pydss report.".format(file_name)) return None else: - if data["Report"] not in reportTypes: - reportTypes.append(data["Report"]) - return reportTypes + if data["Report"] not in report_types: + report_types.append(data["Report"]) + return report_types diff --git a/src/pydss/cli/run.py b/src/pydss/cli/run.py index 4df5b9e5..1d9dc4ec 100644 --- a/src/pydss/cli/run.py +++ b/src/pydss/cli/run.py @@ -7,58 +7,37 @@ import sys from loguru import logger -import click +import typer from pydss.utils.utils import get_cli_string, make_human_readable_size from pydss.common import SIMULATION_SETTINGS_FILENAME from pydss.pydss_project import PyDssProject -@click.argument("project-path", type=click.Path(exists=True)) -@click.option( - "-o", "--options", - help="dict-formatted simulation settings that override the config file. " \ - "Example: pydss run ./project --options \"{\\\"Exports\\\": {\\\"Export Compression\\\": \\\"true\\\"}}\"", -) -@click.option( - "-s", "--simulations-file", - required=False, - default = SIMULATION_SETTINGS_FILENAME, - show_default=True, - help="scenario toml file to run (over rides default)", -) - -@click.option( - "-t", "--tar-project", - is_flag=True, - default=False, - show_default=True, - help="Tar project files after successful execution." -) -@click.option( - "-z", "--zip-project", - is_flag=True, - default=False, - show_default=True, - help="Zip project files after successful execution." -) -@click.option( - "--verbose", - is_flag=True, - default=False, - show_default=True, - help="Enable verbose log output." -) -@click.option( - "--dry-run", - is_flag=True, - default=False, - show_default=True, - help="Dry run for getting estimated space." -) -@click.command() - -def run(project_path, options=None, tar_project=False, zip_project=False, verbose=False, simulations_file=None, dry_run=False): +def run( + project_path: str = typer.Argument(..., exists=True), + options: str | None = typer.Option( + None, + "-o", + "--options", + help="dict-formatted simulation settings that override the config file. " + 'Example: pydss run ./project --options "{\\"Exports\\": {\\"Export Compression\\": \\"true\\"}}"', + ), + simulations_file: str = typer.Option( + SIMULATION_SETTINGS_FILENAME, + "-s", + "--simulations-file", + help="scenario toml file to run (over rides default)", + ), + tar_project: bool = typer.Option( + False, "-t", "--tar-project", help="Tar project files after successful execution." + ), + zip_project: bool = typer.Option( + False, "-z", "--zip-project", help="Zip project files after successful execution." + ), + verbose: bool = typer.Option(False, "--verbose", help="Enable verbose log output."), + dry_run: bool = typer.Option(False, "--dry-run", help="Dry run for getting estimated space."), +): """Run a pydss simulation.""" project_path = Path(project_path) settings = PyDssProject.load_simulation_settings(project_path, simulations_file) @@ -67,13 +46,11 @@ def run(project_path, options=None, tar_project=False, zip_project=False, verbos settings.logging.logging_level = "DEBUG" filename = None - console_level = "INFO" - file_level = "INFO" + console_level = "INFO" if not settings.logging.enable_console: console_level = "ERROR" if verbose: console_level = "DEBUG" - file_level = "DEBUG" if settings.logging.enable_file: logs_path = project_path / "Logs" if not logs_path.exists(): @@ -85,7 +62,9 @@ def run(project_path, options=None, tar_project=False, zip_project=False, verbos if filename: logger.add(filename) - logger.info(f"CLI: [{get_cli_string()}]",) + logger.info( + f"CLI: [{get_cli_string()}]", + ) if options is not None: options = ast.literal_eval(options) @@ -93,7 +72,9 @@ def run(project_path, options=None, tar_project=False, zip_project=False, verbos logger.error("options are invalid: %s", options) sys.exit(1) - project = PyDssProject.load_project(project_path, options=options, simulation_file=simulations_file) + project = PyDssProject.load_project( + project_path, options=options, simulation_file=simulations_file + ) project.run(tar_project=tar_project, zip_project=zip_project, dry_run=dry_run) if dry_run: @@ -101,7 +82,7 @@ def run(project_path, options=None, tar_project=False, zip_project=False, verbos if len("ScenarioName") > maxlen: maxlen = len("ScenarioName") template = "{:<{width}} {}\n".format("ScenarioName", "EstimatedSpace", width=maxlen) - + total_size = 0 for k, v in project.estimated_space.items(): total_size += v @@ -109,8 +90,7 @@ def run(project_path, options=None, tar_project=False, zip_project=False, verbos template += "{:<{width}} : {}\n".format(k, vstr, width=maxlen) template = template.strip() logger.info(template) - logger.info("-"*30) + logger.info("-" * 30) logger.info(f"TotalSpace: {make_human_readable_size(total_size)}") - logger.info("="*30) + logger.info("=" * 30) logger.info("Note: compression may reduce the size by ~90% depending on the data.") - diff --git a/src/pydss/cli/run_server.py b/src/pydss/cli/run_server.py index fc4db06f..44cf79d9 100644 --- a/src/pydss/cli/run_server.py +++ b/src/pydss/cli/run_server.py @@ -4,21 +4,21 @@ from loguru import logger from aiohttp import web -import click +import typer -from pydss.api.server import pydss_server +from pydss.api.server import PydssServer -@click.option( - "-p", "--port", - default=9090, - show_default=True, - help="Socket port for the server", -) -@click.command() -def serve(ip="127.0.0.1",port=9090): +def serve( + ip: str = typer.Option("127.0.0.1", hidden=True), + port: int = typer.Option( + 9090, + "-p", + "--port", + help="Socket port for the server", + ), +): """Run a pydss RESTful API server.""" - FORMAT = '%(asctime)s - %(levelname)s - %(message)s' logger.level("DEBUG") - pydss = pydss_server(ip, port) - web.run_app(pydss.app, port=port) \ No newline at end of file + pydss = PydssServer(ip, port) + web.run_app(pydss.app, port=port) diff --git a/src/pydss/common.py b/src/pydss/common.py index fd1082c6..e2813f98 100644 --- a/src/pydss/common.py +++ b/src/pydss/common.py @@ -1,13 +1,11 @@ - import enum import os from collections import namedtuple import pydss -from pydss.utils.utils import load_data -DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f' # '%Y-%m-%d %H:%M:%S.%f', "%m/%d/%Y %H:%M:%S" -TIME_FORMAT = '%H:%M:%S' +DATE_FORMAT = "%Y-%m-%d %H:%M:%S.%f" # '%Y-%m-%d %H:%M:%S.%f', "%m/%d/%Y %H:%M:%S" +TIME_FORMAT = "%H:%M:%S" PLOTS_FILENAME = "plots.toml" SIMULATION_SETTINGS_FILENAME = "simulation.toml" @@ -24,6 +22,7 @@ NODE_NAMES_BY_TYPE_FILENAME = "node_names_by_type.json" INTEGER_NAN = -9999 + class ControllerType(enum.Enum): FAULT_CONTROLLER = "FaultController" GENERATOR_CONTROLLER = "GenController" @@ -39,18 +38,22 @@ class ControllerType(enum.Enum): XMFR_CONTROLLER = "xmfrController" DYNAMIC_VOLTAGE_SUPPORT = "DynamicVoltageSupport" + CONTROLLER_TYPES = tuple(x.value for x in ControllerType) CONFIG_EXT = ".toml" + class ExportMode(enum.Enum): BY_CLASS = "ExportMode-byClass" BY_ELEMENT = "ExportMode-byElement" - SUBSCRIPTIONS = 'Subscriptions' + SUBSCRIPTIONS = "Subscriptions" EXPORTS = "Exports" + def filename_from_enum(obj): return obj.value + CONFIG_EXT + FAULT_CONTROLLER_FILENAME = filename_from_enum(ControllerType.FAULT_CONTROLLER) GENERATOR_CONTROLLER_FILENAME = filename_from_enum(ControllerType.GENERATOR_CONTROLLER) MOTOR_STALL_FILENAME = filename_from_enum(ControllerType.MOTOR_STALL) @@ -61,7 +64,9 @@ def filename_from_enum(obj): PV_VOLTAGE_RIDETHROUGH_FILENAME = filename_from_enum(ControllerType.PV_VOLTAGE_RIDETHROUGH) SOCKET_CONTROLLER_FILENAME = filename_from_enum(ControllerType.SOCKET_CONTROLLER) STORAGE_CONTROLLER_FILENAME = filename_from_enum(ControllerType.STORAGE_CONTROLLER) -THERMOSTATIC_LOAD_CONTROLLER_FILENAME = filename_from_enum(ControllerType.THERMOSTATIC_LOAD_CONTROLLER) +THERMOSTATIC_LOAD_CONTROLLER_FILENAME = filename_from_enum( + ControllerType.THERMOSTATIC_LOAD_CONTROLLER +) XMFR_CONTROLLER_FILENAME = filename_from_enum(ControllerType.XMFR_CONTROLLER) EXPORT_BY_CLASS_FILENAME = filename_from_enum(ExportMode.BY_CLASS) @@ -161,10 +166,7 @@ def filename_from_enum(obj): ) DEFAULT_PLOT_SETTINGS_FILE = os.path.join( - os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss", - "defaults", - PLOTS_FILENAME + os.path.dirname(getattr(pydss, "__path__")[0]), "pydss", "defaults", PLOTS_FILENAME ) DEFAULT_EXPORT_BY_CLASS_SETTINGS_FILE = os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), @@ -194,10 +196,15 @@ def filename_from_enum(obj): "Monte_Carlo", MONTE_CARLO_SETTINGS_FILENAME, ) + + class ControlMode(enum.Enum): """Supported control modes""" + STATIC = "Static" TIME = "Time" + + class DataConversion(enum.Enum): NONE = "none" ABS = "abs" @@ -205,14 +212,19 @@ class DataConversion(enum.Enum): SUM = "sum" SUM_REAL = "sum_real" SUM_ABS_REAL = "sum_abs_real" + + class DatasetPropertyType(enum.Enum): PER_TIME_POINT = "per_time_point" # data is stored at every time point FILTERED = "filtered" # data is stored after being filtered METADATA = "metadata" # metadata for another dataset TIME_STEP = "time_step" # data are time indices, tied to FILTERED VALUE = "value" # Only a single value is written for each element + + class FileFormat(enum.Enum): """Supported file formats""" + CSV = "csv" HDF5 = "h5" @@ -224,6 +236,7 @@ class LimitsFilter(enum.Enum): class LoggingLevel(enum.Enum): """Supported logging levels""" + DEBUG = "debug" INFO = "info" WARNING = "warning" @@ -232,6 +245,7 @@ class LoggingLevel(enum.Enum): class ReportGranularity(enum.Enum): """Specifies the granularity on which data is collected.""" + PER_ELEMENT_PER_TIME_POINT = "per_element_per_time_point" PER_ELEMENT_TOTAL = "per_element_total" ALL_ELEMENTS_PER_TIME_POINT = "all_elements_per_time_point" @@ -240,6 +254,7 @@ class ReportGranularity(enum.Enum): class SimulationType(enum.Enum): """Supported simulation types""" + DYNAMIC = "dynamic" QSTS = "qsts" SNAPSHOT = "snapshot" diff --git a/src/pydss/config_data.py b/src/pydss/config_data.py index ef7294fd..6afa4e3f 100644 --- a/src/pydss/config_data.py +++ b/src/pydss/config_data.py @@ -9,6 +9,7 @@ from pydss.exceptions import InvalidParameter from pydss.utils.utils import dump_data + def convert_config_data_to_toml(filename, name=None): """Converts an Excel config file to TOML. @@ -41,7 +42,12 @@ def _get_config_type(basename): def _convert_controller(filename, name_field): - df = pd.read_excel(filename, skiprows=[0,]) + df = pd.read_excel( + filename, + skiprows=[ + 0, + ], + ) controllers = df.to_dict(orient="records") data = {} for controller in controllers: @@ -66,14 +72,22 @@ def _convert_storage_controller(filename): def _convert_xfmr_controller(filename): return _convert_controller(filename, "Controlled XFMR") + def _convert_motorstall(filename): return _convert_controller(filename, "Controlled Motor") -def _convert_PvVoltageRideThru(filename): + +def _convert_pv_voltage_ride_thru(filename): return _convert_controller(filename, "Controlled PV") + def _convert_exports(filename, name_field): - df = pd.read_excel(filename, skiprows=[0,]) + df = pd.read_excel( + filename, + skiprows=[ + 0, + ], + ) exports = {} for export in df.to_dict(orient="records"): cls = export.pop(name_field) @@ -99,7 +113,12 @@ def _convert_export_by_element(filename): def _convert_plot_config(filename): - df = pd.read_excel(filename, skiprows=[0,]) + df = pd.read_excel( + filename, + skiprows=[ + 0, + ], + ) data = {} for item in df.to_dict(orient="records"): if "Filename" in item: @@ -132,7 +151,7 @@ def _convert_gis_overlay(df): "convert": _convert_motorstall, }, "PvVoltageRideThru": { - "convert": _convert_PvVoltageRideThru, + "convert": _convert_pv_voltage_ride_thru, }, "xfmrController": { "convert": _convert_xfmr_controller, @@ -144,9 +163,9 @@ def _convert_gis_overlay(df): "convert": _convert_export_by_element, }, # TODO: this needs customization - #"GIS overlay": { + # "GIS overlay": { # "convert": _convert_plot_config, - #}, + # }, "Histogram": { "convert": _convert_plot_config, }, diff --git a/src/pydss/controllers.py b/src/pydss/controllers.py deleted file mode 100644 index 7d0394e6..00000000 --- a/src/pydss/controllers.py +++ /dev/null @@ -1,362 +0,0 @@ - -from typing import Optional, Annotated -from collections import defaultdict -from loguru import logger -import json -import abc - -from pydantic import BaseModel, Field -from pydantic import ConfigDict -import opendssdirect as dss - -from pydss.exceptions import InvalidConfiguration, OpenDssConvergenceError -from pydss.pyControllers.Controllers.PvController import PvController -from pydss.utils.timing_utils import TimerStatsCollector, Timer -from pydss.utils.dss_utils import list_element_names_by_class -from pydss.dssInstance import OpenDSS, CONTROLLER_PRIORITIES -from pydss.SolveMode import get_solver_from_simulation_type -from pydss.simulation_input_models import ProjectModel -from pydss.modes.solver_base import solver_base - - -class ControllerBaseModel(BaseModel, abc.ABC): - model_config = ConfigDict(title="ControllerBaseModel", str_strip_whitespace=True, validate_assignment=True, validate_default=True, extra="forbid", use_enum_values=False, populate_by_name=True) - - @staticmethod - @abc.abstractmethod - def get_controller_class(): - """Return the pydss controller class for the model.""" - - @staticmethod - @abc.abstractmethod - def get_element_class(): - """Return the OpenDSS element class for the controller.""" - - -class PvControllerModel(ControllerBaseModel): - control1: Annotated[ - str, - Field( - title="Control1", - description="Algorithm to run in the first control loop", - alias="Control1", - )] - control2: Optional[str] = Field( - title="Control1", - description="Algorithm to run in the second control loop", - alias="Control2", - ) - control3: Optional[str] = Field( - title="Control3", - description="Algorithm to run in the third control loop", - alias="Control3", - ) - pf: int = Field( - title="pf", - description="Power factor for the PV system", - ) - pf_min: Annotated[ - float, - Field( - title="pfMin", - description="Minimum allowable power factor for the PV system. Applied only if enable_pf_limit is set.", - alias="pfMin", - )] - pf_max: Annotated[ - float, - Field( - title="pfMax", - description="Maximum allowable power factor for the PV system. Applied only if enable_pf_limit is set.", - alias="pfMax", - )] - p_min: Annotated[ - float, - Field( - title="Pmin", - description="TODO", - alias="Pmin", - )] - p_max:Annotated[ - float, - Field( - title="Pmax", - description="TODO", - alias="Pmax", - )] - u_min: Annotated[ - float, - Field( - title="uMin", - description="Per unit voltage value at which inverter produces maximum vars. (volt / var algorithm).", - alias="uMin", - )] - u_db_min: Annotated[ - float, - Field( - title="uDbMin", - description="Lower bound for the voltage deadband [per unit]. Inverter will not produce or consume vars within these bands (volt / var algorithm).", - alias="uDbMin", - )] - u_db_max: Annotated[ - float, - Field( - title="uDbMax", - description="Upper bound for the voltage deadband [per unit]. Inverter will not produce or consume vars within these bands (volt / var algorithm).", - alias="uDbMax", - )] - u_max: Annotated[ - float, - Field( - title="uMax", - description="Per unit voltage value at which inverter consumes maximum vars (volt / var algorithm).", - alias="uMax", - )] - q_lim_pu: Annotated[ - float, - Field( - title="QlimPU", - description="Inverter reactive power limit [per unit] for the volt / var algorithm.", - alias="QlimPU", - )] - pf_lim: Annotated[ - float, - Field( - title="PFlim", - description="Inverter power factor limit. Applied only if enable_pf_limit is set.", - alias="PFlim", - )] - enable_pf_limit: Annotated[ - bool, - Field( - title="EnablePFLimit", - description="Flag to enable / disable power factor limits on the inverter", - alias="Enable PF limit", - )] - u_min_c: Annotated[ - float, - Field( - title="uMinC", - description="Lower voltage bound [per unit] for the volt / watt algorithm", - alias="uMinC", - )] - u_max_c: Annotated[ - float, - Field( - title="uMaxC", - description="Upper voltage bound [per unit] for the volt / watt algorithm", - alias="uMaxC", - )] - p_min_vw: Annotated[ - float, - Field( - title="PminVW", - description="Lower bound for the inveter active power output for the volt / watt algorithm", - alias="PminVW", - )] - vw_type: Annotated[ - str, - Field( - title="VWtype", - description="volt / watt algorithm to be implemented on rated or available power", - alias="VWtype", - )] - percent_p_cutin: Annotated[ - float, - Field( - title="PCutin", - description="Percentage cut-in power -- Percentage of kVA rating of inverter. When the inverter is OFF, the power from the array must be greater than this for the inverter to turn on.", - alias="%PCutin", - )] - percent_p_cutout: Annotated[ - float, - Field( - title="%PCutout", - description="Percentage cut-out power -- Percentage of kVA rating of inverter. When the inverter is ON, the inverter turns OFF when the power from the array drops below this value.", - alias="%PCutout", - )] - efficiency: Annotated[ - float, - Field( - title="Efficiency", - description="Efficieny of the inverter system", - alias="Efficiency", - )] - priority: Annotated[ - str, - Field( - title="Priority", - description="Set export priority for active power or reactive power", - alias="Priority", - )] - damp_coef: Annotated[ - float, - Field( - title="DampCoef", - description="Damping cooefficient for the convergence algorithm", - alias="DampCoef", - )] - - @staticmethod - def get_controller_class(): - return PvController - - @staticmethod - def get_element_class(): - return dss.PVsystems - - -def make_default_volt_var_controller(): - return PvControllerModel( - Control1="VVar", - Control2="None", - Control3="None", - pf=1, - pfMin=0.8, - pfMax=1, - Pmin=0, - Pmax=1, - uMin=0.9399999999999999, - uDbMin=0.97, - uDbMax=1.03, - uMax=1.06, - QlimPU=0.44, - PFlim=0.9, - enable_pf_limit=False, - uMinC=1.06, - uMaxC=1.1, - PminVW=10, - VWtype="Rated Power", - percent_p_cutin=10, - percent_p_cutout=10, - Efficiency=100, - Priority="Var", - DampCoef=0.8, - ) - - -class CircuitElementController: - """Contains a controller model and elements to control.""" - - def __init__(self, controller_model: ControllerBaseModel, element_names=None): - """ - Parameters - ---------- - controller_model: ControllerBaseModel - element_names: list | None - List of element names (str). If None, use all elements for the given class. - - """ - self._controller_model = controller_model - if element_names is None: - self._element_names = list_element_names_by_class(self.get_element_class()) - else: - self._element_names = element_names - - @property - def controller_model(self): - return self._controller_model - - @property - def element_names(self): - return self._element_names - - def get_controller_class(self): - return type(self._controller_model).get_controller_class() - - def get_element_class(self): - return type(self._controller_model).get_element_class() - - -class ControllerManager: - """Provides ability to run control algorithms on circuit elements.""" - - def __init__(self, controllers: dict, solver: solver_base, max_control_iterations: int, error_tolerance: float): - self._controllers = controllers - self._max_control_iterations = max_control_iterations - self._error_tolerance = error_tolerance - self._solver = solver - self._stats = TimerStatsCollector() - - @classmethod - def create(cls, controllers: list, settings: ProjectModel): - """Create controllers. The circuit must be loaded in OpenDSS.""" - solver = get_solver_from_simulation_type(settings) - buses = OpenDSS.CreateBusObjects() - elements, elements_by_class = OpenDSS.CreateDssObjects(buses) - controllers_by_class = defaultdict(dict) - for circuit_element_controller in controllers: - controller_class = circuit_element_controller.get_controller_class() - element_class = circuit_element_controller.get_element_class() - for name in circuit_element_controller.element_names: - element = elements.get(name) - if element is None: - raise InvalidConfiguration(f"{name} is not in the circuit") - controller = controller_class( - element, - circuit_element_controller.controller_model.dict(by_alias=True), - dss, - elements, - solver, - ) - controllers_by_class[element_class]["Controller." + name] = controller - - return cls( - controllers_by_class, - solver, - settings.max_control_iterations, - settings.error_tolerance, - ) - - def run_controls(self): - """Run all controls. - - Returns - ------- - bool - True if all controllers converged, otherwise False. - - """ - has_converged = True - with Timer(self._stats, "RunControls"): - for priority in range(CONTROLLER_PRIORITIES): - priority_has_converged = False - for i in range(self._max_control_iterations): - if self._update_controllers(priority, i): - priority_has_converged = True - break - if not self._solver.reSolve(): - raise OpenDssConvergenceError(f"OpenDSS did not converge after update") - if not priority_has_converged: - has_converged = False - - return has_converged - - def _update_controllers(self, priority, iteration): - maxError = 0 - - for element_class, elements in self._controllers.items(): - elm = element_class.First() - while elm: - element_name = dss.CktElement.Name() - controller_name = 'Controller.' + element_name - if controller_name in elements: - controller = elements[controller_name] - error = controller.Update(priority, iteration, False) - maxError = error if error > maxError else maxError - if iteration == self._max_control_iterations - 1: - if error > self._error_tolerance: - errorTag = { - "Report": "Convergence", - #"Scenario": self._settings.active_scenario, - "Time": self._solver.GetTotalSeconds(), - "DateTime": str(self._solver.GetDateTime()), - "Controller": controller.Name(), - "Controlled element": controller.ControlledElement(), - "Error": error, - "Control algorithm": controller.debugInfo()[priority], - } - json_object = json.dumps(errorTag) - # TODO: Make reports logger - logger.warning(json_object) - elm = element_class.Next() - return maxError < self._error_tolerance diff --git a/src/pydss/dataset_buffer.py b/src/pydss/dataset_buffer.py index 0b60602c..d92ff42e 100644 --- a/src/pydss/dataset_buffer.py +++ b/src/pydss/dataset_buffer.py @@ -9,7 +9,6 @@ from pydss.common import DatasetPropertyType - KiB = 1024 MiB = KiB * KiB GiB = MiB * MiB @@ -23,20 +22,31 @@ # entire chunk to be read. DEFAULT_MAX_CHUNK_BYTES = 1 * MiB + class DatasetBuffer: """Provides a write buffer to an HDF dataset to increase performance. Users must call flush_data before the object goes out of scope to ensure that all data is flushed. """ + # TODO add support for context manager, though pydss wouldn't be able to # take advantage in its current implementation. def __init__( - self, hdf_store, path, max_size, dtype, columns, scaleoffset=None, - max_chunk_bytes=None, attributes=None, names=None, - column_ranges_per_name=None, data=None - ): + self, + hdf_store, + path, + max_size, + dtype, + columns, + scaleoffset=None, + max_chunk_bytes=None, + attributes=None, + names=None, + column_ranges_per_name=None, + data=None, + ): if max_chunk_bytes is None: max_chunk_bytes = DEFAULT_MAX_CHUNK_BYTES self._buf_index = 0 @@ -70,7 +80,7 @@ def __init__( shuffle=True, maxshape=[None for _ in range(dim)], # Does not preserve NaN, so don't use it. - #scaleoffset=scaleoffset, + # scaleoffset=scaleoffset, ) # Columns, names, and column_ranges_per_name can't be stored as @@ -80,7 +90,7 @@ def __init__( column_dataset = self._hdf_store.create_dataset( name=column_dataset_path, data=np.array(columns, dtype="S"), - maxshape=(None, ), + maxshape=(None,), ) column_dataset.attrs["type"] = DatasetPropertyType.METADATA.value self._dataset.attrs["column_dataset_path"] = column_dataset_path @@ -88,9 +98,9 @@ def __init__( if names is not None: name_dataset_path = path + "Names" name_dataset = self._hdf_store.create_dataset( - name = name_dataset_path, - data = np.array(names, dtype="S"), - maxshape=(None, ), + name=name_dataset_path, + data=np.array(names, dtype="S"), + maxshape=(None,), ) name_dataset.attrs["type"] = DatasetPropertyType.METADATA.value self._dataset.attrs["name_dataset_path"] = name_dataset_path @@ -115,12 +125,12 @@ def __init__( for attr, val in attributes.items(): self._dataset.attrs[attr] = val - logger.debug("Created DatasetBuffer path=%s shape=%s chunks=%s", - path, shape, chunks) + logger.debug("Created DatasetBuffer path=%s shape=%s chunks=%s", path, shape, chunks) def __del__(self): - assert self._buf_index == 0, \ + assert self._buf_index == 0, ( f"DatasetBuffer destructed with data in memory: {self._dataset.name}" + ) def flush_data(self): """Flush the data in the temporary buffer to storage.""" @@ -129,14 +139,16 @@ def flush_data(self): return new_index = self._dataset_index + length - + if new_index > self._dataset.shape[0]: new_dimensions = (new_index, self._dataset.shape[1]) self._dataset.resize(new_dimensions) - logger.warning(f"result index {new_index} exceed dataset dimension {self._dataset.shape[0]} for dataset {self._dataset.name}. Resizig dataset to {new_dimensions}") - - self._dataset[self._dataset_index:new_index] = self._buf[0:length] - + logger.warning( + f"result index {new_index} exceed dataset dimension {self._dataset.shape[0]} for dataset {self._dataset.name}. Resizig dataset to {new_dimensions}" + ) + + self._dataset[self._dataset_index : new_index] = self._buf[0:length] + self._buf_index = 0 self._dataset_index = new_index self._dataset.attrs["length"] = new_index @@ -164,25 +176,20 @@ def write_value(self, value): def write_data(self, values): """Write the data to the dataset.""" new_index = self._dataset_index + len(values) - self._dataset[self._dataset_index:new_index] = values + self._dataset[self._dataset_index : new_index] = values self._dataset_index = new_index self._dataset.attrs["length"] = new_index @staticmethod - def compute_chunk_count( - num_columns, - max_size, - dtype, - max_chunk_bytes=DEFAULT_MAX_CHUNK_BYTES - ): + def compute_chunk_count(num_columns, max_size, dtype, max_chunk_bytes=DEFAULT_MAX_CHUNK_BYTES): assert max_size > 0, f"max_size={max_size}" tmp = np.empty((1, num_columns), dtype=dtype) size_row = tmp.size * tmp.itemsize chunk_count = min(int(max_chunk_bytes / size_row), max_size) if chunk_count == 0: raise InvalidConfiguration( - f"HDF Max Chunk Bytes is smaller than the size of a row. Please increase it. " \ - f"max_chunk_bytes={max_chunk_bytes} num_columns={num_columns} " \ + f"HDF Max Chunk Bytes is smaller than the size of a row. Please increase it. " + f"max_chunk_bytes={max_chunk_bytes} num_columns={num_columns} " f"size_row={size_row}" ) diff --git a/src/pydss/defaults/pyControllerList/GenController.toml b/src/pydss/defaults/pyControllerList/GenController.toml index 93391ca1..464b2873 100644 --- a/src/pydss/defaults/pyControllerList/GenController.toml +++ b/src/pydss/defaults/pyControllerList/GenController.toml @@ -1,13 +1,9 @@ ["Generator.pv_system_p1ulv3"] Category = "test" -Control1 = "None" -Control2 = "VVar" -Control3 = "None" UcalcMode = "Max" "Model as PVsystem" = true PFlim = 0.95 DampCoef = 0.8 -Priority = 0.45 uMin = 0.95 uDbMin = 1.00 uDbMax = 1.00 diff --git a/src/pydss/defaults/pyControllerList/PvController.toml b/src/pydss/defaults/pyControllerList/PvController.toml index 9fbeec2d..8a2ea8c2 100644 --- a/src/pydss/defaults/pyControllerList/PvController.toml +++ b/src/pydss/defaults/pyControllerList/PvController.toml @@ -1,7 +1,4 @@ [NO_VRT] -Control1 = "None" -Control2 = "None" -Control3 = "None" pf = 1 pfMin = 0.8 pfMax = 1 @@ -21,14 +18,10 @@ VWtype = "Rated Power" "%PCutin" = 10 "%PCutout" = 10 Efficiency = 100 -Priority = "Var" DampCoef = 0.8 VmeaMethod = "Max" [cpf] -Control1 = "None" -Control2 = "VVar" -Control3 = "None" pf = 1 pfMin = 0.8 pfMax = 1 @@ -48,14 +41,10 @@ VWtype = "Rated Power" "%PCutin" = 10 "%PCutout" = 10 Efficiency = 100 -Priority = "Var" DampCoef = 0.8 VmeaMethod = "Max" [volt-var] -Control1 = "None" -Control2 = "VVar" -Control3 = "None" pf = 1 pfMin = 0.8 pfMax = 1 @@ -75,6 +64,5 @@ VWtype = "Rated Power" "%PCutin" = 10 "%PCutout" = 10 Efficiency = 100 -Priority = "Var" DampCoef = 0.8 VmeaMethod = "Max" \ No newline at end of file diff --git a/src/pydss/defaults/pyControllerList/PvFrequencyRideThru.toml b/src/pydss/defaults/pyControllerList/PvFrequencyRideThru.toml index c8820c54..cd94b070 100644 --- a/src/pydss/defaults/pyControllerList/PvFrequencyRideThru.toml +++ b/src/pydss/defaults/pyControllerList/PvFrequencyRideThru.toml @@ -1,8 +1,5 @@ ["Generator.pv_1"] Category = "test" -Control1 = "None" -Control2 = "None" -Control3 = "None" kV = nan kVA = 4 maxKW = 4 @@ -10,7 +7,6 @@ KvarLimit = 1.76 "%PCutin" = 10 "%PCutout" = 10 UcalcMode = "Max" -Priority = "Equal" "Enable PF limit" = false pfMin = 0.95 "Follow standard" = "1547-2018" diff --git a/src/pydss/defaults/pyControllerList/PvVoltageRideThru.toml b/src/pydss/defaults/pyControllerList/PvVoltageRideThru.toml index c96fc523..ad374180 100644 --- a/src/pydss/defaults/pyControllerList/PvVoltageRideThru.toml +++ b/src/pydss/defaults/pyControllerList/PvVoltageRideThru.toml @@ -6,7 +6,6 @@ "%PCutin" = 10.0 "%PCutout" = 10.0 "UcalcMode" = "Max" -"Priority" = "Equal" "Enable PF limit" = false "pfMin" = 0.95 "Follow standard" = "1547-2003" @@ -33,7 +32,6 @@ "%PCutin" = 0.0 "%PCutout" = 0.0 "UcalcMode" = "Max" -"Priority" = "Equal" "Enable PF limit" = false "pfMin" = 0.95 "Follow standard" = "1547-2018" @@ -61,7 +59,6 @@ "%PCutin" = 0.0 "%PCutout" = 0.0 "UcalcMode" = "Max" -"Priority" = "Equal" "Enable PF limit" = false "pfMin" = 0.95 "Follow standard" = "1547-2018" @@ -89,7 +86,6 @@ "%PCutin" = 0 "%PCutout" = 0 "UcalcMode" = "Max" -"Priority" = "Equal" "Enable PF limit" = false "pfMin" = 0.95 "Follow standard" = "1547-2018" diff --git a/src/pydss/defaults/pyControllerList/StorageController.toml b/src/pydss/defaults/pyControllerList/StorageController.toml index 28127de8..a67afd90 100644 --- a/src/pydss/defaults/pyControllerList/StorageController.toml +++ b/src/pydss/defaults/pyControllerList/StorageController.toml @@ -1,9 +1,6 @@ ["Storage.1234"] VoltageMeaElem = "storagebus" PowerMeaElem = "Line.sw0" -Control1 = "PS" -Control2 = "None" -Control3 = "None" BaseLoadLim = 0 ExpWindowStart = 18:00:00 ExpWindowEnd = 08:00:00 diff --git a/src/pydss/defaults/pyControllerList/__init__.py b/src/pydss/defaults/pyControllerList/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pydss/defaults/pyPlotList/__init__.py b/src/pydss/defaults/pyPlotList/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pydss/dssBus.py b/src/pydss/dssBus.py deleted file mode 100644 index d9b915bf..00000000 --- a/src/pydss/dssBus.py +++ /dev/null @@ -1,89 +0,0 @@ -import opendssdirect as dss - -from pydss.dssObjectBase import dssObjectBase - - -class dssBus(dssObjectBase): - - VARIABLE_OUTPUTS_BY_LABEL = { - "PuVoltage": { - "is_complex": True, - "units": ['[pu]'] - }, - "SeqVoltages": { - "is_complex": False, - "units": ['[kV]', '[Deg]'] - }, - "CplxSeqVoltages": { - "is_complex": True, - "units": ['[kV]'] - }, - "VMagAngle": { - "is_complex": False, - "units": ['[kV]', '[Deg]'] - }, - "Voc": { - "is_complex": True, - "units": ['[kV]'] - }, - "Voltages": { - "is_complex": True, - "units": ['[kV]'] - }, - "puVmagAngle": { - "is_complex": False, - "units": ['[pu]', '[Deg]'] - - }, - "Isc": { - "is_complex": True, - "units": ['[Amps]'] - - }, - - } - VARIABLE_OUTPUTS_COMPLEX = () - - def __init__(self, dssInstance=None): - if dssInstance is None: - dssInstance = dss - name = dssInstance.Bus.Name() - super(dssBus, self).__init__(dssInstance, name, name) - self._Index = None - self.XY = None - self._Class = 'Bus' - # self._Nodes is nested in a list to be consistent with dssElement._Nodes - self._Nodes = [dssInstance.Bus.Nodes()] - self._NumTerminals = 1 - self._NumConductors = len(dssInstance.Bus.Nodes()) - self.Distance = dssInstance.Bus.Distance() - BusVarDict = dssInstance.Bus.__dict__ - for key in BusVarDict.keys(): - try: - self._Variables[key] = getattr(dssInstance.Bus, key) - except: - self._Variables[key] = None - if self.GetVariable('X') is not None: - self.XY = [self.GetVariable('X'), self.GetVariable('Y')] - else: - self.XY = [0, 0] - - @property - def NumConductors(self): - return self._NumConductors - - @property - def NumPhases(self): - return len(self._Nodes) - - @property - def Phases(self): - return self._Nodes[:] - - def SetActiveObject(self): - try: - if self._dssInstance.Bus.Name() != self._Name: - self._dssInstance.Circuit.SetActiveBus(self._Name) - return 1 - except: - return 0 diff --git a/src/pydss/dssCircuit.py b/src/pydss/dssCircuit.py deleted file mode 100644 index 20289972..00000000 --- a/src/pydss/dssCircuit.py +++ /dev/null @@ -1,34 +0,0 @@ -import opendssdirect as dss - -from pydss.dssObjectBase import dssObjectBase - -class dssCircuit(dssObjectBase): - - VARIABLE_OUTPUTS_BY_LABEL = {} - VARIABLE_OUTPUTS_BY_LIST = ( - "AllBusMagPu", - ) - VARIABLE_OUTPUTS_COMPLEX = ( - "LineLosses", - "Losses", - "SubstationLosses", - "TotalPower", - ) - - def __init__(self, dssInstance=None): - if dssInstance is None: - dssInstance = dss - name = dssInstance.Circuit.Name() - fullName = "Circuit." + name - self._Class = 'Circuit' - super(dssCircuit, self).__init__(dssInstance, name, fullName) - - CktElmVarDict = dssInstance.Circuit.__dict__ - for key in CktElmVarDict.keys(): - try: - self._Variables[key] = getattr(dssInstance.Circuit, key) - except: - self._Variables[key] = None - - def SetActiveObject(self): - pass diff --git a/src/pydss/dssElement.py b/src/pydss/dssElement.py deleted file mode 100644 index 377c3fa0..00000000 --- a/src/pydss/dssElement.py +++ /dev/null @@ -1,205 +0,0 @@ -import ast - -from opendssdirect import DSSException - -from pydss.dssBus import dssBus -from pydss.dssObjectBase import dssObjectBase -from pydss.exceptions import InvalidParameter -from pydss.value_storage import ValueByNumber - - -class dssElement(dssObjectBase): - - VARIABLE_OUTPUTS_BY_LABEL = { - "Currents": { - "is_complex": True, - "units": ['[Amps]'] - }, - "CurrentsMagAng": { - "is_complex": False, - "units" : ['[Amps]', '[Deg]'] - }, - "Powers": { - "is_complex": True, - "units": ['[kVA]'] - }, - "Voltages": { - "is_complex": True, - "units": ['[kV]'] - }, - 'VoltagesMagAng': { - "is_complex": False, - "units": ['[kV]', '[Deg]'] - }, - 'CplxSeqCurrents': { - "is_complex": True, - "units": ['[Amps]'] - }, - 'SeqCurrents': { - "is_complex": False, - "units": ['[Amps]', '[Deg]'] - }, - 'SeqPowers': { - "is_complex": False, - "units": ['[kVA]', '[Deg]'] - } - } - - VARIABLE_OUTPUTS_COMPLEX = ( - "Losses", - ) - - _MAX_CONDUCTORS = 4 - - def __init__(self, dssInstance): - fullName = dssInstance.Element.Name() - if dssInstance.CktElement.Name() != fullName: - raise Exception(f"name mismatch {dssInstance.CktElement.Name()} {fullName}") - - self._Class, name = fullName.split('.', 1) - super(dssElement, self).__init__(dssInstance, name, fullName) - self._Enabled = dssInstance.CktElement.Enabled() - if not self._Enabled: - return - - self._Parameters = {} - self._NumTerminals = dssInstance.CktElement.NumTerminals() - self._NumConductors = dssInstance.CktElement.NumConductors() - - assert self._NumConductors <= self._MAX_CONDUCTORS, str(self._NumConductors) - self._NumPhases = dssInstance.CktElement.NumPhases() - - n = self._NumConductors - nodes = dssInstance.CktElement.NodeOrder() - self._Nodes = [nodes[i * n:(i + 1) * n] for i in range((len(nodes) + n - 1) // n)] - - assert len(nodes) == self._NumTerminals * self._NumConductors, \ - f"{self._Nodes} {self._NumTerminals} {self._NumConductors}" - - self._dssInstance = dssInstance - - PropertiesNames = self._dssInstance.Element.AllPropertyNames() - AS = range(len(PropertiesNames)) - for i, PptName in zip(AS, PropertiesNames): - self._Parameters[PptName] = str(i) - - CktElmVarDict = dssInstance.CktElement.__dict__ - try: - for VarName in dssInstance.CktElement.AllVariableNames(): - CktElmVarDict[VarName] = None - except DSSException as e: - # Prior to OpenDSSDirect.py v0.8.0 this returned an empty list for non-PC elements. - # v0.8.0 and later raises an exception. Ignore the error. - if e.args[1] != "The active circuit element is not a PC Element": - raise - - for key in CktElmVarDict.keys(): - try: - self._Variables[key] = getattr(dssInstance.CktElement, key) - except: - self._Variables[key] = None - self.Bus = dssInstance.CktElement.BusNames() - self.BusCount = len(self.Bus) - self.sBus = [] - for BusName in self.Bus: - self._dssInstance.Circuit.SetActiveBus(BusName) - self.sBus.append(dssBus(self._dssInstance)) - - def GetInfo(self): - return self._Class, self._Name - - def IsValidAttribute(self, VarName): - # Overridden from base because dssElement has Parameters. - if VarName in self._Variables: - return True - elif VarName in self._Parameters: - return True - else: - return False - - def DataLength(self, VarName): - if VarName in self._Variables: - VarValue = self.GetVariable(VarName) - elif VarName in self._Parameters: - VarValue = self.GetParameter(VarName) - else: - return 0, None - - if isinstance(VarValue, list): - return len(VarValue), 'List' - elif isinstance(VarValue, str): - return 1, 'String' - elif isinstance(VarValue, int or float or bool): - return 1, 'Number' - else: - return 0, None - - def GetValue(self, VarName, convert=False): - if self._dssInstance.Element.Name() != self._FullName: - self.SetActiveObject() - if VarName in self._Variables: - VarValue = self.GetVariable(VarName, convert=convert) - elif VarName in self._Parameters: - VarValue = self.GetParameter(VarName) - if convert: - VarValue = ValueByNumber(self._FullName, VarName, VarValue) - else: - return None - return VarValue - - def SetActiveObject(self): - self._dssInstance.Circuit.SetActiveElement(self._FullName) - if self._dssInstance.CktElement.Name() != self._dssInstance.Element.Name(): - raise InvalidParameter('Object is not a circuit element') - - def SetParameter(self, Param, Value): - reply = self._dssInstance.utils.run_command(self._FullName + '.' + Param + ' = ' + str(Value)) - if reply != "": - raise Exception(f"SetParameter failed: {reply}") - return self.GetParameter(Param) - - def GetParameter(self, Param): - if self._dssInstance.Element.Name() != self._FullName: - self._dssInstance.Circuit.SetActiveElement(self._FullName) - if self._dssInstance.Element.Name() == self._FullName: - # This always returns a string. - # The real value could be a number, a list of numbers, or a string. - x = self._dssInstance.Properties.Value(Param) - try: - return float(x) - except ValueError: - try: - return ast.literal_eval(x) - except (SyntaxError, ValueError): - return x - else: - return None - - @property - def Conductors(self): - letters = 'ABCN' - return [letters[i] for i in range(self._NumConductors)] - - @property - def ConductorByTerminal(self): - return [f"{j}{i}" for i in self.Conductors for j in self.Terminals] - - @property - def NodeOrder(self): - return self._NodeOrder[:] - - @property - def NumPhases(self): - return self._NumPhases - - @property - def NumConductors(self): - return self._NumConductors - - @property - def NumTerminals(self): - return self._NumTerminals - - @property - def Terminals(self): - return list(range(1, self._NumTerminals + 1)) diff --git a/src/pydss/dssInstance.py b/src/pydss/dssInstance.py deleted file mode 100644 index 3a08d6c5..00000000 --- a/src/pydss/dssInstance.py +++ /dev/null @@ -1,529 +0,0 @@ -from pydss.common import SimulationType -from pydss.simulation_input_models import SimulationSettingsModel -from pydss.pyContrReader import read_controller_settings_from_registry -from pydss.dssElementFactory import create_dss_element -from pydss.utils.utils import make_human_readable_size -from pydss.pyContrReader import pyContrReader as pcr -from pydss.exceptions import ( - InvalidConfiguration, PyDssConvergenceError, PyDssConvergenceErrorCountExceeded, - PyDssConvergenceMaxError, OpenDssModelError, OpenDssConvergenceErrorCountExceeded -) -from pydss.ProfileManager import ProfileInterface -from pydss.pyPostprocessor import pyPostprocess -import pydss.pyControllers as pyControllers -from pydss import helics_interface as HI -from pydss.ResultData import ResultData -from pydss.dssCircuit import dssCircuit -from pydss.common import SnapshotTimePointSelectionMode, DATE_FORMAT -from pydss.dssBus import dssBus -from pydss import SolveMode -from pydss.simulation_input_models import SimulationSettingsModel -from pydss.utils.simulation_utils import SimulationFilteredTimeRange -from pydss.utils.timing_utils import Timer, timer_stats_collector, track_timing -from pydss.get_snapshot_timepoints import get_snapshot_timepoint - -import opendssdirect as dss -import numpy as np -from loguru import logger -import json -import time -import os -from collections import defaultdict -from pathlib import Path - -from opendssdirect.utils import run_command - -CONTROLLER_PRIORITIES = 3 - -class OpenDSS: - def __init__(self, settings: SimulationSettingsModel): - self._dssInstance = dss - self._TempResultList = [] - self._dssBuses = {} - self._dssObjects = {} - self._dssObjectsByClass = {} - self._DelFlag = 0 - self._settings = settings - self._convergenceErrors = 0 - self._convergenceErrorsOpenDSS = 0 - self._maxConvergenceErrorCount = None - self._maxConvergenceError = 0.0 - self._controller_iteration_counts = {} - self._simulation_range = SimulationFilteredTimeRange.from_settings(settings) - - root_path = settings.project.project_path - active_project_path = root_path / settings.project.active_project - import_path = active_project_path / 'Scenarios' - active_scenario_path = import_path / settings.project.active_scenario - self._ActiveProject = settings.project.active_project - - self._dssPath = { - 'root': root_path, - 'Import': import_path, - 'ExportLists': active_scenario_path / 'ExportLists', - 'pyControllers': active_scenario_path / 'pyControllerList', - 'Export': active_project_path / 'Exports', - 'Log': active_project_path / 'Logs', - 'dssFiles': active_project_path / 'DSSfiles', - 'dssFilePath': active_project_path / 'DSSfiles' / settings.project.dss_file, - } - - if settings.project.dss_file_absolute_path: - self._dssPath['dssFilePath'] = Path(settings.project.dss_file) - - if not self._dssPath['dssFilePath'].exists(): - raise InvalidConfiguration(f"DSS file {self._dssPath['dssFilePath']} does not exist") - - logger.info('An instance of OpenDSS version ' + self._dssInstance.__version__ + ' has been created.') - - for key, path in self._dssPath.items(): - if path.name == "pyControllerList" and not path.exists(): - # This will happen if a zipped project with no controllers is unzipped and then run. - path.mkdir() - else: - assert path.exists(), '{} path: {} does not exist!'.format(key, path) - - self._compile_model() - - logger.info('OpenDSS fundamental frequency set to : ' + str(settings.frequency.fundamental_frequency) + ' Hz') - - #run_command('Set %SeriesRL={}'.format(settings.frequency.percentage_load_in_series)) - if settings.frequency.neglect_shunt_admittance: - run_command('Set NeglectLoadY=Yes') - - active_scenario = self._GetActiveScenario() - if active_scenario.snapshot_time_point_selection_config.mode != SnapshotTimePointSelectionMode.NONE: - self._SetSnapshotTimePoint(active_scenario) - - self._dssCircuit = self._dssInstance.Circuit - self._dssElement = self._dssInstance.Element - self._dssBus = self._dssInstance.Bus - self._dssClass = self._dssInstance.ActiveClass - self._dssCommand = run_command - self._dssSolution = self._dssInstance.Solution - self._dssSolver = SolveMode.GetSolver(settings=settings, dssInstance=self._dssInstance) - self._dssBuses = self.CreateBusObjects() - self._dssObjects, self._dssObjectsByClass = self.CreateDssObjects(self._dssBuses) - self._dssSolver.reSolve() - - if settings.profiles.use_profile_manager: - #TODO: disable internal profiles - logger.info('Disabling internal yearly and duty-cycle profiles.') - for m in ["Loads", "PVSystem", "Generator", "Storage"]: - run_command(f'BatchEdit {m}..* yearly=NONE duty=None') - profileSettings = self._settings.profiles.settings - profileSettings["objects"] = self._dssObjects - self.profileStore = ProfileInterface.Create( - self._dssInstance, self._dssSolver, self._settings, logger, **profileSettings - ) - - self.ResultContainer = ResultData(settings, self._dssPath, self._dssObjects, self._dssObjectsByClass, - self._dssBuses, self._dssSolver, self._dssCommand, self._dssInstance) - - if settings.project.use_controller_registry: - ControllerList = read_controller_settings_from_registry(self._dssPath['pyControllers']) - else: - pyCtrlReader = pcr(self._dssPath['pyControllers']) - ControllerList = pyCtrlReader.pyControllers - - if ControllerList is not None: - self._CreateControllers(ControllerList) - - self._increment_flag = True - if settings.helics.co_simulation_mode: - self._heilcs_interface = HI.helics_interface(self._dssSolver, self._dssObjects, self._dssObjectsByClass, settings, - self._dssPath) - logger.info("Simulation initialization complete") - return - - @track_timing(timer_stats_collector) - def _compile_model(self): - self._dssInstance.Basic.ClearAll() - self._dssInstance.utils.run_command('Log=NO') - run_command('Clear') - logger.info('Loading OpenDSS model') - reply = "" - try: - orig_dir = os.getcwd() - reply = run_command('compile ' + str(self._dssPath['dssFilePath'])) - finally: - os.chdir(orig_dir) - - logger.info('OpenDSS: ' + reply) - if reply != "": - raise OpenDssModelError(f"Error compiling OpenDSS model: {reply}") - - def _CreateControllers(self, ControllerDict): - self._pyControls = {} - self._pyControls_types = {} - for ControllerType, ElementsDict in ControllerDict.items(): - for ElmName, SettingsDict in ElementsDict.items(): - Controller = pyControllers.pyController.Create(ElmName, ControllerType, SettingsDict, self._dssObjects, - self._dssInstance, self._dssSolver) - if Controller != -1: - controller_name = 'Controller.' + ElmName - self._pyControls[controller_name] = Controller - class_name, element_name = Controller.ControlledElement().split(".") - if controller_name not in self._pyControls_types: - self._pyControls_types[controller_name] = class_name - logger.info('Created pyController -> Controller.' + ElmName) - return - - def _update_controllers(self, Priority, Time, Iteration, UpdateResults): - errors = [] - maxError = 0 - _pyControls_types = set(self._pyControls_types.values()) - - for class_name in _pyControls_types: - self._dssInstance.Basic.SetActiveClass(class_name) - elm = self._dssInstance.ActiveClass.First() - while elm: - element_name = self._dssInstance.CktElement.Name() - controller_name = 'Controller.' + element_name - if controller_name in self._pyControls: - controller = self._pyControls[controller_name] - error = controller.Update(Priority, Time, UpdateResults) - maxError = error if error > maxError else maxError - elm = self._dssInstance.ActiveClass.Next() - return maxError < self._settings.project.error_tolerance, maxError - - @staticmethod - def CreateBusObjects(): - dssBuses = {} - BusNames = dss.Circuit.AllBusNames() - dss.run_command('New Fault.DEFAULT Bus1={} enabled=no r=0.01'.format(BusNames[0])) - for BusName in BusNames: - dss.Circuit.SetActiveBus(BusName) - dssBuses[BusName] = dssBus() - return dssBuses - - @staticmethod - def CreateDssObjects(dssBuses): - dssObjects = {} - dssObjectsByClass = defaultdict(dict) - - InvalidSelection = ['Settings', 'ActiveClass', 'dss', 'utils', 'PDElements', 'XYCurves', 'Bus', 'Properties'] - # TODO: this causes a segmentation fault. Aadil says it may not be needed. - #self._dssObjectsByClass={'LoadShape': self._get_relavent_object_dict('LoadShape')} - - for ElmName in dss.Circuit.AllElementNames(): - Class, Name = ElmName.split('.', 1) - ClassName = Class + 's' - dss.Circuit.SetActiveElement(ElmName) - dssObjectsByClass[ClassName][ElmName] = create_dss_element(Class, Name) - dssObjects[ElmName] = dssObjectsByClass[ClassName][ElmName] - - for ObjName in dssObjects.keys(): - ClassName = ObjName.split('.')[0] + 's' - if ObjName not in dssObjectsByClass[Class]: - dssObjectsByClass[Class][ObjName] = dssObjects[ObjName] - - dssObjects['Circuit.' + dss.Circuit.Name()] = dssCircuit() - dssObjectsByClass['Circuits'] = { - 'Circuit.' + dss.Circuit.Name(): dssObjects['Circuit.' + dss.Circuit.Name()] - } - dssObjectsByClass['Buses'] = dssBuses - - return dssObjects, dssObjectsByClass - - def _get_relavent_object_dict(self, key): - object_list = {} - element_collection = getattr(self._dssInstance, key) - element = element_collection.First() - while element: - fullName = self._dssInstance.Element.Name() - object, name = fullName.split('.', 1) - object_list[fullName] = create_dss_element(object, name, self._dssInstance) - element = element_collection.Next() - return object_list - - @track_timing(timer_stats_collector) - def RunStep(self, step, updateObjects=None): - # updating parameters before simulation run - if self._settings.logging.log_time_step_updates: - logger.info(f'Pydss datetime - {self._dssSolver.GetDateTime()}') - logger.info(f'OpenDSS time [h] - {self._dssSolver.GetOpenDSSTime()}') - if self._settings.profiles.use_profile_manager: - self.profileStore.update() - - if self._settings.helics.co_simulation_mode: - self._heilcs_interface.updateHelicsSubscriptions() - else: - if updateObjects: - for object, params in updateObjects.items(): - cl, name = object.split('.') - self._Modifier.Edit_Element(cl, name, params) - - # run simulation time step and get results - time_step_has_converged = True - if not self._settings.project.disable_pydss_controllers: - with Timer(timer_stats_collector, "UpdateControllers"): - for priority in range(CONTROLLER_PRIORITIES): - priority_has_converged = False - for i in range(self._settings.project.max_control_iterations): - has_converged, error = self._update_controllers(priority, step, i, UpdateResults=False) - logger.debug('Control Loop {} convergence error: {}'.format(priority, error)) - if has_converged: - priority_has_converged = True - break - self._dssSolver.reSolve() - if i == 0: - # Don't track 0. - pass - elif i not in self._controller_iteration_counts: - self._controller_iteration_counts[i] = 1 - else: - self._controller_iteration_counts[i] += 1 - if not priority_has_converged: - time_step_has_converged = False - logger.warning('Control Loop {} no convergence @ {} '.format(priority, step)) - self._HandleConvergenceErrorChecks(step, error) - - - if self._settings.frequency.enable_frequency_sweep and \ - self._settings.project.simulation_type != SimulationType.DYNAMIC: - self._dssSolver.setMode('Harmonic') - for frequency in np.arange(self._settings.frequency.start_frequency, - self._settings.frequency.end_frequency + 1, - self._settings.frequency.frequency_increment): - self._dssSolver.setFrequency(frequency * self._settings.frequency.fundamental_frequency) - self._dssSolver.reSolve() - if self._settings.exports.export_results: - self.ResultContainer.UpdateResults() - if self._settings.project.simulation_type != SimulationType.SNAPSHOT: - self._dssSolver.setMode('Snapshot') - else: - self._dssSolver.setMode('Yearly') - - if self._settings.helics.co_simulation_mode: - self._heilcs_interface.updateHelicsPublications() - self._increment_flag, helics_time = self._heilcs_interface.request_time_increment() - - return time_step_has_converged - - def _HandleConvergenceErrorChecks(self, step, error): - self._convergenceErrors += 1 - - if self._maxConvergenceError != 0.0 and error > self._maxConvergenceError: - logger.error("Convergence error %s exceeded max value %s at step %s", error, self._maxConvergenceError, step) - raise PyDssConvergenceMaxError(f"Exceeded max convergence error {error}") - - if self._maxConvergenceErrorCount is not None and self._convergenceErrors > self._maxConvergenceErrorCount: - logger.error("Exceeded convergence error count threshold at step %s", step) - raise PyDssConvergenceErrorCountExceeded(f"{self._convergenceErrors} errors occurred") - - def _HandleOpenDSSConvergenceErrorChecks(self, step): - self._convergenceErrorsOpenDSS += 1 - - if self._maxConvergenceErrorCount is not None and self._convergenceErrorsOpenDSS > self._maxConvergenceErrorCount: - logger.error("Exceeded OpenDSS convergence error count threshold at step %s", step) - raise OpenDssConvergenceErrorCountExceeded(f"{self._convergenceErrorsOpenDSS} errors occurred") - - def DryRunSimulation(self, project, scenario): - """Run one time point for getting estimated space.""" - if not self._settings.exports.export_results: - raise InvalidConfiguration("Log Reults must set to be True.") - - Steps, _, _ = self._dssSolver.SimulationSteps() - logger.info('Dry run simulation...') - self.ResultContainer.InitializeDataStore(project.hdf_store, Steps) - - try: - self.RunStep(0) - self.ResultContainer.UpdateResults() - finally: - self.ResultContainer.Close() - - return self.ResultContainer.max_num_bytes() - - def initStore(self, hdf_store, Steps, MC_scenario_number=None): - self.ResultContainer.InitializeDataStore(hdf_store, Steps, MC_scenario_number) - - def RunSimulation(self, project, scenario, MC_scenario_number=None): - """Yields a tuple of the results of each step. - - Yields - ------ - tuple - is_complete, step, has_converged, results - - """ - startTime = time.time() - Steps, sTime, eTime = self._dssSolver.SimulationSteps() - threshold = self._settings.project.convergence_error_percent_threshold - if threshold > 0: - self._maxConvergenceErrorCount = round(threshold * .01 * Steps) - self._maxConvergenceError = self._settings.project.max_error_tolerance - dss.Solution.Convergence(self._settings.project.error_tolerance) - logger.info('Running simulation from {} till {}.'.format(sTime, eTime)) - logger.info('Simulation time step {}.'.format(Steps)) - logger.info("Set OpenDSS convergence to %s", dss.Solution.Convergence()) - logger.info('Max convergence error count {}.'.format(self._maxConvergenceErrorCount)) - logger.info("initializing store") - self.ResultContainer.InitializeDataStore(project.hdf_store, Steps, MC_scenario_number) - postprocessors = [ - pyPostprocess.Create( - project, - scenario, - ppInfo, - self._dssInstance, - self._dssSolver, - self._dssObjects, - self._dssObjectsByClass, - self._settings, - logger, - ) for ppInfo in scenario.post_process_infos - ] - if not postprocessors: - logger.info('No post processing script selected') - - is_complete = False - step = 0 - has_converged = False - current_results = {} - try: - while step < Steps: - pydss_has_converged = True - opendss_has_converged = True - within_range = self._simulation_range.is_within_range(self._dssSolver.GetDateTime()) - if within_range: - pydss_has_converged = self.RunStep(step) - opendss_has_converged = dss.Solution.Converged() - if not opendss_has_converged: - logger.error("OpenDSS did not converge at step=%s pydss_converged=%s", - step, pydss_has_converged) - self._HandleOpenDSSConvergenceErrorChecks(step) - has_converged = pydss_has_converged and opendss_has_converged - if step == 0 and self.ResultContainer is not None: - size = make_human_readable_size(self.ResultContainer.max_num_bytes()) - logger.info('Storage requirement estimation: %s, estimated based on first time step run.', size) - if postprocessors and within_range: - step, has_converged = self._RunPostProcessors(step, Steps, postprocessors) - if self._increment_flag: - step += 1 - - # In the case of a frequency sweep, the code updates results at each frequency. - # Doing so again would cause a duplicate result. - if ( - self._settings.exports.export_results and not - (self._settings.frequency.enable_frequency_sweep and \ - self._settings.project.simulation_type != SimulationType.DYNAMIC) - ): - store_nan = ( - not within_range or - (not has_converged and - self._settings.project.skip_export_on_convergence_error) - ) - self.ResultContainer.UpdateResults(store_nan=store_nan) - - if self._settings.helics.co_simulation_mode: - if self._increment_flag: - self._dssSolver.IncStep() - else: - self._dssSolver.reSolve() - else: - self._dssSolver.IncStep() - - if self._settings.exports.export_results: - current_results = self.ResultContainer.CurrentResults - yield False, step, has_converged, current_results - - finally: - if self._settings and self._settings.exports.export_results: - # This is here to guarantee that DatasetBuffers aren't left - # with any data in memory. - self.ResultContainer.Close() - - for postprocessor in postprocessors: - postprocessor.finalize() - - if self._settings and self._settings.exports.export_results: - self.ResultContainer.ExportResults() - - timer_stats_collector.log_stats(clear=True) - if self._controller_iteration_counts: - data = { - "Report": "ControllerIterationCounts", - "Scenario": self._settings.project.active_scenario, - "Counts": self._controller_iteration_counts, - } - - logger.info(f'Simulation completed in { time.time() - startTime} seconds',) - logger.info('End of simulation') - yield True, step, has_converged, current_results - - def _RunPostProcessors(self, step, Steps, postprocessors): - for postprocessor in postprocessors: - orig_step = step - step, has_converged, error = postprocessor.run(step, Steps, simulation=self) - assert step <= orig_step, "step cannot increment in postprocessor" - if not has_converged: - name = postprocessor.__class__.__name__ - logger.warn("postprocessor %s reported a convergence error at step %s", name, step) - self._HandleConvergenceErrorChecks(step, error) - - return step, has_converged - - def RunMCsimulation(self, project, scenario, samples): - from pydss.Extensions.MonteCarlo import MonteCarloSim - MC = MonteCarloSim(self._settings, self._dssPath, self._dssObjects, self._dssObjectsByClass) - for i in range(samples): - MC.Create_Scenario() - for is_complete, _, _, _ in self.RunSimulation(project, scenario, i): - if is_complete: - break - return - - def _GetActiveScenario(self): - active_scenario = self._settings.project.active_scenario - for scenario in self._settings.project.scenarios: - if scenario.name == active_scenario: - return scenario - raise InvalidConfiguration(f"Active Scenario {active_scenario} is not present") - - @track_timing(timer_stats_collector) - def _SetSnapshotTimePoint(self, scenario): - """Adjusts the time parameters based on the mode.""" - p_settings = self._settings.project - config = scenario.snapshot_time_point_selection_config - mode = config.mode - assert mode != SnapshotTimePointSelectionMode.NONE, mode - - if mode != SnapshotTimePointSelectionMode.NONE: - if p_settings.simulation_type != SimulationType.QSTS: - raise InvalidConfiguration(f"{mode} is only supported with QSTS simulations") - - # These settings have to be temporarily overridden because of the underlying - # implementation to create a load shape dataframes.. - orig_start = p_settings.start_time - orig_duration = p_settings.simulation_duration_min - if orig_duration != p_settings.step_resolution_sec / 60: - raise InvalidConfiguration("Simulation duration must be the same as resolution") - try: - p_settings.start_time = config.start_time - p_settings.simulation_duration_min = config.search_duration_min - new_start = get_snapshot_timepoint(self._settings, mode).strftime(DATE_FORMAT) - p_settings.start_time = new_start - logger.info("Changed simulation start time from %s to %s", - orig_start, - new_start, - ) - except Exception: - p_settings.start_time = orig_start - raise - finally: - p_settings.simulation_duration_min = orig_duration - else: - assert False, f"unsupported mode {mode}" - - # def __del__(self): - # logger.info('An instance of OpenDSS (' + str(self) + ') has been deleted.') - # loggers = [logger, self._reportsLogger] - # if self._settings["Logging"]["Log to external file"]: - # for L in loggers: - # handlers = list(L.handlers) - # for filehandler in handlers: - # filehandler.flush() - # filehandler.close() - # L.removeHandler(filehandler) - # return diff --git a/src/pydss/dssObjectBase.py b/src/pydss/dssObjectBase.py deleted file mode 100644 index 82fb5f2e..00000000 --- a/src/pydss/dssObjectBase.py +++ /dev/null @@ -1,127 +0,0 @@ - -import abc - -from pydss.exceptions import InvalidParameter -from pydss.value_storage import ValueByLabel, ValueByList, ValueByNumber -import numpy as np - -class dssObjectBase(abc.ABC): - - VARIABLE_OUTPUTS_BY_LABEL = {} - VARIABLE_OUTPUTS_BY_LIST = () - VARIABLE_OUTPUTS_COMPLEX = () - - def __init__(self, dssInstance, name, fullName): - self._Name = name - self._FullName = fullName - self._Variables = {} - self._dssInstance = dssInstance - self._Enabled = True - self._CachedValueStorage = {} - - @property - def dss(self): - return self._dssInstance - - @property - def Enabled(self): - return self._Enabled - - @abc.abstractmethod - def SetActiveObject(self): - """Set the active DSS object.""" - - def _get_labels(self, VarName): - pass - - def DataLength(self, VarName): - self.SetActiveObject() - if VarName in self._Variables: - VarValue = self.GetVariable(VarName) - else: - return 0, None - if isinstance(VarValue, list): - return len(VarValue) , 'List' - elif isinstance(VarValue, str): - return 1, 'String' - elif isinstance(VarValue, int or float or bool): - return 1, 'Number' - else: - return 0, None - - def GetInfo(self): - return self._Name - - def GetValue(self, VarName, convert=False): - self.SetActiveObject() - if VarName in self._Variables: - VarValue = self.GetVariable(VarName, convert=convert) - else: - VarValue = np.nan - return VarValue - - def GetVariable(self, VarName, convert=False): - if VarName not in self._Variables: - raise InvalidParameter(f'{VarName} is an invalid variable name for element {self._FullName}') - if self._dssInstance.Element.Name() != self._FullName: - self.SetActiveObject() - func = self._Variables[VarName] - if func is None: - raise InvalidParameter(f"get function for {self._FullName} / {VarName} is None") - - value = func() - if not convert: - return value - - if VarName in self.VARIABLE_OUTPUTS_BY_LABEL: - info = self.VARIABLE_OUTPUTS_BY_LABEL[VarName] - is_complex = info["is_complex"] - units = info["units"] - return ValueByLabel(self._FullName, VarName, value, self._Nodes, is_complex, units) - elif VarName in self.VARIABLE_OUTPUTS_COMPLEX: - assert isinstance(value, list) and len(value) == 2, str(value) - value = complex(value[0], value[1]) - elif VarName in self.VARIABLE_OUTPUTS_BY_LIST: - assert isinstance(value, list), str(value) - labels = [f"_bus_index_{i}" for i in range(len(value))] - return ValueByList(self._FullName, VarName, value, labels) - return ValueByNumber(self._FullName, VarName, value) - - def UpdateValue(self, VarName): - - cachedValue = self._CachedValueStorage.get(VarName) - if cachedValue is None: - cachedValue = self.GetValue(VarName, convert=True) - self._CachedValueStorage[VarName] = cachedValue - else: - value = self.GetValue(VarName, convert=False) - if isinstance(cachedValue, ValueByNumber) and VarName in self.VARIABLE_OUTPUTS_COMPLEX: - value = complex(value[0], value[1]) - cachedValue.set_value_from_raw(value) - - return cachedValue - - def GetVariableNames(self): - return self._Variables.keys() - - def inVariableDict(self, VarName): - return VarName in self._Variables - - def IsValidAttribute(self, VarName): - return self.inVariableDict(VarName) - - @property - def FullName(self): - return self._FullName - - @property - def Name(self): - return self._Name - - def SetVariable(self, VarName, Value): - if self._dssInstance.Element.Name() != self._FullName: - self.SetActiveObject() - if VarName not in self._Variables: - raise InvalidParameter(f"invalid variable name {VarName}") - - return self._Variables[VarName](Value) diff --git a/src/pydss/dssTransformer.py b/src/pydss/dssTransformer.py deleted file mode 100644 index 1907db69..00000000 --- a/src/pydss/dssTransformer.py +++ /dev/null @@ -1,82 +0,0 @@ - -from pydss.dssElement import dssElement -from pydss.value_storage import ValueByNumber -from pydss.value_storage import ValueByList -import ast -from pydss.exceptions import InvalidParameter - -class dssTransformer(dssElement): - - VARIABLE_OUTPUTS_BY_LABEL = { - "Currents": { - "is_complex": True, - "units": ['[Amps]'] - }, - "CurrentsMagAng": { - "is_complex": False, - "units": ['[Amps]', '[Deg]'] - }, - "Powers": { - "is_complex": True, - "units": ['[kVA]'] - }, - "Voltages": { - "is_complex": True, - "units": ['[kV]'] - }, - 'VoltagesMagAng': { - "is_complex": False, - "units": ['[kV]', '[Deg]'] - }, - 'CplxSeqCurrents': { - "is_complex": True, - "units": ['[Amps]'] - }, - 'SeqCurrents': { - "is_complex": False, - "units": ['[Amps]', '[Deg]'] - }, - 'SeqPowers': { - "is_complex": False, - "units": ['[kVA]', '[Deg]'] - } - } - - VARIABLE_OUTPUTS_COMPLEX = ( - 'Losses', - ) - - VARIABLE_OUTPUTS_BY_LIST = [ - 'taps' - ] - - def __init__(self, dssInstance): - super(dssTransformer, self).__init__(dssInstance) - self._NumWindings = dssInstance.Transformers.NumWindings() - self._dssInstance = dssInstance - - @property - def NumWindings(self): - return self._NumWindings - - @staticmethod - def chunk_list(values, nLists): - return [values[i * nLists:(i + 1) * nLists] for i in range((len(values) + nLists - 1) // nLists)] - - def GetValue(self, VarName, convert=False): - if VarName in self._Variables: - VarValue = self.GetVariable(VarName, convert=convert) - elif VarName in self._Parameters: - VarValue = self.GetParameter(VarName) - if convert: - if VarName in self.VARIABLE_OUTPUTS_BY_LIST: - VarValue = VarValue[:self.NumWindings] - VarValue = ValueByList( - self._FullName, VarName, VarValue, ['wdg{}'.format(i+1) for i in range(self.NumWindings)] - ) - else: - VarValue = ValueByNumber(self._FullName, VarName, VarValue) - - else: - return None - return VarValue diff --git a/src/pydss/dss_bus.py b/src/pydss/dss_bus.py new file mode 100644 index 00000000..caa02978 --- /dev/null +++ b/src/pydss/dss_bus.py @@ -0,0 +1,68 @@ +import opendssdirect as dss + +from pydss.dss_object_base import DssObjectBase + + +class DssBus(DssObjectBase): + VARIABLE_OUTPUTS_BY_LABEL = { + "PuVoltage": {"is_complex": True, "units": ["[pu]"]}, + "SeqVoltages": {"is_complex": False, "units": ["[kV]", "[Deg]"]}, + "CplxSeqVoltages": {"is_complex": True, "units": ["[kV]"]}, + "VMagAngle": {"is_complex": False, "units": ["[kV]", "[Deg]"]}, + "Voc": {"is_complex": True, "units": ["[kV]"]}, + "Voltages": {"is_complex": True, "units": ["[kV]"]}, + "puVmagAngle": {"is_complex": False, "units": ["[pu]", "[Deg]"]}, + "Isc": {"is_complex": True, "units": ["[Amps]"]}, + } + VARIABLE_OUTPUTS_COMPLEX = () + + def __init__(self, dss_instance=None): + if dss_instance is None: + dss_instance = dss + name = dss_instance.Bus.Name() + super(DssBus, self).__init__(dss_instance, name, name) + self._Index = None + self.XY = None + self._Class = "Bus" + # self._Nodes is nested in a list to be consistent with DssElement._Nodes + self._Nodes = [dss_instance.Bus.Nodes()] + self._NumTerminals = 1 + self._NumConductors = len(dss_instance.Bus.Nodes()) + self.Distance = dss_instance.Bus.Distance() + bus_var_dict = dss_instance.Bus.__dict__ + for key in bus_var_dict.keys(): + try: + self._Variables[key] = getattr(dss_instance.Bus, key) + except Exception: + self._Variables[key] = None + if self.get_variable("X") is not None: + self.XY = [self.get_variable("X"), self.get_variable("Y")] + else: + self.XY = [0, 0] + + @property + def num_conductors(self): + return self._NumConductors + + @property + def num_phases(self): + return len(self._Nodes) + + @property + def phases(self): + return self._Nodes[:] + + def set_active_object(self): + try: + if self._dssInstance.Bus.Name() != self._Name: + self._dssInstance.Circuit.SetActiveBus(self._Name) + return 1 + except Exception: + return 0 + + +globals()["dssBus"] = DssBus + +setattr(DssBus, "NumConductors", DssBus.num_conductors) +setattr(DssBus, "NumPhases", DssBus.num_phases) +setattr(DssBus, "Phases", DssBus.phases) diff --git a/src/pydss/dss_circuit.py b/src/pydss/dss_circuit.py new file mode 100644 index 00000000..2c202c29 --- /dev/null +++ b/src/pydss/dss_circuit.py @@ -0,0 +1,35 @@ +import opendssdirect as dss + +from pydss.dss_object_base import DssObjectBase + + +class DssCircuit(DssObjectBase): + VARIABLE_OUTPUTS_BY_LABEL = {} + VARIABLE_OUTPUTS_BY_LIST = ("AllBusMagPu",) + VARIABLE_OUTPUTS_COMPLEX = ( + "LineLosses", + "Losses", + "SubstationLosses", + "TotalPower", + ) + + def __init__(self, dss_instance=None): + if dss_instance is None: + dss_instance = dss + name = dss_instance.Circuit.Name() + full_name = "Circuit." + name + self._Class = "Circuit" + super(DssCircuit, self).__init__(dss_instance, name, full_name) + + ckt_elm_var_dict = dss_instance.Circuit.__dict__ + for key in ckt_elm_var_dict.keys(): + try: + self._Variables[key] = getattr(dss_instance.Circuit, key) + except Exception: + self._Variables[key] = None + + def set_active_object(self): + pass + + +globals()["dssCircuit"] = DssCircuit diff --git a/src/pydss/dss_element.py b/src/pydss/dss_element.py new file mode 100644 index 00000000..f0e49bdd --- /dev/null +++ b/src/pydss/dss_element.py @@ -0,0 +1,194 @@ +import ast + +from opendssdirect import DSSException + +from pydss.dss_bus import DssBus +from pydss.dss_object_base import DssObjectBase +from pydss.exceptions import InvalidParameter +from pydss.value_storage import ValueByNumber + + +class DssElement(DssObjectBase): + VARIABLE_OUTPUTS_BY_LABEL = { + "Currents": {"is_complex": True, "units": ["[Amps]"]}, + "CurrentsMagAng": {"is_complex": False, "units": ["[Amps]", "[Deg]"]}, + "Powers": {"is_complex": True, "units": ["[kVA]"]}, + "Voltages": {"is_complex": True, "units": ["[kV]"]}, + "VoltagesMagAng": {"is_complex": False, "units": ["[kV]", "[Deg]"]}, + "CplxSeqCurrents": {"is_complex": True, "units": ["[Amps]"]}, + "SeqCurrents": {"is_complex": False, "units": ["[Amps]", "[Deg]"]}, + "SeqPowers": {"is_complex": False, "units": ["[kVA]", "[Deg]"]}, + } + + VARIABLE_OUTPUTS_COMPLEX = ("Losses",) + + _MAX_CONDUCTORS = 4 + + def __init__(self, dss_instance): + full_name = dss_instance.Element.Name() + if dss_instance.CktElement.Name() != full_name: + raise Exception(f"name mismatch {dss_instance.CktElement.Name()} {full_name}") + + self._Class, name = full_name.split(".", 1) + super(DssElement, self).__init__(dss_instance, name, full_name) + self._Enabled = dss_instance.CktElement.Enabled() + if not self._Enabled: + return + + self._Parameters = {} + self._NumTerminals = dss_instance.CktElement.NumTerminals() + self._NumConductors = dss_instance.CktElement.NumConductors() + + assert self._NumConductors <= self._MAX_CONDUCTORS, str(self._NumConductors) + self._NumPhases = dss_instance.CktElement.NumPhases() + + n = self._NumConductors + nodes = dss_instance.CktElement.NodeOrder() + self._Nodes = [nodes[i * n : (i + 1) * n] for i in range((len(nodes) + n - 1) // n)] + + assert len(nodes) == self._NumTerminals * self._NumConductors, ( + f"{self._Nodes} {self._NumTerminals} {self._NumConductors}" + ) + + self._dssInstance = dss_instance + + properties_names = self._dssInstance.Element.AllPropertyNames() + as_ = range(len(properties_names)) + for i, ppt_name in zip(as_, properties_names): + self._Parameters[ppt_name] = str(i) + + ckt_elm_var_dict = dss_instance.CktElement.__dict__ + try: + for var_name in dss_instance.CktElement.AllVariableNames(): + ckt_elm_var_dict[var_name] = None + except DSSException as e: + # Prior to OpenDSSDirect.py v0.8.0 this returned an empty list for non-PC elements. + # v0.8.0 and later raises an exception. Ignore the error. + if e.args[1] != "The active circuit element is not a PC Element": + raise + + for key in ckt_elm_var_dict.keys(): + try: + self._Variables[key] = getattr(dss_instance.CktElement, key) + except Exception: + self._Variables[key] = None + self.Bus = dss_instance.CktElement.BusNames() + self.BusCount = len(self.Bus) + self.sBus = [] + for bus_name in self.Bus: + self._dssInstance.Circuit.SetActiveBus(bus_name) + self.sBus.append(DssBus(self._dssInstance)) + + def get_info(self): + return self._Class, self._Name + + def is_valid_attribute(self, var_name): + # Overridden from base because DssElement has Parameters. + if var_name in self._Variables: + return True + elif var_name in self._Parameters: + return True + else: + return False + + def data_length(self, var_name): + if var_name in self._Variables: + var_value = self.get_variable(var_name) + elif var_name in self._Parameters: + var_value = self.get_parameter(var_name) + else: + return 0, None + + if isinstance(var_value, list): + return len(var_value), "List" + elif isinstance(var_value, str): + return 1, "String" + elif isinstance(var_value, int or float or bool): + return 1, "Number" + else: + return 0, None + + def get_value(self, var_name, convert=False): + if self._dssInstance.Element.Name() != self._FullName: + self.set_active_object() + if var_name in self._Variables: + var_value = self.get_variable(var_name, convert=convert) + elif var_name in self._Parameters: + var_value = self.get_parameter(var_name) + if convert: + var_value = ValueByNumber(self._FullName, var_name, var_value) + else: + return None + return var_value + + def set_active_object(self): + self._dssInstance.Circuit.SetActiveElement(self._FullName) + if self._dssInstance.CktElement.Name() != self._dssInstance.Element.Name(): + raise InvalidParameter("Object is not a circuit element") + + def set_parameter(self, param, value): + reply = self._dssInstance.utils.run_command( + self._FullName + "." + param + " = " + str(value) + ) + if reply != "": + raise Exception(f"SetParameter failed: {reply}") + return self.get_parameter(param) + + def get_parameter(self, param): + if self._dssInstance.Element.Name() != self._FullName: + self._dssInstance.Circuit.SetActiveElement(self._FullName) + if self._dssInstance.Element.Name() == self._FullName: + # This always returns a string. + # The real value could be a number, a list of numbers, or a string. + x = self._dssInstance.Properties.Value(param) + try: + return float(x) + except ValueError: + try: + return ast.literal_eval(x) + except (SyntaxError, ValueError): + return x + else: + return None + + @property + def conductors(self): + letters = "ABCN" + return [letters[i] for i in range(self._NumConductors)] + + @property + def conductor_by_terminal(self): + return [f"{j}{i}" for i in self.conductors for j in self.terminals] + + @property + def node_order(self): + return self._NodeOrder[:] + + @property + def num_phases(self): + return self._NumPhases + + @property + def num_conductors(self): + return self._NumConductors + + @property + def num_terminals(self): + return self._NumTerminals + + @property + def terminals(self): + return list(range(1, self._NumTerminals + 1)) + + +globals()["dssElement"] = DssElement + +setattr(DssElement, "SetParameter", DssElement.set_parameter) +setattr(DssElement, "GetParameter", DssElement.get_parameter) +setattr(DssElement, "Conductors", DssElement.conductors) +setattr(DssElement, "ConductorByTerminal", DssElement.conductor_by_terminal) +setattr(DssElement, "NodeOrder", DssElement.node_order) +setattr(DssElement, "NumPhases", DssElement.num_phases) +setattr(DssElement, "NumConductors", DssElement.num_conductors) +setattr(DssElement, "NumTerminals", DssElement.num_terminals) +setattr(DssElement, "Terminals", DssElement.terminals) diff --git a/src/pydss/dssElementFactory.py b/src/pydss/dss_element_factory.py similarity index 52% rename from src/pydss/dssElementFactory.py rename to src/pydss/dss_element_factory.py index 67f6103d..e1dddff7 100644 --- a/src/pydss/dssElementFactory.py +++ b/src/pydss/dss_element_factory.py @@ -1,14 +1,14 @@ import opendssdirect as dss -from pydss.dssTransformer import dssTransformer -from pydss.dssElement import dssElement +from pydss.dss_transformer import DssTransformer +from pydss.dss_element import DssElement def create_dss_element(element_class, element_name, dss_instance=None): """Instantiate the correct class for the given element_class and element_name.""" if dss_instance is None: dss_instance = dss - if element_class == "Transformer": - return dssTransformer(dss_instance) + if element_class in {"Transformer", "Transformers"}: + return DssTransformer(dss_instance) else: - return dssElement(dss_instance) + return DssElement(dss_instance) diff --git a/src/pydss/dss_instance.py b/src/pydss/dss_instance.py new file mode 100644 index 00000000..ace26d3b --- /dev/null +++ b/src/pydss/dss_instance.py @@ -0,0 +1,623 @@ +from pydss.common import SimulationType +from pydss.simulation_input_models import SimulationSettingsModel +from pydss.py_contr_reader import read_controller_settings_from_registry +from pydss.dss_element_factory import create_dss_element +from pydss.utils.utils import make_human_readable_size +from pydss.py_contr_reader import PyContrReader +from pydss.exceptions import ( + InvalidConfiguration, + PyDssConvergenceErrorCountExceeded, + PyDssConvergenceMaxError, + OpenDssModelError, + OpenDssConvergenceErrorCountExceeded, +) +from pydss.profile_manager import profile_interface +from pydss.py_postprocessor import py_postprocess +from pydss.py_controllers import py_controller +from pydss.helics_interface import HelicsInterface +from pydss.result_data import ResultData +from pydss.dss_circuit import DssCircuit +from pydss.common import SnapshotTimePointSelectionMode, DATE_FORMAT +from pydss.dss_bus import DssBus +from pydss import solve_mode +from pydss.utils.simulation_utils import SimulationFilteredTimeRange +from pydss.utils.timing_utils import timer_stats_collector, track_timing +from pydss.get_snapshot_timepoints import get_snapshot_timepoint +from pydss.openmdao_components import ( + CircuitExplicitComponent, + OpenDSSCircuitAdapter, + VariableSpec, +) +from pydss.openmdao_model import build_openmdao_problem, controller_circuit_variable + +import opendssdirect as dss +import openmdao.api as om +import numpy as np +from loguru import logger +import time +import os +from collections import defaultdict +from pathlib import Path + +from opendssdirect.utils import run_command + + +class OpenDSS: + def __init__(self, settings: SimulationSettingsModel): + self._dssInstance = dss + self._TempResultList = [] + self._dssBuses = {} + self._dssObjects = {} + self._dssObjectsByClass = {} + self._DelFlag = 0 + self._settings = settings + self._convergenceErrors = 0 + self._convergenceErrorsOpenDSS = 0 + self._maxConvergenceErrorCount = None + self._maxConvergenceError = 0.0 + self._controller_iteration_counts = {} + self._simulation_range = SimulationFilteredTimeRange.from_settings(settings) + + root_path = settings.project.project_path + active_project_path = root_path / settings.project.active_project + import_path = active_project_path / "Scenarios" + active_scenario_path = import_path / settings.project.active_scenario + self._ActiveProject = settings.project.active_project + + self._dssPath = { + "root": root_path, + "Import": import_path, + "ExportLists": active_scenario_path / "ExportLists", + "py_controllers": active_scenario_path / "pyControllerList", + "Export": active_project_path / "Exports", + "Log": active_project_path / "Logs", + "dssFiles": active_project_path / "DSSfiles", + "dssFilePath": active_project_path / "DSSfiles" / settings.project.dss_file, + } + + if settings.project.dss_file_absolute_path: + self._dssPath["dssFilePath"] = Path(settings.project.dss_file) + + if not self._dssPath["dssFilePath"].exists(): + raise InvalidConfiguration(f"DSS file {self._dssPath['dssFilePath']} does not exist") + + logger.info( + "An instance of OpenDSS version " + self._dssInstance.__version__ + " has been created." + ) + + for key, path in self._dssPath.items(): + if path.name == "pyControllerList" and not path.exists(): + # This will happen if a zipped project with no controllers is unzipped and then run. + path.mkdir() + else: + assert path.exists(), "{} path: {} does not exist!".format(key, path) + + self._compile_model() + + logger.info( + "OpenDSS fundamental frequency set to : " + + str(settings.frequency.fundamental_frequency) + + " Hz" + ) + + # run_command('Set %SeriesRL={}'.format(settings.frequency.percentage_load_in_series)) + if settings.frequency.neglect_shunt_admittance: + run_command("Set NeglectLoadY=Yes") + + active_scenario = self._GetActiveScenario() + if ( + active_scenario.snapshot_time_point_selection_config.mode + != SnapshotTimePointSelectionMode.NONE + ): + self._SetSnapshotTimePoint(active_scenario) + + self._dssCircuit = self._dssInstance.Circuit + self._dssElement = self._dssInstance.Element + self._dssBus = self._dssInstance.Bus + self._dssClass = self._dssInstance.ActiveClass + self._dssCommand = run_command + self._dssSolution = self._dssInstance.Solution + self._dssSolver = solve_mode.get_solver(settings=settings, dss_instance=self._dssInstance) + self._dssBuses = self.create_bus_objects() + self._dssObjects, self._dssObjectsByClass = self.create_dss_objects(self._dssBuses) + self._dssSolver.reSolve() + + if settings.profiles.use_profile_manager: + # TODO: disable internal profiles + logger.info("Disabling internal yearly and duty-cycle profiles.") + for m in ["Loads", "PVSystem", "Generator", "Storage"]: + run_command(f"BatchEdit {m}..* yearly=NONE duty=None") + profile_settings = self._settings.profiles.settings + profile_settings["objects"] = self._dssObjects + self.profileStore = profile_interface.create( + self._dssInstance, self._dssSolver, self._settings, logger, **profile_settings + ) + + self.ResultContainer = ResultData( + settings, + self._dssPath, + self._dssObjects, + self._dssObjectsByClass, + self._dssBuses, + self._dssSolver, + self._dssCommand, + self._dssInstance, + ) + + if settings.project.use_controller_registry: + controller_list = read_controller_settings_from_registry( + self._dssPath["py_controllers"] + ) + else: + py_ctrl_reader = PyContrReader(self._dssPath["py_controllers"]) + controller_list = py_ctrl_reader.pyControllers + + if controller_list is not None: + self._create_controllers(controller_list) + + self._openmdao_problem = None + self._openmdao_adapter = None + if getattr(self, "_pyControls", None): + self._setup_openmdao_problem() + + self._increment_flag = True + if settings.helics.co_simulation_mode: + self._heilcs_interface = HelicsInterface( + self._dssSolver, self._dssObjects, self._dssObjectsByClass, settings, self._dssPath + ) + logger.info("Simulation initialization complete") + return + + @track_timing(timer_stats_collector) + def _compile_model(self): + self._dssInstance.Basic.ClearAll() + self._dssInstance.utils.run_command("Log=NO") + run_command("Clear") + logger.info("Loading OpenDSS model") + reply = "" + try: + orig_dir = os.getcwd() + reply = run_command("compile " + str(self._dssPath["dssFilePath"])) + finally: + os.chdir(orig_dir) + + logger.info("OpenDSS: " + reply) + if reply != "": + raise OpenDssModelError(f"Error compiling OpenDSS model: {reply}") + + def _create_controllers(self, controller_dict): + self._pyControls = {} + for controller_type, elements_dict in controller_dict.items(): + for element_name, settings_dict in elements_dict.items(): + self._pyControls[element_name] = py_controller.create( + controller_type, settings_dict, element_name=element_name + ) + logger.info("Created OpenMDAO controller -> Controller." + element_name) + return + + def _setup_openmdao_problem(self): + controller_items = sorted(self._pyControls.items()) + elements = {name: self._dssObjects[name] for name, _ in controller_items} + command_specs = [] + measurement_specs = [] + command_map = {} + measurement_map = {} + controllers = [] + connections = [] + + for index, (element_name, controller) in enumerate(controller_items): + subsystem_name = f"controller_{index}" + controllers.append((subsystem_name, controller)) + element_class = element_name.split(".", 1)[0] + if element_class.lower() == "pvsystem": + controller.options["rated_kva"] = float(elements[element_name].get_parameter("kVA")) + parameter_map = controller.command_parameters(element_class) + for spec in controller.output_specs(): + if spec.name.startswith("command_"): + circuit_name = controller_circuit_variable(index, spec.name) + command_specs.append( + VariableSpec(circuit_name, spec.shape, spec.units, spec.val) + ) + parameter = parameter_map[spec.name] + command_map[circuit_name] = (element_name, parameter) + connections.append((f"{subsystem_name}.{spec.name}", f"circuit.{circuit_name}")) + for spec in controller.input_specs(): + circuit_name = controller_circuit_variable(index, spec.name) + measurement_specs.append( + VariableSpec(circuit_name, spec.shape, spec.units, spec.val) + ) + measurement_map[circuit_name] = (element_name, spec.name) + connections.append((f"circuit.{circuit_name}", f"{subsystem_name}.{spec.name}")) + + self._openmdao_adapter = OpenDSSCircuitAdapter( + self._dssInstance, self._dssSolver, elements, command_map, measurement_map + ) + circuit = CircuitExplicitComponent( + adapter=self._openmdao_adapter, + command_specs=tuple(command_specs), + measurement_specs=tuple(measurement_specs), + ) + reports_enabled = ( + self._settings.project.openmdao_reports and "PYTEST_CURRENT_TEST" not in os.environ + ) + self._openmdao_problem = build_openmdao_problem( + circuit, + controllers, + connections, + max_iterations=self._settings.project.max_control_iterations, + tolerance=self._settings.project.error_tolerance, + reports=reports_enabled, + work_dir=( + self._dssPath["dssFiles"].parent / "OpenMDAOReports" if reports_enabled else None + ), + name=self._ActiveProject, + ) + + @staticmethod + def create_bus_objects(): + dss_buses = {} + bus_names = dss.Circuit.AllBusNames() + dss.run_command("New Fault.DEFAULT Bus1={} enabled=no r=0.01".format(bus_names[0])) + for bus_name in bus_names: + dss.Circuit.SetActiveBus(bus_name) + dss_buses[bus_name] = DssBus() + return dss_buses + + @staticmethod + def create_dss_objects(dss_buses): + dss_objects = {} + dss_objects_by_class = defaultdict(dict) + + # TODO: this causes a segmentation fault. Aadil says it may not be needed. + # self._dssObjectsByClass={'LoadShape': self._get_relavent_object_dict('LoadShape')} + + for element_name in dss.Circuit.AllElementNames(): + class_name, object_name = element_name.split(".", 1) + class_name = class_name + "s" + dss.Circuit.SetActiveElement(element_name) + dss_objects_by_class[class_name][element_name] = create_dss_element( + class_name, object_name + ) + dss_objects[element_name] = dss_objects_by_class[class_name][element_name] + + for object_name in dss_objects.keys(): + class_name = object_name.split(".")[0] + "s" + if object_name not in dss_objects_by_class[class_name]: + dss_objects_by_class[class_name][object_name] = dss_objects[object_name] + + dss_objects["Circuit." + dss.Circuit.Name()] = DssCircuit() + dss_objects_by_class["Circuits"] = { + "Circuit." + dss.Circuit.Name(): dss_objects["Circuit." + dss.Circuit.Name()] + } + dss_objects_by_class["Buses"] = dss_buses + + return dss_objects, dss_objects_by_class + + def _get_relavent_object_dict(self, key): + object_list = {} + element_collection = getattr(self._dssInstance, key) + element = element_collection.First() + while element: + full_name = self._dssInstance.Element.Name() + object, name = full_name.split(".", 1) + object_list[full_name] = create_dss_element(object, name, self._dssInstance) + element = element_collection.Next() + return object_list + + @track_timing(timer_stats_collector) + def run_step(self, step, update_objects=None): + # updating parameters before simulation run + if self._settings.logging.log_time_step_updates: + logger.info(f"Pydss datetime - {self._dssSolver.GetDateTime()}") + logger.info(f"OpenDSS time [h] - {self._dssSolver.GetOpenDSSTime()}") + if self._settings.profiles.use_profile_manager: + self.profileStore.update() + + if self._settings.helics.co_simulation_mode: + self._heilcs_interface.update_helics_subscriptions() + else: + if update_objects: + for object, params in update_objects.items(): + cl, name = object.split(".") + self._Modifier.Edit_Element(cl, name, params) + + # run simulation time step and get results + time_step_has_converged = True + if self._openmdao_problem is not None: + self._openmdao_adapter.set_time(step * self._settings.project.step_resolution_sec) + self._openmdao_problem.model.reset_step() + try: + self._openmdao_problem.run_model() + except om.AnalysisError as exc: + logger.warning("OpenMDAO time step {} did not converge: {}", step, exc) + self._handle_convergence_error_checks(step, float("inf")) + time_step_has_converged = False + else: + self._openmdao_problem.model.commit_step() + + if ( + self._settings.frequency.enable_frequency_sweep + and self._settings.project.simulation_type != SimulationType.DYNAMIC + ): + self._dssSolver.setMode("Harmonic") + for frequency in np.arange( + self._settings.frequency.start_frequency, + self._settings.frequency.end_frequency + 1, + self._settings.frequency.frequency_increment, + ): + self._dssSolver.setFrequency( + frequency * self._settings.frequency.fundamental_frequency + ) + self._dssSolver.reSolve() + if self._settings.exports.export_results: + self.ResultContainer.update_results() + if self._settings.project.simulation_type != SimulationType.SNAPSHOT: + self._dssSolver.setMode("Snapshot") + else: + self._dssSolver.setMode("Yearly") + + if self._settings.helics.co_simulation_mode: + self._heilcs_interface.update_helics_publications() + self._increment_flag, helics_time = self._heilcs_interface.request_time_increment() + + return time_step_has_converged + + def _handle_convergence_error_checks(self, step, error): + self._convergenceErrors += 1 + + if self._maxConvergenceError != 0.0 and error > self._maxConvergenceError: + logger.error( + "Convergence error %s exceeded max value %s at step %s", + error, + self._maxConvergenceError, + step, + ) + raise PyDssConvergenceMaxError(f"Exceeded max convergence error {error}") + + if ( + self._maxConvergenceErrorCount is not None + and self._convergenceErrors > self._maxConvergenceErrorCount + ): + logger.error("Exceeded convergence error count threshold at step %s", step) + raise PyDssConvergenceErrorCountExceeded(f"{self._convergenceErrors} errors occurred") + + def _handle_opendss_convergence_error_checks(self, step): + self._convergenceErrorsOpenDSS += 1 + + if ( + self._maxConvergenceErrorCount is not None + and self._convergenceErrorsOpenDSS > self._maxConvergenceErrorCount + ): + logger.error("Exceeded OpenDSS convergence error count threshold at step %s", step) + raise OpenDssConvergenceErrorCountExceeded( + f"{self._convergenceErrorsOpenDSS} errors occurred" + ) + + def dry_run_simulation(self, project, scenario): + """Run one time point for getting estimated space.""" + if not self._settings.exports.export_results: + raise InvalidConfiguration("Log Reults must set to be True.") + + steps, _, _ = self._dssSolver.SimulationSteps() + logger.info("Dry run simulation...") + self.ResultContainer.initialize_data_store(project.hdf_store, steps) + + try: + self.run_step(0) + self.ResultContainer.update_results() + finally: + self.ResultContainer.close() + + return self.ResultContainer.max_num_bytes() + + def init_store(self, hdf_store, steps, mc_scenario_number=None): + self.ResultContainer.initialize_data_store(hdf_store, steps, mc_scenario_number) + + def run_simulation(self, project, scenario, mc_scenario_number=None): + """Yields a tuple of the results of each step. + + Yields + ------ + tuple + is_complete, step, has_converged, results + + """ + elapsed_start_time = time.time() + steps, simulation_start_time, simulation_end_time = self._dssSolver.SimulationSteps() + threshold = self._settings.project.convergence_error_percent_threshold + if threshold > 0: + self._maxConvergenceErrorCount = round(threshold * 0.01 * steps) + self._maxConvergenceError = self._settings.project.max_error_tolerance + dss.Solution.Convergence(self._settings.project.error_tolerance) + logger.info( + "Running simulation from {} till {}.".format(simulation_start_time, simulation_end_time) + ) + logger.info("Simulation time step {}.".format(steps)) + logger.info("Set OpenDSS convergence to %s", dss.Solution.Convergence()) + logger.info("Max convergence error count {}.".format(self._maxConvergenceErrorCount)) + logger.info("initializing store") + self.ResultContainer.initialize_data_store(project.hdf_store, steps, mc_scenario_number) + postprocessors = [ + py_postprocess.create( + project, + scenario, + ppInfo, + self._dssInstance, + self._dssSolver, + self._dssObjects, + self._dssObjectsByClass, + self._settings, + logger, + ) + for ppInfo in scenario.post_process_infos + ] + if not postprocessors: + logger.info("No post processing script selected") + + step = 0 + has_converged = False + current_results = {} + try: + while step < steps: + pydss_has_converged = True + opendss_has_converged = True + within_range = self._simulation_range.is_within_range(self._dssSolver.GetDateTime()) + if within_range: + pydss_has_converged = self.run_step(step) + opendss_has_converged = dss.Solution.Converged() + if not opendss_has_converged: + logger.error( + "OpenDSS did not converge at step=%s pydss_converged=%s", + step, + pydss_has_converged, + ) + self._handle_opendss_convergence_error_checks(step) + has_converged = pydss_has_converged and opendss_has_converged + if step == 0 and self.ResultContainer is not None: + size = make_human_readable_size(self.ResultContainer.max_num_bytes()) + logger.info( + "Storage requirement estimation: %s, estimated based on first time step run.", + size, + ) + if postprocessors and within_range: + step, has_converged = self._RunPostProcessors(step, steps, postprocessors) + if self._increment_flag: + step += 1 + + # In the case of a frequency sweep, the code updates results at each frequency. + # Doing so again would cause a duplicate result. + if self._settings.exports.export_results and not ( + self._settings.frequency.enable_frequency_sweep + and self._settings.project.simulation_type != SimulationType.DYNAMIC + ): + store_nan = not within_range or ( + not has_converged + and self._settings.project.skip_export_on_convergence_error + ) + self.ResultContainer.update_results(store_nan=store_nan) + + if self._settings.helics.co_simulation_mode: + if self._increment_flag: + self._dssSolver.IncStep() + else: + self._dssSolver.reSolve() + else: + self._dssSolver.IncStep() + + if self._settings.exports.export_results: + current_results = self.ResultContainer.current_results + yield False, step, has_converged, current_results + + finally: + if self._settings and self._settings.exports.export_results: + # This is here to guarantee that DatasetBuffers aren't left + # with any data in memory. + self.ResultContainer.close() + + for postprocessor in postprocessors: + postprocessor.finalize() + + if self._settings and self._settings.exports.export_results: + self.ResultContainer.export_results() + + timer_stats_collector.log_stats(clear=True) + if self._controller_iteration_counts: + pass + + logger.info( + f"Simulation completed in {time.time() - elapsed_start_time} seconds", + ) + logger.info("End of simulation") + yield True, step, has_converged, current_results + + def _run_post_processors(self, step, steps, postprocessors): + for postprocessor in postprocessors: + orig_step = step + step, has_converged, error = postprocessor.run(step, steps, simulation=self) + assert step <= orig_step, "step cannot increment in postprocessor" + if not has_converged: + name = postprocessor.__class__.__name__ + logger.warn("postprocessor %s reported a convergence error at step %s", name, step) + self._handle_convergence_error_checks(step, error) + + return step, has_converged + + def run_mc_simulation(self, project, scenario, samples): + from pydss.extensions.monte_carlo import MonteCarloSim + + monte_carlo = MonteCarloSim( + self._settings, self._dssPath, self._dssObjects, self._dssObjectsByClass + ) + for i in range(samples): + monte_carlo.create_scenario() + for is_complete, _, _, _ in self.run_simulation(project, scenario, i): + if is_complete: + break + return + + def _get_active_scenario(self): + active_scenario = self._settings.project.active_scenario + for scenario in self._settings.project.scenarios: + if scenario.name == active_scenario: + return scenario + raise InvalidConfiguration(f"Active Scenario {active_scenario} is not present") + + @track_timing(timer_stats_collector) + def _set_snapshot_time_point(self, scenario): + """Adjusts the time parameters based on the mode.""" + p_settings = self._settings.project + config = scenario.snapshot_time_point_selection_config + mode = config.mode + assert mode != SnapshotTimePointSelectionMode.NONE, mode + + if mode != SnapshotTimePointSelectionMode.NONE: + if p_settings.simulation_type != SimulationType.QSTS: + raise InvalidConfiguration(f"{mode} is only supported with QSTS simulations") + + # These settings have to be temporarily overridden because of the underlying + # implementation to create a load shape dataframes.. + orig_start = p_settings.start_time + orig_duration = p_settings.simulation_duration_min + if orig_duration != p_settings.step_resolution_sec / 60: + raise InvalidConfiguration("Simulation duration must be the same as resolution") + try: + p_settings.start_time = config.start_time + p_settings.simulation_duration_min = config.search_duration_min + new_start = get_snapshot_timepoint(self._settings, mode).strftime(DATE_FORMAT) + p_settings.start_time = new_start + logger.info( + "Changed simulation start time from %s to %s", + orig_start, + new_start, + ) + except Exception: + p_settings.start_time = orig_start + raise + finally: + p_settings.simulation_duration_min = orig_duration + else: + assert False, f"unsupported mode {mode}" + + # def __del__(self): + # logger.info('An instance of OpenDSS (' + str(self) + ') has been deleted.') + # loggers = [logger, self._reportsLogger] + # if self._settings["Logging"]["Log to external file"]: + # for L in loggers: + # handlers = list(L.handlers) + # for filehandler in handlers: + # filehandler.flush() + # filehandler.close() + # L.removeHandler(filehandler) + # return + + +setattr(OpenDSS, "CreateBusObjects", OpenDSS.create_bus_objects) +setattr(OpenDSS, "CreateDssObjects", OpenDSS.create_dss_objects) +setattr(OpenDSS, "RunStep", OpenDSS.run_step) +setattr(OpenDSS, "RunSimulation", OpenDSS.run_simulation) +setattr(OpenDSS, "RunMCsimulation", OpenDSS.run_mc_simulation) +setattr(OpenDSS, "DryRunSimulation", OpenDSS.dry_run_simulation) +setattr(OpenDSS, "initStore", OpenDSS.init_store) +setattr(OpenDSS, "_RunPostProcessors", OpenDSS._run_post_processors) +setattr(OpenDSS, "_GetActiveScenario", OpenDSS._get_active_scenario) +setattr(OpenDSS, "_SetSnapshotTimePoint", OpenDSS._set_snapshot_time_point) diff --git a/src/pydss/dss_object_base.py b/src/pydss/dss_object_base.py new file mode 100644 index 00000000..22f5ffb1 --- /dev/null +++ b/src/pydss/dss_object_base.py @@ -0,0 +1,148 @@ +import abc + +from pydss.exceptions import InvalidParameter +from pydss.value_storage import ValueByLabel, ValueByList, ValueByNumber +import numpy as np + + +class DssObjectBase(abc.ABC): + VARIABLE_OUTPUTS_BY_LABEL = {} + VARIABLE_OUTPUTS_BY_LIST = () + VARIABLE_OUTPUTS_COMPLEX = () + + def __init__(self, dss_instance, name, full_name): + self._Name = name + self._FullName = full_name + self._Variables = {} + self._dssInstance = dss_instance + self._Enabled = True + self._CachedValueStorage = {} + + @property + def dss(self): + return self._dssInstance + + @property + def enabled(self): + return self._Enabled + + @abc.abstractmethod + def set_active_object(self): + """Set the active DSS object.""" + + def _get_labels(self, var_name): + pass + + def data_length(self, var_name): + self.set_active_object() + if var_name in self._Variables: + var_value = self.get_variable(var_name) + else: + return 0, None + if isinstance(var_value, list): + return len(var_value), "List" + elif isinstance(var_value, str): + return 1, "String" + elif isinstance(var_value, int or float or bool): + return 1, "Number" + else: + return 0, None + + def get_info(self): + return self._Name + + def get_value(self, var_name, convert=False): + self.set_active_object() + if var_name in self._Variables: + var_value = self.get_variable(var_name, convert=convert) + else: + var_value = np.nan + return var_value + + def get_variable(self, var_name, convert=False): + if var_name not in self._Variables: + raise InvalidParameter( + f"{var_name} is an invalid variable name for element {self._FullName}" + ) + if self._dssInstance.Element.Name() != self._FullName: + self.set_active_object() + func = self._Variables[var_name] + if func is None: + raise InvalidParameter(f"get function for {self._FullName} / {var_name} is None") + + value = func() + if not convert: + return value + + if var_name in self.VARIABLE_OUTPUTS_BY_LABEL: + info = self.VARIABLE_OUTPUTS_BY_LABEL[var_name] + is_complex = info["is_complex"] + units = info["units"] + return ValueByLabel(self._FullName, var_name, value, self._Nodes, is_complex, units) + elif var_name in self.VARIABLE_OUTPUTS_COMPLEX: + assert isinstance(value, list) and len(value) == 2, str(value) + value = complex(value[0], value[1]) + elif var_name in self.VARIABLE_OUTPUTS_BY_LIST: + assert isinstance(value, list), str(value) + labels = [f"_bus_index_{i}" for i in range(len(value))] + return ValueByList(self._FullName, var_name, value, labels) + return ValueByNumber(self._FullName, var_name, value) + + def update_value(self, var_name): + + cached_value = self._CachedValueStorage.get(var_name) + if cached_value is None: + cached_value = self.get_value(var_name, convert=True) + self._CachedValueStorage[var_name] = cached_value + else: + value = self.get_value(var_name, convert=False) + if ( + isinstance(cached_value, ValueByNumber) + and var_name in self.VARIABLE_OUTPUTS_COMPLEX + ): + value = complex(value[0], value[1]) + cached_value.set_value_from_raw(value) + + return cached_value + + def get_variable_names(self): + return self._Variables.keys() + + def in_variable_dict(self, var_name): + return var_name in self._Variables + + def is_valid_attribute(self, var_name): + return self.in_variable_dict(var_name) + + @property + def full_name(self): + return self._FullName + + @property + def name(self): + return self._Name + + def set_variable(self, var_name, value): + if self._dssInstance.Element.Name() != self._FullName: + self.set_active_object() + if var_name not in self._Variables: + raise InvalidParameter(f"invalid variable name {var_name}") + + return self._Variables[var_name](value) + + +globals()["dssObjectBase"] = DssObjectBase + +setattr(DssObjectBase, "Enabled", DssObjectBase.enabled) +setattr(DssObjectBase, "FullName", DssObjectBase.full_name) +setattr(DssObjectBase, "Name", DssObjectBase.name) +setattr(DssObjectBase, "GetInfo", DssObjectBase.get_info) +setattr(DssObjectBase, "DataLength", DssObjectBase.data_length) +setattr(DssObjectBase, "SetActiveObject", DssObjectBase.set_active_object) +setattr(DssObjectBase, "GetValue", DssObjectBase.get_value) +setattr(DssObjectBase, "GetVariable", DssObjectBase.get_variable) +setattr(DssObjectBase, "UpdateValue", DssObjectBase.update_value) +setattr(DssObjectBase, "GetVariableNames", DssObjectBase.get_variable_names) +setattr(DssObjectBase, "inVariableDict", DssObjectBase.in_variable_dict) +setattr(DssObjectBase, "IsValidAttribute", DssObjectBase.is_valid_attribute) +setattr(DssObjectBase, "SetVariable", DssObjectBase.set_variable) diff --git a/src/pydss/dss_transformer.py b/src/pydss/dss_transformer.py new file mode 100644 index 00000000..70b76845 --- /dev/null +++ b/src/pydss/dss_transformer.py @@ -0,0 +1,62 @@ +from pydss.dss_element import DssElement +from pydss.value_storage import ValueByNumber +from pydss.value_storage import ValueByList + + +class DssTransformer(DssElement): + VARIABLE_OUTPUTS_BY_LABEL = { + "Currents": {"is_complex": True, "units": ["[Amps]"]}, + "CurrentsMagAng": {"is_complex": False, "units": ["[Amps]", "[Deg]"]}, + "Powers": {"is_complex": True, "units": ["[kVA]"]}, + "Voltages": {"is_complex": True, "units": ["[kV]"]}, + "VoltagesMagAng": {"is_complex": False, "units": ["[kV]", "[Deg]"]}, + "CplxSeqCurrents": {"is_complex": True, "units": ["[Amps]"]}, + "SeqCurrents": {"is_complex": False, "units": ["[Amps]", "[Deg]"]}, + "SeqPowers": {"is_complex": False, "units": ["[kVA]", "[Deg]"]}, + } + + VARIABLE_OUTPUTS_COMPLEX = ("Losses",) + + VARIABLE_OUTPUTS_BY_LIST = ["taps"] + + def __init__(self, dss_instance): + super(DssTransformer, self).__init__(dss_instance) + self._NumWindings = dss_instance.Transformers.NumWindings() + self._dssInstance = dss_instance + + @property + def num_windings(self): + return self._NumWindings + + @staticmethod + def chunk_list(values, n_lists): + return [ + values[i * n_lists : (i + 1) * n_lists] + for i in range((len(values) + n_lists - 1) // n_lists) + ] + + def get_value(self, var_name, convert=False): + if var_name in self._Variables: + var_value = self.get_variable(var_name, convert=convert) + elif var_name in self._Parameters: + var_value = self.get_parameter(var_name) + if convert: + if var_name in self.VARIABLE_OUTPUTS_BY_LIST: + var_value = var_value[: self.num_windings] + var_value = ValueByList( + self._FullName, + var_name, + var_value, + ["wdg{}".format(i + 1) for i in range(self.num_windings)], + ) + else: + var_value = ValueByNumber(self._FullName, var_name, var_value) + + else: + return None + return var_value + + +globals()["dssTransformer"] = DssTransformer + +setattr(DssTransformer, "NumWindings", DssTransformer.num_windings) diff --git a/src/pydss/element_fields.py b/src/pydss/element_fields.py index 90014fb8..7526b88a 100644 --- a/src/pydss/element_fields.py +++ b/src/pydss/element_fields.py @@ -1,159 +1,180 @@ ELEMENT_FIELDS = { "Lines": [ { - "names": ["Currents", "Powers", 'Voltages', 'PhaseLosses'], + "names": ["Currents", "Powers", "Voltages", "PhaseLosses"], "options": [ "phase_terminal", ], }, { - "names": ["SeqVoltages", "SeqPowers", 'SeqCurrents', 'VoltagesMagAng', 'CurrentsMagAng'], - "options": [ - "phase_terminal", "mag_ang" - ], + "names": [ + "SeqVoltages", + "SeqPowers", + "SeqCurrents", + "VoltagesMagAng", + "CurrentsMagAng", + ], + "options": ["phase_terminal", "mag_ang"], }, { "names": ["Losses", "NormalAmps"], - "options": [ - ], + "options": [], }, ], "Transformers": [ { - "names": ["Currents", "Powers", 'Voltages', 'PhaseLosses'], + "names": ["Currents", "Powers", "Voltages", "PhaseLosses"], "options": [ "phase_terminal", ], }, { - "names": ["SeqVoltages", "SeqPowers", 'SeqCurrents', 'VoltagesMagAng', 'CurrentsMagAng'], - "options": [ - "phase_terminal", "mag_ang" - ], + "names": [ + "SeqVoltages", + "SeqPowers", + "SeqCurrents", + "VoltagesMagAng", + "CurrentsMagAng", + ], + "options": ["phase_terminal", "mag_ang"], }, { - "names": ["taps", "maxtap", 'mintap', 'numtaps', 'kv', 'kva'], + "names": ["taps", "maxtap", "mintap", "numtaps", "kv", "kva"], "options": [ "wdg", ], }, { "names": ["Losses", "NormalAmps", "tap"], + "options": [], + }, + ], + "Generators": [ + { + "names": ["Currents", "Powers", "Voltages", "PhaseLosses"], "options": [ + "phase_terminal", ], }, + { + "names": [ + "SeqVoltages", + "SeqPowers", + "SeqCurrents", + "VoltagesMagAng", + "CurrentsMagAng", + ], + "options": ["phase_terminal", "mag_ang"], + }, ], - "Generators": [ - { - "names": ["Currents", "Powers", 'Voltages', 'PhaseLosses'], - "options": [ - "phase_terminal", - ], - }, - { - "names": ["SeqVoltages", "SeqPowers", 'SeqCurrents', 'VoltagesMagAng', 'CurrentsMagAng'], - "options": [ - "phase_terminal", "mag_ang" - ], - }, - ], "PVSystems": [ - { - "names": ["Currents", "Powers", 'Voltages', 'PhaseLosses'], - "options": [ - "phase_terminal", - ], - }, - { - "names": ["SeqVoltages", "SeqPowers", 'SeqCurrents', 'VoltagesMagAng', 'CurrentsMagAng'], - "options": [ - "phase_terminal", "mag_ang" - ], - }, - { - "names": ["Pmpp"], - "options": [ - ], - }, + { + "names": ["Currents", "Powers", "Voltages", "PhaseLosses"], + "options": [ + "phase_terminal", ], + }, + { + "names": [ + "SeqVoltages", + "SeqPowers", + "SeqCurrents", + "VoltagesMagAng", + "CurrentsMagAng", + ], + "options": ["phase_terminal", "mag_ang"], + }, + { + "names": ["Pmpp"], + "options": [], + }, + ], "Loads": [ - { - "names": ["Currents", "Powers", 'Voltages', 'PhaseLosses'], - "options": [ - "phase_terminal", - ], - }, - { - "names": ["SeqVoltages", "SeqPowers", 'SeqCurrents', 'VoltagesMagAng', 'CurrentsMagAng'], - "options": [ - "phase_terminal", "mag_ang" - ], - }, + { + "names": ["Currents", "Powers", "Voltages", "PhaseLosses"], + "options": [ + "phase_terminal", ], + }, + { + "names": [ + "SeqVoltages", + "SeqPowers", + "SeqCurrents", + "VoltagesMagAng", + "CurrentsMagAng", + ], + "options": ["phase_terminal", "mag_ang"], + }, + ], "Storages": [ { - "names": ["Currents", "Powers", 'Voltages', 'PhaseLosses'], + "names": ["Currents", "Powers", "Voltages", "PhaseLosses"], "options": [ "phase_terminal", ], }, { - "names": ["SeqVoltages", "SeqPowers", 'SeqCurrents', 'VoltagesMagAng', 'CurrentsMagAng'], + "names": [ + "SeqVoltages", + "SeqPowers", + "SeqCurrents", + "VoltagesMagAng", + "CurrentsMagAng", + ], + "options": ["phase_terminal", "mag_ang"], + }, + ], + "Capacitors": [ + { + "names": ["Currents", "Powers", "Voltages", "PhaseLosses"], "options": [ - "phase_terminal", "mag_ang" + "phase_terminal", ], }, + { + "names": [ + "SeqVoltages", + "SeqPowers", + "SeqCurrents", + "VoltagesMagAng", + "CurrentsMagAng", + ], + "options": ["phase_terminal", "mag_ang"], + }, + { + "names": ["states", "open", "close"], + "options": [], + }, ], - "Capacitors": [ - { - "names": ["Currents", "Powers", 'Voltages', 'PhaseLosses'], - "options": [ - "phase_terminal", - ], - }, - { - "names": ["SeqVoltages", "SeqPowers", 'SeqCurrents', 'VoltagesMagAng', 'CurrentsMagAng'], - "options": [ - "phase_terminal", "mag_ang" - ], - }, -{ - "names": ["states", 'open', 'close'], - "options": [ - ], - }, - ], "Meters": [ - { - "names": ["AllocFactors"], - "options": [ - "phase_terminal", - ], - } + { + "names": ["AllocFactors"], + "options": [ + "phase_terminal", ], + } + ], "Buses": [ { - "names": ["CplxSeqVoltages", "Voc", 'Voltages', 'puVLL', 'PuVoltage'], + "names": ["CplxSeqVoltages", "Voc", "Voltages", "puVLL", "PuVoltage"], "options": [ "phase_terminal", ], }, { - "names": ["SeqVoltages", "VMagAngle", 'SeqCurrents', 'puVmagAngle'], - "options": [ - "phase_terminal", "mag_ang" - ], + "names": ["SeqVoltages", "VMagAngle", "SeqCurrents", "puVmagAngle"], + "options": ["phase_terminal", "mag_ang"], }, { "names": ["Distance"], - "options": [ - ], + "options": [], }, ], "Circuits": [ - { - "names": ["TotalPower", "LineLosses", "Losses", "SubstationLosses"], - "options": [], - } - ], + { + "names": ["TotalPower", "LineLosses", "Losses", "SubstationLosses"], + "options": [], + } + ], } diff --git a/src/pydss/element_options.py b/src/pydss/element_options.py index 0a35d6d5..24e7dc5e 100644 --- a/src/pydss/element_options.py +++ b/src/pydss/element_options.py @@ -4,8 +4,10 @@ from pydss.element_fields import ELEMENT_FIELDS + class ElementOptions: """Stores the options available for element classes and properties.""" + def __init__(self, data=None): if data is None: data = ELEMENT_FIELDS diff --git a/src/pydss/exceptions.py b/src/pydss/exceptions.py index a3390c76..8825b9ac 100644 --- a/src/pydss/exceptions.py +++ b/src/pydss/exceptions.py @@ -1,11 +1,11 @@ """Exceptions used in Pydss""" -class InvalidConfiguration(Exception): +class InvalidConfigurationError(Exception): """Raised when a bad configuration is detected.""" -class InvalidParameter(Exception): +class InvalidParameterError(Exception): """Raised when bad user input is detected.""" @@ -13,7 +13,7 @@ class OpenDssConvergenceError(Exception): """Raised when OpenDSS fails to converge on a solution.""" -class OpenDssConvergenceErrorCountExceeded(Exception): +class OpenDssConvergenceErrorCountExceededError(Exception): """Raised when OpenDSS exceeds the threshold of convergence error counts.""" @@ -29,5 +29,11 @@ class PyDssConvergenceMaxError(Exception): """Raised when pydss exceeds a max convergence error threshold.""" -class PyDssConvergenceErrorCountExceeded(Exception): +class PyDssConvergenceErrorCountExceededError(Exception): """Raised when pydss exceeds the threshold of convergence error counts.""" + + +InvalidConfiguration = InvalidConfigurationError +InvalidParameter = InvalidParameterError +OpenDssConvergenceErrorCountExceeded = OpenDssConvergenceErrorCountExceededError +PyDssConvergenceErrorCountExceeded = PyDssConvergenceErrorCountExceededError diff --git a/src/pydss/export_list_reader.py b/src/pydss/export_list_reader.py index d87bd639..3a105636 100644 --- a/src/pydss/export_list_reader.py +++ b/src/pydss/export_list_reader.py @@ -1,5 +1,3 @@ - - from collections import defaultdict from pathlib import Path import os @@ -8,15 +6,18 @@ from loguru import logger -from pydss.common import DataConversion, LimitsFilter, StoreValuesType, \ - DatasetPropertyType, MinMax -from pydss.pyContrReader import pyExportReader +from pydss.common import DataConversion, LimitsFilter, StoreValuesType, DatasetPropertyType, MinMax +from pydss.py_contr_reader import PyExportReader from pydss.utils.utils import load_data from pydss.exceptions import InvalidConfiguration, InvalidParameter from pydss.metrics import ( - NodeVoltageMetric, TrackCapacitorChangeCounts, - TrackRegControlTapNumberChanges, ExportLoadingsMetric, OverloadsMetricInMemory, - ExportPowersMetric, FeederHeadMetrics + NodeVoltageMetric, + TrackCapacitorChangeCounts, + TrackRegControlTapNumberChanges, + ExportLoadingsMetric, + OverloadsMetricInMemory, + ExportPowersMetric, + FeederHeadMetrics, ) @@ -26,15 +27,16 @@ "CktElement.OverloadsMetricInMemory": OverloadsMetricInMemory, "CktElement.ExportPowersMetric": ExportPowersMetric, "FeederHead.FeederHeadMetrics": FeederHeadMetrics, - #"Lines.LoadingPercent": LineLoadingPercent, + # "Lines.LoadingPercent": LineLoadingPercent, "Nodes.VoltageMetric": NodeVoltageMetric, "RegControls.TrackTapNumberChanges": TrackRegControlTapNumberChanges, - #"Transformers.LoadingPercent": TransformerLoadingPercent, + # "Transformers.LoadingPercent": TransformerLoadingPercent, } class ExportListProperty: """Contains export options for an element property.""" + def __init__(self, elem_class, data): self.elem_class = elem_class self._opendss_classes = data.get("opendss_classes", []) @@ -61,11 +63,11 @@ def __init__(self, elem_class, data): # Note to devs: any field added here needs to be handled in serialize() - if self._sum_elements and self._store_values_type not in \ - (StoreValuesType.ALL, StoreValuesType.SUM): - raise InvalidConfiguration( - "sum_elements requires store_values_types = all or sum" - ) + if self._sum_elements and self._store_values_type not in ( + StoreValuesType.ALL, + StoreValuesType.SUM, + ): + raise InvalidConfiguration("sum_elements requires store_values_types = all or sum") if self._is_max() and self._limits is not None: raise InvalidConfiguration("limits are not allowed with max types") @@ -75,7 +77,11 @@ def __init__(self, elem_class, data): "OverloadsMetricInMemory", "ExportPowersMetric", ) - if elem_class == "CktElement" and self.name in requires_opendss_classes and not self._opendss_classes: + if ( + elem_class == "CktElement" + and self.name in requires_opendss_classes + and not self._opendss_classes + ): raise InvalidConfiguration( f"Exporting {elem_class}.{self.name} requires that opendss_classes be specifed" ) @@ -83,7 +89,7 @@ def __init__(self, elem_class, data): def _check_sum_groups(self, sum_groups_file): if sum_groups_file is not None: if self._sum_groups: - raise InvalidConfiguration(f"Cannot set both sum_groups and sum_groups_file") + raise InvalidConfiguration("Cannot set both sum_groups and sum_groups_file") # This path needs to be relative to the current directory, not the Exports.toml. # This might need to be changed. @@ -107,7 +113,8 @@ def _check_sum_groups(self, sum_groups_file): def _is_max(self): return self._store_values_type in ( - StoreValuesType.MAX, StoreValuesType.MOVING_AVERAGE_MAX, + StoreValuesType.MAX, + StoreValuesType.MOVING_AVERAGE_MAX, ) @property @@ -116,7 +123,8 @@ def are_names_filtered(self): def is_moving_average(self): return self._store_values_type in ( - StoreValuesType.MOVING_AVERAGE, StoreValuesType.MOVING_AVERAGE_MAX, + StoreValuesType.MOVING_AVERAGE, + StoreValuesType.MOVING_AVERAGE_MAX, ) @staticmethod @@ -194,19 +202,23 @@ def get_dataset_property_type(self): """ if self._limits is not None: return DatasetPropertyType.FILTERED - if self._store_values_type in \ - (StoreValuesType.SUM, StoreValuesType.MAX, - StoreValuesType.MIN, - StoreValuesType.MOVING_AVERAGE_MAX, - StoreValuesType.CHANGE_COUNT): + if self._store_values_type in ( + StoreValuesType.SUM, + StoreValuesType.MAX, + StoreValuesType.MIN, + StoreValuesType.MOVING_AVERAGE_MAX, + StoreValuesType.CHANGE_COUNT, + ): return DatasetPropertyType.VALUE return DatasetPropertyType.PER_TIME_POINT def get_max_size(self, num_steps): """Return the max number of items that could be stored.""" singles = ( - StoreValuesType.CHANGE_COUNT, StoreValuesType.MAX, - StoreValuesType.MIN, StoreValuesType.MOVING_AVERAGE_MAX, + StoreValuesType.CHANGE_COUNT, + StoreValuesType.MAX, + StoreValuesType.MIN, + StoreValuesType.MOVING_AVERAGE_MAX, StoreValuesType.SUM, ) if self._store_values_type in singles: @@ -250,7 +262,7 @@ def opendss_classes(self): def serialize(self): """Serialize object to a dictionary.""" if self._are_names_regex: - #raise InvalidConfiguration("cannot serialize when names are regex") + # raise InvalidConfiguration("cannot serialize when names are regex") logger.warning("cannot serialize when names are regex") names = None else: @@ -376,6 +388,7 @@ def window_sizes(self): class ExportListReader: """Reads export files and provides access to export properties.""" + def __init__(self, filename): self._elem_classes = defaultdict(list) legacy_files = ("ExportMode-byClass.toml", "ExportMode-byElement.toml") @@ -385,9 +398,7 @@ def __init__(self, filename): parser = self._parse_file for elem_class, data in parser(filename): - self._elem_classes[elem_class].append(ExportListProperty( - elem_class, data - )) + self._elem_classes[elem_class].append(ExportListProperty(elem_class, data)) # TODO: verify that multiple instances of the same property have # the same names. @@ -407,7 +418,7 @@ def _parse_file(filename): @staticmethod def _parse_legacy_file(filename): - reader = pyExportReader(filename) + reader = PyExportReader(filename) publications = {tuple(x.split()) for x in reader.publicationList} for elem_class, props in reader.pyControllers.items(): for prop in props: @@ -451,9 +462,9 @@ def list_element_properties(self, elem_class): def list_element_property_names(self, elem_class): return sorted({x.name for x in self._elem_classes[elem_class]}) - # This name needs to match the interface defined in pyExportReader. + # This compatibility property matches the legacy PyExportReader interface. @property - def publicationList(self): + def publication_list(self): """Return the properties to be published to HELICS. Returns @@ -462,10 +473,7 @@ def publicationList(self): Format: ["ElementClass Property"] """ - return [ - f"{x.elem_class} {x.name}" for x in self.iter_export_properties() - if x.publish - ] + return [f"{x.elem_class} {x.name}" for x in self.iter_export_properties() if x.publish] def serialize(self): """Serialize object to a dictionary.""" @@ -475,3 +483,6 @@ def serialize(self): data[elem_class].append(prop.serialize()) return data + + +setattr(ExportListReader, "publicationList", ExportListReader.publication_list) diff --git a/src/pydss/Extensions/__init__.py b/src/pydss/extensions/__init__.py similarity index 100% rename from src/pydss/Extensions/__init__.py rename to src/pydss/extensions/__init__.py diff --git a/src/pydss/extensions/monte_carlo.py b/src/pydss/extensions/monte_carlo.py new file mode 100644 index 00000000..8286beed --- /dev/null +++ b/src/pydss/extensions/monte_carlo.py @@ -0,0 +1,76 @@ +from ast import literal_eval +import os + +from loguru import logger +from scipy import stats +import numpy as np + +from pydss.simulation_input_models import SimulationSettingsModel +from pydss.utils import utils + + +class MonteCarloSim: + def __init__( + self, + settings: SimulationSettingsModel, + dss_paths, + dss_objects, + dss_objects_by_class, + ): + self.__dss_paths = dss_paths + self.__dss_objects = dss_objects + self._settings = settings + self.__dss_objects_by_class = dss_objects_by_class + + try: + mc_file = os.path.join( + self._settings.project.active_scenario, "Monte_Carlo", "MonteCarloSettings.toml" + ) + mc_file_path = os.path.join(self.__dss_paths["Import"], mc_file) + + logger.info("Reading monte carlo scenario settings file from " + mc_file_path) + self.__mc_settings_dict = utils.load_data(mc_file_path) + except: + logger.error("Failed to read Monte Carlo scenario generation file %s", mc_file_path) + raise + return + + def create_scenario(self): + for properties in self.__mc_settings_dict.values(): + if properties["Class"] in self.__dss_objects_by_class: + elements = self.__dss_objects_by_class[properties["Class"]] + element_names = list(elements.keys()) + if properties["useWildCard"]: + element_names = [x for x in element_names if properties["Wildcard"] in x] + num_elements = len(element_names) + distribution_params = literal_eval(properties["Parameters"]) + + distribution = getattr(stats, properties["Distribution"].replace(" ", "")) + if not properties["isList"]: + samples = distribution.rvs(*distribution_params, size=num_elements) + if properties["isInteger"]: + samples = [int(round(x)) for x in samples] + for element_name, value in zip(element_names, samples): + elements[element_name].SetParameter(properties["Property"], value) + else: + samples = distribution.rvs( + *distribution_params, size=num_elements * properties["ListLength"] + ) + if properties["isInteger"]: + samples = [int(round(x)) for x in samples] + samples = np.reshape(samples, (num_elements, properties["ListLength"])) + for element_name, value in zip(element_names, samples): + value = ( + str(value) + .replace("\n", "") + .replace("\r", "") + .replace("[ ", "[") + .replace(" ]", "]") + ) + elements[element_name].SetParameter(properties["Property"], value) + else: + logger.warning(properties["Class"] + " class not present in object dictionary.") + return + + +MonteCarloSim.Create_Scenario = MonteCarloSim.create_scenario diff --git a/src/pydss/get_snapshot_timepoints.py b/src/pydss/get_snapshot_timepoints.py index a594ea91..d83219fa 100644 --- a/src/pydss/get_snapshot_timepoints.py +++ b/src/pydss/get_snapshot_timepoints.py @@ -11,7 +11,6 @@ from pydss.common import SnapshotTimePointSelectionMode from pydss.utils.simulation_utils import create_loadshape_pmult_dataframe_for_simulation from pydss.utils.utils import dump_data -from pydss.reports.reports import logger from pydss.simulation_input_models import SimulationSettingsModel @@ -33,28 +32,40 @@ def get_snapshot_timepoint(settings: SimulationSettingsModel, mode: SnapshotTime else: assert False, f"{mode} is not supported" - temp_filename = settings.project.project_path / settings.project.active_project / "Exports" / ".snapshot_time_points.json" - final_filename = settings.project.project_path / settings.project.active_project / "Exports" / "snapshot_time_points.json" + temp_filename = ( + settings.project.project_path + / settings.project.active_project + / "Exports" + / ".snapshot_time_points.json" + ) + final_filename = ( + settings.project.project_path + / settings.project.active_project + / "Exports" + / "snapshot_time_points.json" + ) if temp_filename.exists(): timepoints = pd.read_json(temp_filename) if settings.project.active_scenario == settings.project.scenarios[-1].name: os.rename(temp_filename, final_filename) return pd.to_datetime(timepoints[column].iloc[0]).to_pydatetime() - pv_generation_hours = {'start_time': '8:00', 'end_time': '17:00'} - aggregate_profiles = pd.DataFrame(columns=['Load', 'PV']) + pv_generation_hours = {"start_time": "8:00", "end_time": "17:00"} + aggregate_profiles = pd.DataFrame(columns=["Load", "PV"]) pv_shapes = {} for pv_name in pv_systems: dss.PVsystems.Name(pv_name) - pmpp = float(dss.Properties.Value('Pmpp')) - profile_name = dss.Properties.Value('yearly') + pmpp = float(dss.Properties.Value("Pmpp")) + profile_name = dss.Properties.Value("yearly") dss.LoadShape.Name(profile_name) if profile_name not in pv_shapes.keys(): pv_shapes[profile_name] = create_loadshape_pmult_dataframe_for_simulation(settings) if len(aggregate_profiles) == 0: - aggregate_profiles['PV'] = (pv_shapes[profile_name] * pmpp)[0] + aggregate_profiles["PV"] = (pv_shapes[profile_name] * pmpp)[0] aggregate_profiles = aggregate_profiles.replace(np.nan, 0) else: - aggregate_profiles['PV'] = aggregate_profiles['PV'] + (pv_shapes[profile_name] * pmpp)[0] + aggregate_profiles["PV"] = ( + aggregate_profiles["PV"] + (pv_shapes[profile_name] * pmpp)[0] + ) del pv_shapes loads = dss.Loads.AllNames() if not loads: @@ -62,34 +73,42 @@ def get_snapshot_timepoint(settings: SimulationSettingsModel, mode: SnapshotTime load_shapes = {} for load_name in loads: dss.Loads.Name(load_name) - kw = float(dss.Properties.Value('kW')) - profile_name = dss.Properties.Value('yearly') + kw = float(dss.Properties.Value("kW")) + profile_name = dss.Properties.Value("yearly") dss.LoadShape.Name(profile_name) if profile_name not in load_shapes.keys(): load_shapes[profile_name] = create_loadshape_pmult_dataframe_for_simulation(settings) if len(aggregate_profiles) == 0: - aggregate_profiles['Load'] = (load_shapes[profile_name] * kw)[0] + aggregate_profiles["Load"] = (load_shapes[profile_name] * kw)[0] else: - aggregate_profiles['Load'] = aggregate_profiles['Load'] + (load_shapes[profile_name] * kw)[0] + aggregate_profiles["Load"] = ( + aggregate_profiles["Load"] + (load_shapes[profile_name] * kw)[0] + ) del load_shapes if pv_systems: - aggregate_profiles['PV to Load Ratio'] = aggregate_profiles['PV'] / aggregate_profiles['Load'] - aggregate_profiles['PV minus Load'] = aggregate_profiles['PV'] - aggregate_profiles['Load'] + aggregate_profiles["PV to Load Ratio"] = ( + aggregate_profiles["PV"] / aggregate_profiles["Load"] + ) + aggregate_profiles["PV minus Load"] = aggregate_profiles["PV"] - aggregate_profiles["Load"] - timepoints = pd.DataFrame(columns=['Timepoints']) - timepoints.loc['Max Load'] = aggregate_profiles['Load'].idxmax() + timepoints = pd.DataFrame(columns=["Timepoints"]) + timepoints.loc["Max Load"] = aggregate_profiles["Load"].idxmax() if pv_systems: - timepoints.loc['Max PV to Load Ratio'] = aggregate_profiles.between_time(pv_generation_hours['start_time'], - pv_generation_hours['end_time'])['PV to Load Ratio'].idxmax() - timepoints.loc['Max PV minus Load'] = aggregate_profiles.between_time(pv_generation_hours['start_time'], - pv_generation_hours['end_time'])['PV minus Load'].idxmax() - timepoints.loc['Max PV'] = aggregate_profiles.between_time(pv_generation_hours['start_time'], - pv_generation_hours['end_time'])['PV'].idxmax() - timepoints.loc['Min Load'] = aggregate_profiles['Load'].idxmin() - timepoints.loc['Min Daytime Load'] = aggregate_profiles.between_time(pv_generation_hours['start_time'], - pv_generation_hours['end_time'])['Load'].idxmin() + timepoints.loc["Max PV to Load Ratio"] = aggregate_profiles.between_time( + pv_generation_hours["start_time"], pv_generation_hours["end_time"] + )["PV to Load Ratio"].idxmax() + timepoints.loc["Max PV minus Load"] = aggregate_profiles.between_time( + pv_generation_hours["start_time"], pv_generation_hours["end_time"] + )["PV minus Load"].idxmax() + timepoints.loc["Max PV"] = aggregate_profiles.between_time( + pv_generation_hours["start_time"], pv_generation_hours["end_time"] + )["PV"].idxmax() + timepoints.loc["Min Load"] = aggregate_profiles["Load"].idxmin() + timepoints.loc["Min Daytime Load"] = aggregate_profiles.between_time( + pv_generation_hours["start_time"], pv_generation_hours["end_time"] + )["Load"].idxmin() logger.info("Time points: %s", {k: str(v) for k, v in timepoints.to_records()}) - dump_data(timepoints.astype(str).to_dict(orient='index'), temp_filename, indent=2) + dump_data(timepoints.astype(str).to_dict(orient="index"), temp_filename, indent=2) if settings.project.active_scenario == settings.project.scenarios[-1].name: os.rename(temp_filename, final_filename) return timepoints.loc[column].iloc[0].to_pydatetime() diff --git a/src/pydss/helics_interface.py b/src/pydss/helics_interface.py index 5a5faaa1..92068c39 100644 --- a/src/pydss/helics_interface.py +++ b/src/pydss/helics_interface.py @@ -12,35 +12,36 @@ from pydss.utils.utils import load_data TYPE_INFO = { - 'CurrentsMagAng': 'vector', - 'Currents': 'vector', - 'RatedCurrent': 'double', - 'EmergAmps': 'double', - 'NormalAmps': 'double', - 'normamps': 'double', - 'Losses': 'vector', - 'PhaseLosses': 'vector', - 'Powers': 'vector', - 'TotalPower': 'vector', - 'LineLosses': 'vector', - 'SubstationLosses': 'vector', - 'kV': 'double', - 'kVARated': 'double', - 'kvar': 'double', - 'kW': 'double', - 'kVABase': 'double', - 'kWh': 'double', - 'puVmagAngle': 'vector', - 'VoltagesMagAng': 'vector', - 'VMagAngle': 'vector', - 'Voltages': 'vector', - 'Vmaxpu': 'double', - 'Vminpu': 'double', - 'Frequency': 'double', - 'Taps': 'vector', - '%stored': 'double', - 'Distance': 'double' - } + "CurrentsMagAng": "vector", + "Currents": "vector", + "RatedCurrent": "double", + "EmergAmps": "double", + "NormalAmps": "double", + "normamps": "double", + "Losses": "vector", + "PhaseLosses": "vector", + "Powers": "vector", + "TotalPower": "vector", + "LineLosses": "vector", + "SubstationLosses": "vector", + "kV": "double", + "kVARated": "double", + "kvar": "double", + "kW": "double", + "kVABase": "double", + "kWh": "double", + "puVmagAngle": "vector", + "VoltagesMagAng": "vector", + "VMagAngle": "vector", + "Voltages": "vector", + "Vmaxpu": "double", + "Vminpu": "double", + "Frequency": "double", + "Taps": "vector", + "%stored": "double", + "Distance": "double", +} + class DataType(Enum): DOUBLE = "double" @@ -49,11 +50,12 @@ class DataType(Enum): BOOLEAN = "boolean" INTEGER = "integer" + class Subscription(BaseModel): model: str property: str id: str - unit: Optional[str] = None + unit: Optional[str] = None subscribe: bool = True data_type: DataType multiplier: float = 1.0 @@ -62,6 +64,7 @@ class Subscription(BaseModel): sub: Any = None model_config = ConfigDict(arbitrary_types_allowed=True) + class Publication(BaseModel): model: str property: str @@ -69,111 +72,119 @@ class Publication(BaseModel): object: Any = None pub: Any = None data_type: DataType - + + class Subscriptions(BaseModel): federate: Any = None opendss_models: Dict subscriptions: List[Subscription] - @model_validator(mode='after') - def is_in_opendss_model(self)-> 'Subscriptions': + @model_validator(mode="after") + def is_in_opendss_model(self) -> "Subscriptions": for subscription in self.subscriptions: if subscription.model not in self.opendss_models: - raise AssertionError(f"The loaded OpenDSS model does not have an element define with the name {subscription}") - + raise AssertionError( + f"The loaded OpenDSS model does not have an element define with the name {subscription}" + ) + if subscription.subscribe: subscription.object = self.opendss_models[subscription.model] subscription.sub = helics.helicsFederateRegisterSubscription( - self.federate, - subscription.id, - subscription.unit + self.federate, subscription.id, subscription.unit ) return self - + + class Publications(BaseModel): - federate: Any = None federate_name: str opendss_models: Dict publications: List[Publication] = [] legacy_input: Dict = {} input: Dict = {} - - @model_validator(mode='after') - def build_from_legacy(self)-> 'Publications': + + @model_validator(mode="after") + def build_from_legacy(self) -> "Publications": publications = [] for object_type, k in self.legacy_input.items(): if object_type in self.opendss_models: - models = self.opendss_models[object_type] + models = self.opendss_models[object_type] for model in models: - for ppty in k['Publish']: - name = '{}.{}.{}'.format(self.federate_name, model, ppty) + for ppty in k["Publish"]: + name = "{}.{}.{}".format(self.federate_name, model, ppty) pub_dict = { - "model" : model, - "object" : models[model], - "id" : name, - "property" : ppty, - "data_type" : TYPE_INFO[ppty], - "pub" : helics.helicsFederateRegisterGlobalTypePublication( - self.federate, - name, - TYPE_INFO[ppty], - '' - ) + "model": model, + "object": models[model], + "id": name, + "property": ppty, + "data_type": TYPE_INFO[ppty], + "pub": helics.helicsFederateRegisterGlobalTypePublication( + self.federate, name, TYPE_INFO[ppty], "" + ), } publications.append(Publication.model_validate(pub_dict)) self.publications = publications return self - @model_validator(mode='after') - def build_from_export(self)-> 'Publications': + @model_validator(mode="after") + def build_from_export(self) -> "Publications": publications = [] for object_type, export_properties in self.input.items(): if object_type in self.opendss_models: - models = self.opendss_models[object_type] + models = self.opendss_models[object_type] for export_property in export_properties: filtered_models = {} - if export_property['publish']: - if 'names' in export_property and export_property['names']: + if export_property["publish"]: + if "names" in export_property and export_property["names"]: for k, v in models.items(): - if k in export_property['names']: + if k in export_property["names"]: filtered_models[k] = models[k] - elif 'name_regexes' in export_property and export_property['name_regexes']: - for regex_expression in export_property['name_regexes']: + elif "name_regexes" in export_property and export_property["name_regexes"]: + for regex_expression in export_property["name_regexes"]: r = re.compile(regex_expression) matches = list(filter(r.match, models.keys())) for match in matches: filtered_models[match] = models[match] else: filtered_models = models - + for model_name, model_obj in filtered_models.items(): if object_type == "Buses": - name = '{}.Bus.{}.{}'.format(self.federate_name, model_name, export_property["property"]) + name = "{}.Bus.{}.{}".format( + self.federate_name, model_name, export_property["property"] + ) else: - name = '{}.{}.{}'.format(self.federate_name, model_name, export_property["property"]) - pub_dict = { - "model" : model_name, - "object" : model_obj, - "id" : name, - "property" : export_property["property"], - "data_type" : TYPE_INFO[export_property["property"]], - "pub" : helics.helicsFederateRegisterGlobalTypePublication( - self.federate, - name, - TYPE_INFO[export_property["property"]], - '' + name = "{}.{}.{}".format( + self.federate_name, model_name, export_property["property"] ) + pub_dict = { + "model": model_name, + "object": model_obj, + "id": name, + "property": export_property["property"], + "data_type": TYPE_INFO[export_property["property"]], + "pub": helics.helicsFederateRegisterGlobalTypePublication( + self.federate, name, TYPE_INFO[export_property["property"]], "" + ), } publications.append(Publication.model_validate(pub_dict)) - self.publications = publications + self.publications = publications return self -class helics_interface: + +class HelicsInterface: n_states = 5 init_state = 1 - - def __init__(self, dss_solver, objects_by_name, objects_by_class, settings: SimulationSettingsModel, system_paths, default=True): + + def __init__( + self, + dss_solver, + objects_by_name, + objects_by_class, + settings: SimulationSettingsModel, + system_paths, + default=True, + ): self.itr = 0 self.c_seconds = 0 self.c_seconds_old = -1 @@ -187,42 +198,51 @@ def __init__(self, dss_solver, objects_by_name, objects_by_class, settings: Simu self._create_helics_federate() self._dss_solver = dss_solver if default: - self.registerPubSubTags() + self.register_pub_sub_tags() - def registerPubSubTags(self, pubs=None, subs=None): + def register_pub_sub_tags(self, pubs=None, subs=None): - self._registerFederateSubscriptions(subs) - self._registerFederatePublications(pubs) + self._register_federate_subscriptions(subs) + self._register_federate_publications(pubs) helics.helicsFederateEnterExecutingModeIterative( - self._federate, - helics.helics_iteration_request_iterate_if_needed + self._federate, helics.helics_iteration_request_iterate_if_needed ) - logger.info('Entered HELICS execution mode') + logger.info("Entered HELICS execution mode") def _create_helics_federate(self): self.fedinfo = helics.helicsCreateFederateInfo() helics.helicsFederateInfoSetCoreName(self.fedinfo, self._settings.helics.federate_name) - helics.helicsFederateInfoSetCoreTypeFromString(self.fedinfo, self._settings.helics.core_type) - helics.helicsFederateInfoSetCoreInitString(self.fedinfo, f"--federates=1") - IP = self._settings.helics.broker - Port = self._settings.helics.broker_port - logger.info("Connecting to broker @ {}".format(f"{IP}:{Port}" if Port else IP)) + helics.helicsFederateInfoSetCoreTypeFromString( + self.fedinfo, self._settings.helics.core_type + ) + helics.helicsFederateInfoSetCoreInitString(self.fedinfo, "--federates=1") + broker = self._settings.helics.broker + broker_port = self._settings.helics.broker_port + logger.info( + "Connecting to broker @ {}".format(f"{broker}:{broker_port}" if broker_port else broker) + ) if self._settings.helics.broker: helics.helicsFederateInfoSetBroker(self.fedinfo, str(self._settings.helics.broker)) if self._settings.helics.broker_port: helics.helicsFederateInfoSetBrokerPort(self.fedinfo, self._settings.helics.broker_port) - helics.helicsFederateInfoSetTimeProperty(self.fedinfo, helics.helics_property_time_delta, - self._settings.helics.time_delta) - helics.helicsFederateInfoSetIntegerProperty(self.fedinfo, helics.helics_property_int_log_level, - self._settings.helics.logging_level) - helics.helicsFederateInfoSetIntegerProperty(self.fedinfo, helics.helics_property_int_max_iterations, - self._settings.helics.max_co_iterations) - self._federate = helics.helicsCreateValueFederate(self._settings.helics.federate_name, self.fedinfo) + helics.helicsFederateInfoSetTimeProperty( + self.fedinfo, helics.helics_property_time_delta, self._settings.helics.time_delta + ) + helics.helicsFederateInfoSetIntegerProperty( + self.fedinfo, helics.helics_property_int_log_level, self._settings.helics.logging_level + ) + helics.helicsFederateInfoSetIntegerProperty( + self.fedinfo, + helics.helics_property_int_max_iterations, + self._settings.helics.max_co_iterations, + ) + self._federate = helics.helicsCreateValueFederate( + self._settings.helics.federate_name, self.fedinfo + ) return - - def _registerFederateSubscriptions(self, subscriptions:Subscriptions = None): + def _register_federate_subscriptions(self, subscriptions: Subscriptions = None): """ :param subs: :return: @@ -232,11 +252,13 @@ def _registerFederateSubscriptions(self, subscriptions:Subscriptions = None): self._system_paths["ExportLists"], SUBSCRIPTIONS_FILENAME, ) - assert os.path.exists(subscription_file), f"The following file does not exist: {subscription_file}" + assert os.path.exists(subscription_file), ( + f"The following file does not exist: {subscription_file}" + ) file_data = load_data(subscription_file) file_data["opendss_models"] = self._objects_by_element file_data["federate"] = self._federate - + self.subscriptions = Subscriptions.model_validate(file_data) else: self.subscriptions = subscriptions @@ -245,7 +267,7 @@ def _registerFederateSubscriptions(self, subscriptions:Subscriptions = None): logger.info(f"subscription created: {subscription}") return - def updateHelicsSubscriptions(self): + def update_helics_subscriptions(self): for subscription in self.subscriptions.subscriptions: if subscription.subscribe: value = None @@ -259,18 +281,18 @@ def updateHelicsSubscriptions(self): value = helics.helicsInputGetBoolean(subscription.sub) elif subscription.data_type == DataType.INTEGER: value = helics.helicsInputGetInteger(subscription.sub) - + if value and value != 0: if value > 1e6 or value < -1e6: - value = 1.0 + value = 1.0 value = value * subscription.multiplier - subscription.object.SetParameter(subscription.property, value) - logger.info('Value for "{}.{}" changed to "{}"'.format( - subscription.model, - subscription.property, - value - )) + subscription.object.SetParameter(subscription.property, value) + logger.info( + 'Value for "{}.{}" changed to "{}"'.format( + subscription.model, subscription.property, value + ) + ) if self._settings.helics.iterative_mode: if self.c_seconds != self.c_seconds_old: @@ -280,46 +302,46 @@ def updateHelicsSubscriptions(self): subscription.states[0] = value self.c_seconds_old = self.c_seconds - - def _registerFederatePublications(self, publications:Publications = None): + + def _register_federate_publications(self, publications: Publications = None): if publications: self.publicatiuons = publications else: legacy_export_file = os.path.join( - self._system_paths ["ExportLists"], + self._system_paths["ExportLists"], ExportMode.BY_CLASS.value + ".toml", ) export_file = os.path.join( - self._system_paths ["ExportLists"], - ExportMode.EXPORTS.value + ".toml", + self._system_paths["ExportLists"], + ExportMode.EXPORTS.value + ".toml", ) - + publication_dict = { - "opendss_models" : self._objects_by_class, - "federate_name" : self._settings.helics.federate_name, - "federate" : self._federate, + "opendss_models": self._objects_by_class, + "federate_name": self._settings.helics.federate_name, + "federate": self._federate, } - + if os.path.exists(export_file): export_data = load_data(export_file) - publication_dict["input"] = export_data + publication_dict["input"] = export_data elif os.path.exists(legacy_export_file): legacy_data = load_data(legacy_export_file) - publication_dict["legacy_input"] = legacy_data + publication_dict["legacy_input"] = legacy_data else: raise FileNotFoundError("No valid export settings found for the current scenario") - + self.publications = Publications.model_validate(publication_dict) logger.info(str(self.publications.publications)) for publication in self.publications.publications: logger.info(f"pubscription created: {publication}") return - def updateHelicsPublications(self): - + def update_helics_publications(self): + for publication in self.publications.publications: - value = publication.object.GetValue(publication.property) - + value = publication.object.get_value(publication.property) + if publication.data_type == DataType.VECTOR: helics.helicsPublicationPublishVector(publication.pub, value) elif publication.data_type == DataType.DOUBLE: @@ -336,23 +358,27 @@ def updateHelicsPublications(self): return def request_time_increment(self): - error = sum([abs(sub.states[0] - sub.states[1]) for sub in self.subscriptions.subscriptions]) - r_seconds = self._dss_solver.GetTotalSeconds() #- self._dss_solver.GetStepResolutionSeconds() + error = sum( + [abs(sub.states[0] - sub.states[1]) for sub in self.subscriptions.subscriptions] + ) + r_seconds = ( + self._dss_solver.get_total_seconds() + ) # - self._dss_solver.GetStepResolutionSeconds() if not self._settings.helics.iterative_mode: while self.c_seconds < r_seconds: self.c_seconds = helics.helicsFederateRequestTime(self._federate, r_seconds) - logger.info('Time requested: {} - time granted: {} '.format(r_seconds, self.c_seconds)) + logger.info("Time requested: {} - time granted: {} ".format(r_seconds, self.c_seconds)) return True, self.c_seconds else: - self.c_seconds, iteration_state = helics.helicsFederateRequestTimeIterative( - self._federate, - r_seconds, - helics.helics_iteration_request_iterate_if_needed + self._federate, r_seconds, helics.helics_iteration_request_iterate_if_needed ) - logger.info('Time requested: {} - time granted: {} error: {} it: {}'.format( - r_seconds, self.c_seconds, error, self.itr)) + logger.info( + "Time requested: {} - time granted: {} error: {} it: {}".format( + r_seconds, self.c_seconds, error, self.itr + ) + ) if error > -1 and self.itr < self._co_convergance_max_iterations - 1: self.itr += 1 return False, self.c_seconds @@ -362,7 +388,23 @@ def request_time_increment(self): def __del__(self): helics.helicsFederateDisconnect(self._federate) - state = helics.helicsFederateGetState(self._federate) + helics.helicsFederateGetState(self._federate) helics.helicsFederateInfoFree(self.fedinfo) helics.helicsFederateFree(self._federate) - logger.info('HELICS federate for pydss destroyed') + logger.info("HELICS federate for pydss destroyed") + + +globals()["helics_interface"] = HelicsInterface +setattr(HelicsInterface, "registerPubSubTags", HelicsInterface.register_pub_sub_tags) +setattr( + HelicsInterface, + "_registerFederateSubscriptions", + HelicsInterface._register_federate_subscriptions, +) +setattr(HelicsInterface, "updateHelicsSubscriptions", HelicsInterface.update_helics_subscriptions) +setattr( + HelicsInterface, + "_registerFederatePublications", + HelicsInterface._register_federate_publications, +) +setattr(HelicsInterface, "updateHelicsPublications", HelicsInterface.update_helics_publications) diff --git a/src/pydss/metrics.py b/src/pydss/metrics.py index 1730ac01..a1d73a01 100644 --- a/src/pydss/metrics.py +++ b/src/pydss/metrics.py @@ -21,6 +21,7 @@ from pydss.thermal_metrics import ThermalMetrics from pydss.utils.simulation_utils import get_start_time, get_simulation_resolution + class MetricBase(abc.ABC): """Base class for all metrics""" @@ -42,8 +43,10 @@ def __init__(self, prop, dss_objs, settings: SimulationSettingsModel): def add_property(self, prop): """Add an instance of ExportListProperty for tracking.""" if prop.are_names_filtered != self._are_names_filtered: - raise InvalidConfiguration(f"All properties for shared elements must have the same filters: " - f"{self._elem_class.__name__} / {prop.name}.") + raise InvalidConfiguration( + f"All properties for shared elements must have the same filters: " + f"{self._elem_class.__name__} / {prop.name}." + ) existing = self._properties.get(prop.store_values_type) if existing is None: self._properties[prop.store_values_type] = prop @@ -112,14 +115,10 @@ def make_empty_storage_container(self, path, prop): elem_names = self._make_elem_names() cls = STORAGE_TYPE_MAP[prop.store_values_type] values = [ValueByNumber(x.FullName, self.label(), 0.0) for x in self._dss_objs] - container = cls( - self._hdf_store, path, prop, 1, self._max_chunk_bytes, values, elem_names - ) + container = cls(self._hdf_store, path, prop, 1, self._max_chunk_bytes, values, elem_names) return container - def make_storage_container( - self, path, prop, num_steps, max_chunk_bytes, values, **kwargs - ): + def make_storage_container(self, path, prop, num_steps, max_chunk_bytes, values, **kwargs): """Make a storage container. Returns @@ -176,12 +175,8 @@ def append_values(self, time_step, store_nan=False): def close(self): assert len(self._properties) == 1 prop = next(iter(self._properties.values())) - path = ( - f"{self._base_path}/{prop.elem_class}/ElementProperties/{prop.storage_name}" - ) - values = [ - ValueByNumber(x, prop.name, y) for x, y in self._change_counts.items() - ] + path = f"{self._base_path}/{prop.elem_class}/ElementProperties/{prop.storage_name}" + values = [ValueByNumber(x, prop.name, y) for x, y in self._change_counts.items()] # This class creates an instance of ValueContainer directly because # these metrics can only store one type, and so don't need an instance # of StorageFilterBase. @@ -262,8 +257,8 @@ def _initialize_containers(self, values): def append_values(self, time_step, store_nan=False): if not self._name_order: self._name_order[:] = [x.FullName for x in self._iter_dss_objs()] - - #values = [self._get_value(x, time_step) for x in self._dss_objs] + + # values = [self._get_value(x, time_step) for x in self._dss_objs] values = [] objects_changed = False for dss_obj, expected_name in zip(self._iter_dss_objs(), self._name_order): @@ -285,7 +280,7 @@ def append_values(self, time_step, store_nan=False): if store_nan: for val in values: val.set_nan() - + for value_type, container in self._containers.items(): prop = self._properties[value_type] if prop.data_conversion != DataConversion.NONE: @@ -313,7 +308,7 @@ class OpenDssPropertyMetric(MultiValueTypeMetricBase): """Stores metrics for any OpenDSS element property.""" def _get_value(self, dss_obj, _time_step): - return dss_obj.UpdateValue(self._name) + return dss_obj.update_value(self._name) def append_values(self, time_step, store_nan=False): curr_data = {} @@ -330,7 +325,7 @@ def append_values(self, time_step, store_nan=False): # These next two might work but are untested. -#class LineLoadingPercent(MultiValueTypeMetricBase): +# class LineLoadingPercent(MultiValueTypeMetricBase): # """Calculates line loading percent at every time point.""" # # def __init__(self, prop, dss_objs, settings): @@ -341,16 +336,16 @@ def append_values(self, time_step, store_nan=False): # line = dss_obj # normal_amps = self._normal_amps.get(line.Name) # if normal_amps is None: -# normal_amps = line.GetValue("NormalAmps", convert=True).value +# normal_amps = line.get_value("NormalAmps", convert=True).value # self._normal_amps[line.Name] = normal_amps # -# currents = line.UpdateValue("Currents").value +# currents = line.update_value("Currents").value # current = max([abs(x) for x in currents]) # loading = current / normal_amps * 100 # return ValueByNumber(line.Name, "LineLoading", loading) # # -#class TransformerLoadingPercent(MultiValueTypeMetricBase): +# class TransformerLoadingPercent(MultiValueTypeMetricBase): # """Calculates transformer loading percent at every time point.""" # # def __init__(self, prop, dss_objs, settings): @@ -361,16 +356,18 @@ def append_values(self, time_step, store_nan=False): # transformer = dss_obj # normal_amps = self._normal_amps.get(transformer.Name) # if normal_amps is None: -# normal_amps = transformer.GetValue("NormalAmps", convert=True).value +# normal_amps = transformer.get_value("NormalAmps", convert=True).value # self._normal_amps[transformer.Name] = normal_amps # -# currents = transformer.UpdateValue("Currents").value +# currents = transformer.update_value("Currents").value # current = max([abs(x) for x in currents]) # loading = current / normal_amps * 100 # return ValueByNumber(transformer.Name, "TransformerLoading", loading) -FeederHeadValues = namedtuple("FeederHeadValues", ["load_kvar", "load_kw", "loading", "reverse_power_flow"]) +FeederHeadValues = namedtuple( + "FeederHeadValues", ["load_kvar", "load_kw", "loading", "reverse_power_flow"] +) class FeederHeadMetrics(MetricBase): @@ -393,7 +390,9 @@ def _initialize_containers(self): "load_kvar": ValueByNumber("FeederHead", "load_kvar", values.load_kvar), "load_kw": ValueByNumber("FeederHead", "load_kw", values.load_kw), "loading": ValueByNumber("FeederHead", "loading", values.loading), - "reverse_power_flow": ValueByNumber("FeederHead", "reverse_power_flow", values.reverse_power_flow), + "reverse_power_flow": ValueByNumber( + "FeederHead", "reverse_power_flow", values.reverse_power_flow + ), } for name in self._values: @@ -439,7 +438,7 @@ def _get_feeder_head_loading(self): raise Exception("Failed to set the feeder head line") n_phases = dss.CktElement.NumPhases() max_amps = dss.CktElement.NormalAmps() - currents = dss.CktElement.CurrentsMagAng()[:2*n_phases] + currents = dss.CktElement.CurrentsMagAng()[: 2 * n_phases] current_magnitude = currents[::2] max_flow = max(max(current_magnitude), 1e-10) @@ -487,7 +486,7 @@ def __init__(self, prop, dss_objs, settings): self._data_conversion = prop.data_conversion def _get_value(self, obj): - value = obj.UpdateValue(self._name) + value = obj.update_value(self._name) if self._data_conversion != DataConversion.NONE: value = convert_data( "Total", @@ -531,7 +530,9 @@ def append_values(self, time_step, store_nan=False): prop = next(iter(self._properties.values())) assert prop.store_values_type in (StoreValuesType.ALL, StoreValuesType.SUM) total.set_name("Total") - path = f"{self._base_path}/{prop.elem_class}/SummedElementProperties/{prop.storage_name}" + path = ( + f"{self._base_path}/{prop.elem_class}/SummedElementProperties/{prop.storage_name}" + ) self._container = self.make_storage_container( path, prop, @@ -554,6 +555,7 @@ class SummedElementsByGroupOpenDssPropertyMetric(MetricBase): Elements are separated into groups by name. """ + def __init__(self, prop, dss_objs, settings): super().__init__(prop, dss_objs, settings) self._containers = {} @@ -573,7 +575,7 @@ def __init__(self, prop, dss_objs, settings): self._data_conversion = prop.data_conversion def _get_value(self, obj): - value = obj.UpdateValue(self._name) + value = obj.update_value(self._name) if self._data_conversion != DataConversion.NONE: value = convert_data( "Total", @@ -657,9 +659,7 @@ def __init__(self, prop, dss_obj, settings): start_time = get_start_time(settings) sim_resolution = get_simulation_resolution(settings) inputs = ReportBase.get_inputs_from_defaults(settings, "Voltage Metrics") - window_size = max(1, int( - timedelta(minutes=inputs["window_size_minutes"]) / sim_resolution - )) + window_size = max(1, int(timedelta(minutes=inputs["window_size_minutes"]) / sim_resolution)) self._voltage_metrics = NodeVoltageMetrics( prop, start_time, sim_resolution, window_size, inputs["store_per_element_data"] ) @@ -689,8 +689,7 @@ def append_values(self, time_step, store_nan=False): self._node_names = dss.Circuit.AllNodeNames() self._identify_primary_v_secondary() self._voltages = [ - ValueByNumber(x, "Voltage", y) - for x, y in zip(self._node_names, voltages) + ValueByNumber(x, "Voltage", y) for x, y in zip(self._node_names, voltages) ] self._voltage_metrics.set_node_info( self._primary_node_names, @@ -750,9 +749,7 @@ def _update_counts(self, capacitor): cur_value = sum(states) last_value = self._last_values[capacitor.FullName] if last_value is not None and cur_value != last_value: - logger.debug( - "%s changed state old=%s new=%s", capacitor.Name, last_value, cur_value - ) + logger.debug("%s changed state old=%s new=%s", capacitor.Name, last_value, cur_value) self._change_counts[capacitor.FullName] += 1 self._last_values[capacitor.FullName] = cur_value @@ -868,12 +865,12 @@ def _append_summed_values(self, time_step, store_nan=False): prop = next(iter(self._properties.values())) if not self._containers: if len(self._properties) > 1: - raise InvalidConfiguration( - "summing elements only supports one Property" - ) + raise InvalidConfiguration("summing elements only supports one Property") assert len(self._properties) == 1 assert prop.store_values_type in (StoreValuesType.ALL, StoreValuesType.SUM) - path = f"{self._base_path}/{prop.elem_class}/SummedElementProperties/{prop.storage_name}" + path = ( + f"{self._base_path}/{prop.elem_class}/SummedElementProperties/{prop.storage_name}" + ) self._containers[prop.store_values_type] = self.make_storage_container( path, prop, @@ -900,9 +897,7 @@ def _get_name_from_line(fields): def _get_window_size_by_name_index(self, prop): """Returns a list of window sizes per element name corresponding to self._names.""" if not prop.opendss_classes: - raise InvalidConfiguration( - f"window_sizes requires opendss_classes: {prop.name}" - ) + raise InvalidConfiguration(f"window_sizes requires opendss_classes: {prop.name}") window_sizes = [None] * len(self._names) for opendss_class, window_size in prop.window_sizes.items(): @@ -973,6 +968,7 @@ def requires_upper_case(): class ExportLoadingsMetric(OpenDssExportMetric): """Stores line and transformer loading percentages in HDF5.""" + @staticmethod def element_class(): return "CktElement" @@ -1018,6 +1014,7 @@ def requires_upper_case(): class ExportPowersMetric(OpenDssExportMetric): """Stores power values in HDF5.""" + @staticmethod def element_class(): return "CktElement" @@ -1059,6 +1056,7 @@ def requires_upper_case(): class OverloadsMetricInMemory(OpenDssExportMetric): """Stores line and transformer loading percentages in memory.""" + def __init__(self, prop, dss_objs, settings): super().__init__(prop, dss_objs, settings) # Indices for node names are tied to indices for node voltages. @@ -1077,9 +1075,13 @@ def __init__(self, prop, dss_objs, settings): transformer_window_size_hours=inputs["transformer_window_size_hours"], transformer_window_size=transformer_window_size, line_loading_percent_threshold=inputs["line_loading_percent_threshold"], - line_loading_percent_moving_average_threshold=inputs["line_loading_percent_moving_average_threshold"], + line_loading_percent_moving_average_threshold=inputs[ + "line_loading_percent_moving_average_threshold" + ], transformer_loading_percent_threshold=inputs["transformer_loading_percent_threshold"], - transformer_loading_percent_moving_average_threshold=inputs["transformer_loading_percent_moving_average_threshold"], + transformer_loading_percent_moving_average_threshold=inputs[ + "transformer_loading_percent_moving_average_threshold" + ], store_per_element_data=inputs["store_per_element_data"], ) @@ -1106,8 +1108,8 @@ def _append_values(self, time_step, store_nan=False): line_loadings = self._values[:] transformer_loadings = [] else: - line_loadings = self._values[:self._transformer_index] - transformer_loadings = self._values[self._transformer_index:] + line_loadings = self._values[: self._transformer_index] + transformer_loadings = self._values[self._transformer_index :] if not store_nan: self._thermal_metrics.update(time_step, line_loadings, transformer_loadings) @@ -1187,13 +1189,9 @@ def convert_data(name, prop_name, value, conversion): elif conversion == DataConversion.ABS_SUM: converted = ValueByNumber(name, prop_name, abs(sum(value.value))) elif conversion == DataConversion.SUM_REAL: - converted = ValueByNumber( - name, prop_name, sum((x.real for x in value.value)) - ) + converted = ValueByNumber(name, prop_name, sum((x.real for x in value.value))) elif conversion == DataConversion.SUM_ABS_REAL: - converted = ValueByNumber( - name, prop_name, sum((abs(x.real) for x in value.value)) - ) + converted = ValueByNumber(name, prop_name, sum((abs(x.real) for x in value.value))) else: converted = value diff --git a/src/pydss/modes/Dynamic.py b/src/pydss/modes/Dynamic.py deleted file mode 100644 index 8a4a4e36..00000000 --- a/src/pydss/modes/Dynamic.py +++ /dev/null @@ -1,71 +0,0 @@ -from datetime import timedelta -import math - -from loguru import logger - -from pydss.modes.solver_base import solver_base -from pydss.simulation_input_models import ProjectModel - - -class Dynamic(solver_base): - def __init__(self, dssInstance, settings: ProjectModel): - super().__init__(dssInstance, settings) - self.setMode('Dynamic') - self._dssInstance.utils.run_command('Set ControlMode={}'.format(settings.control_mode.value)) - self._dssSolution.Number(1) - self._dssSolution.StepSize(self._sStepRes) - self._dssSolution.MaxControlIterations(settings.max_control_iterations) - self._dssSolution.DblHour(self._Hour + self._Second / 3600.0) - return - - def setFrequency(self, frequency): - self._dssSolution.Frequency(frequency) - return - - def getFrequency(self): - return self._dssSolution.Frequency() - - def SimulationSteps(self): - Seconds = (self._EndTime - self._StartTime).total_seconds() - Steps = math.ceil(Seconds / self._sStepRes) - return Steps, self._StartTime, self._EndTime - - def GetOpenDSSTime(self): - return self._dssSolution.DblHour() - - def reset(self): - self.setMode('Dynamic') - self._dssSolution.Hour(self._Hour) - self._dssSolution.Seconds(self._Second) - self._dssSolution.Number(1) - self._dssSolution.StepSize(self._sStepRes) - self._dssSolution.MaxControlIterations(self._settings.project.max_control_iterations) - return - - def SolveFor(self, mStartTime, mTimeStep): - Hour = int(mStartTime/60) - Min = mStartTime % 60 - self._dssSolution.DblHour(Hour + Min / 60.0) - self._dssSolution.Number(mTimeStep) - self._dssSolution.Solve() - return self._dssSolution.Converged() - - def IncStep(self): - self._dssSolution.StepSize(self._sStepRes) - self._dssSolution.Solve() - self._Time = self._Time + timedelta(seconds=self._sStepRes) - self._Hour = int(self._dssSolution.DblHour() // 1) - self._Second = (self._dssSolution.DblHour() % 1) * 60 * 60 - logger.debug('OpenDSS time [h] - ' + str(self._dssSolution.DblHour())) - logger.debug('Pydss datetime - ' + str(self._Time)) - return self._dssSolution.Converged() - - def reSolve(self): - self._dssSolution.StepSize(0) - self._dssSolution.SolveNoControl() - return self._dssSolution.Converged() - - def Solve(self): - self._dssSolution.StepSize(0) - self._dssSolution.Solve() - return self._dssSolution.Converged() diff --git a/src/pydss/modes/QSTS.py b/src/pydss/modes/QSTS.py deleted file mode 100644 index 79a97907..00000000 --- a/src/pydss/modes/QSTS.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import timedelta - -from pydss.modes.solver_base import solver_base -from pydss.simulation_input_models import ProjectModel -from pydss.utils.dss_utils import get_load_shape_resolution_secs -from pydss.utils.timing_utils import timer_stats_collector, track_timing - - -class QSTS(solver_base): - def __init__(self, dssInstance, settings: ProjectModel): - super().__init__(dssInstance, settings) - self._dssSolution.Mode(2) - self._dssInstance.utils.run_command('Set ControlMode={}'.format(settings.control_mode.value)) - self._dssSolution.Number(1) - self._dssSolution.StepSize(self._sStepRes) - self._dssSolution.MaxControlIterations(settings.max_control_iterations) - - start_time_hours = self._Hour + self._Second / 3600.0 - load_shape_resolutions_secs = get_load_shape_resolution_secs() - if load_shape_resolutions_secs == self._sStepRes: - # I don't know why this is needed in this case. - # The first data point gets skipped without it. - # FIXME - start_time_hours += self._sStepRes / 3600.0 - self._dssSolution.DblHour(start_time_hours) - return - - def SolveFor(self, mStartTime, mTimeStep): - Hour = int(mStartTime/60) - Min = mStartTime%60 - self._dssSolution.DblHour(Hour + Min / 60.0) - self._dssSolution.Number(mTimeStep) - self._dssSolution.Solve() - return self._dssSolution.Converged() - - def IncStep(self): - self._dssSolution.StepSize(self._sStepRes) - self._dssSolution.Solve() - self._Time = self._Time + timedelta(seconds=self._sStepRes) - self._Hour = int(self._dssSolution.DblHour() // 1) - self._Second = (self._dssSolution.DblHour() % 1) * 60 * 60 - return self._dssSolution.Converged() - - def reSolve(self): - self._dssSolution.StepSize(0) - self._dssSolution.SolveNoControl() - return self._dssSolution.Converged() - - def Solve(self): - self._dssSolution.StepSize(0) - self._dssSolution.Solve() - return self._dssSolution.Converged() - - def setMode(self, mode): - self._dssInstance.utils.run_command('Set Mode={}'.format(mode)) - if mode.lower() == 'yearly': - self._dssSolution.Mode(2) - self._dssSolution.DblHour(self._Hour + self._Second / 3600.0) - self._dssSolution.Number(1) - self._dssSolution.StepSize(self._sStepRes) - self._dssSolution.MaxControlIterations(self._settings.project.max_control_iterations) - - def reset(self): - pass diff --git a/src/pydss/modes/Snapshot.py b/src/pydss/modes/Snapshot.py deleted file mode 100644 index cfa666d7..00000000 --- a/src/pydss/modes/Snapshot.py +++ /dev/null @@ -1,31 +0,0 @@ -from pydss.modes.solver_base import solver_base -from pydss.simulation_input_models import ProjectModel - - -class Snapshot(solver_base): - def __init__(self, dssInstance, settings: ProjectModel): - super().__init__(dssInstance, settings) - self._dssSolution.Mode(0) - self._dssInstance.utils.run_command('Set ControlMode={}'.format(settings.control_mode.value)) - self._dssSolution.MaxControlIterations(settings.max_control_iterations) - return - - def reSolve(self): - self._dssSolution.SolveNoControl() - return self._dssSolution.Converged() - - def SimulationSteps(self): - return 1, self._StartTime, self._EndTime - - def Solve(self): - self._dssSolution.Solve() - return self._dssSolution.Converged() - - def IncStep(self): - return self._dssSolution.Solve() - - def SolveFor(self): - pass - - def reset(self): - pass diff --git a/src/pydss/modes/dynamic.py b/src/pydss/modes/dynamic.py new file mode 100644 index 00000000..e1c0f2a9 --- /dev/null +++ b/src/pydss/modes/dynamic.py @@ -0,0 +1,83 @@ +from datetime import timedelta +import math + +from loguru import logger + +from pydss.modes.solver_base import SolverBase +from pydss.simulation_input_models import ProjectModel + + +class Dynamic(SolverBase): + def __init__(self, dss_instance, settings: ProjectModel): + super().__init__(dss_instance, settings) + self.set_mode("Dynamic") + self._dss_instance.utils.run_command( + "Set ControlMode={}".format(settings.control_mode.value) + ) + self._dss_solution.Number(1) + self._dss_solution.StepSize(self._step_resolution) + self._dss_solution.MaxControlIterations(settings.max_control_iterations) + self._dss_solution.DblHour(self._hour + self._second / 3600.0) + return + + def set_frequency(self, frequency): + self._dss_solution.Frequency(frequency) + return + + def get_frequency(self): + return self._dss_solution.Frequency() + + def simulation_steps(self): + seconds = (self._end_time - self._start_time).total_seconds() + steps = math.ceil(seconds / self._step_resolution) + return steps, self._start_time, self._end_time + + def get_opendss_time(self): + return self._dss_solution.DblHour() + + def reset(self): + self.set_mode("Dynamic") + self._dss_solution.Hour(self._hour) + self._dss_solution.Seconds(self._second) + self._dss_solution.Number(1) + self._dss_solution.StepSize(self._step_resolution) + self._dss_solution.MaxControlIterations(self._settings.project.max_control_iterations) + return + + def solve_for(self, start_time, time_step): + hour = int(start_time / 60) + minute = start_time % 60 + self._dss_solution.DblHour(hour + minute / 60.0) + self._dss_solution.Number(time_step) + self._dss_solution.Solve() + return self._dss_solution.Converged() + + def inc_step(self): + self._dss_solution.StepSize(self._step_resolution) + self._dss_solution.Solve() + self._time = self._time + timedelta(seconds=self._step_resolution) + self._hour = int(self._dss_solution.DblHour() // 1) + self._second = (self._dss_solution.DblHour() % 1) * 60 * 60 + logger.debug("OpenDSS time [h] - " + str(self._dss_solution.DblHour())) + logger.debug("Pydss datetime - " + str(self._time)) + return self._dss_solution.Converged() + + def re_solve(self): + self._dss_solution.StepSize(0) + self._dss_solution.SolveNoControl() + return self._dss_solution.Converged() + + def solve(self): + self._dss_solution.StepSize(0) + self._dss_solution.Solve() + return self._dss_solution.Converged() + + +Dynamic.setFrequency = Dynamic.set_frequency +Dynamic.getFrequency = Dynamic.get_frequency +Dynamic.SimulationSteps = Dynamic.simulation_steps +Dynamic.GetOpenDSSTime = Dynamic.get_opendss_time +Dynamic.SolveFor = Dynamic.solve_for +Dynamic.IncStep = Dynamic.inc_step +Dynamic.reSolve = Dynamic.re_solve +Dynamic.Solve = Dynamic.solve diff --git a/src/pydss/modes/qsts.py b/src/pydss/modes/qsts.py new file mode 100644 index 00000000..5bc9733a --- /dev/null +++ b/src/pydss/modes/qsts.py @@ -0,0 +1,72 @@ +from datetime import timedelta + +from pydss.modes.solver_base import SolverBase +from pydss.simulation_input_models import ProjectModel +from pydss.utils.dss_utils import get_load_shape_resolution_secs + + +class QSTS(SolverBase): + def __init__(self, dss_instance, settings: ProjectModel): + super().__init__(dss_instance, settings) + self._dss_solution.Mode(2) + self._dss_instance.utils.run_command( + "Set ControlMode={}".format(settings.control_mode.value) + ) + self._dss_solution.Number(1) + self._dss_solution.StepSize(self._step_resolution) + self._dss_solution.MaxControlIterations(settings.max_control_iterations) + + start_time_hours = self._hour + self._second / 3600.0 + load_shape_resolution_seconds = get_load_shape_resolution_secs() + if load_shape_resolution_seconds == self._step_resolution: + # I don't know why this is needed in this case. + # The first data point gets skipped without it. + # FIXME + start_time_hours += self._step_resolution / 3600.0 + self._dss_solution.DblHour(start_time_hours) + return + + def solve_for(self, start_time, time_step): + hour = int(start_time / 60) + minute = start_time % 60 + self._dss_solution.DblHour(hour + minute / 60.0) + self._dss_solution.Number(time_step) + self._dss_solution.Solve() + return self._dss_solution.Converged() + + def inc_step(self): + self._dss_solution.StepSize(self._step_resolution) + self._dss_solution.Solve() + self._time = self._time + timedelta(seconds=self._step_resolution) + self._hour = int(self._dss_solution.DblHour() // 1) + self._second = (self._dss_solution.DblHour() % 1) * 60 * 60 + return self._dss_solution.Converged() + + def re_solve(self): + self._dss_solution.StepSize(0) + self._dss_solution.SolveNoControl() + return self._dss_solution.Converged() + + def solve(self): + self._dss_solution.StepSize(0) + self._dss_solution.Solve() + return self._dss_solution.Converged() + + def set_mode(self, mode): + self._dss_instance.utils.run_command("Set Mode={}".format(mode)) + if mode.lower() == "yearly": + self._dss_solution.Mode(2) + self._dss_solution.DblHour(self._hour + self._second / 3600.0) + self._dss_solution.Number(1) + self._dss_solution.StepSize(self._step_resolution) + self._dss_solution.MaxControlIterations(self._settings.project.max_control_iterations) + + def reset(self): + pass + + +QSTS.SolveFor = QSTS.solve_for +QSTS.IncStep = QSTS.inc_step +QSTS.reSolve = QSTS.re_solve +QSTS.Solve = QSTS.solve +QSTS.setMode = QSTS.set_mode diff --git a/src/pydss/modes/snapshot.py b/src/pydss/modes/snapshot.py new file mode 100644 index 00000000..1832a9ac --- /dev/null +++ b/src/pydss/modes/snapshot.py @@ -0,0 +1,40 @@ +from pydss.modes.solver_base import SolverBase +from pydss.simulation_input_models import ProjectModel + + +class Snapshot(SolverBase): + def __init__(self, dss_instance, settings: ProjectModel): + super().__init__(dss_instance, settings) + self._dss_solution.Mode(0) + self._dss_instance.utils.run_command( + "Set ControlMode={}".format(settings.control_mode.value) + ) + self._dss_solution.MaxControlIterations(settings.max_control_iterations) + return + + def re_solve(self): + self._dss_solution.SolveNoControl() + return self._dss_solution.Converged() + + def simulation_steps(self): + return 1, self._start_time, self._end_time + + def solve(self): + self._dss_solution.Solve() + return self._dss_solution.Converged() + + def inc_step(self): + return self._dss_solution.Solve() + + def solve_for(self): + pass + + def reset(self): + pass + + +Snapshot.SimulationSteps = Snapshot.simulation_steps +Snapshot.Solve = Snapshot.solve +Snapshot.IncStep = Snapshot.inc_step +Snapshot.SolveFor = Snapshot.solve_for +Snapshot.reSolve = Snapshot.re_solve diff --git a/src/pydss/modes/solver_base.py b/src/pydss/modes/solver_base.py index df8168b1..15b323d3 100644 --- a/src/pydss/modes/solver_base.py +++ b/src/pydss/modes/solver_base.py @@ -4,96 +4,113 @@ from loguru import logger -from pydss.common import DATE_FORMAT from pydss.simulation_input_models import ProjectModel -class solver_base(abc.ABC): - def __init__(self, dssInstance, settings: ProjectModel): + +class SolverBase(abc.ABC): + def __init__(self, dss_instance, settings: ProjectModel): self._settings = settings - self._Time = settings.start_time - self._Loadshape_init_time = settings.loadshape_start_time - time_offset_days = (self._Time - self._Loadshape_init_time).days - time_offset_seconds = (self._Time - self._Loadshape_init_time).seconds + self._time = settings.start_time + self._loadshape_init_time = settings.loadshape_start_time + time_offset_days = (self._time - self._loadshape_init_time).days + time_offset_seconds = (self._time - self._loadshape_init_time).seconds - self._StartTime = self._Time - self._EndTime = self._Time + timedelta(minutes=settings.simulation_duration_min) + self._start_time = self._time + self._end_time = self._time + timedelta(minutes=settings.simulation_duration_min) - StartDay = time_offset_days - StartTimeMin = time_offset_seconds / 60.0 - sStepResolution = settings.step_resolution_sec + start_day = time_offset_days + start_time_min = time_offset_seconds / 60.0 + step_resolution = settings.step_resolution_sec - self.StartDay = self._StartTime.timetuple().tm_yday - self.EndDay = self._EndTime.timetuple().tm_yday + self.start_day = self._start_time.timetuple().tm_yday + self.end_day = self._end_time.timetuple().tm_yday - self._sStepRes = sStepResolution - self._dssInstance = dssInstance - self._dssSolution = dssInstance.Solution + self._step_resolution = step_resolution + self._dss_instance = dss_instance + self._dss_solution = dss_instance.Solution - self._Hour = StartDay * 24 - self._Second = StartTimeMin * 60.0 + self._hour = start_day * 24 + self._second = start_time_min * 60.0 - #self._dssSolution.DblHour() - self.reSolve() + self.re_solve() logger.info("%s solver setup complete", settings.simulation_type) - def setFrequency(self, frequency): - self._dssSolution.Frequency(frequency) + def set_frequency(self, frequency): + self._dss_solution.Frequency(frequency) return - def getFrequency(self): - return self._dssSolution.Frequency() + def get_frequency(self): + return self._dss_solution.Frequency() - def SimulationSteps(self): - Seconds = (self._EndTime - self._StartTime).total_seconds() - Steps = math.ceil(Seconds / self._sStepRes) - return Steps, self._StartTime, self._EndTime + def simulation_steps(self): + seconds = (self._end_time - self._start_time).total_seconds() + steps = math.ceil(seconds / self._step_resolution) + return steps, self._start_time, self._end_time - def GetTotalSeconds(self): - return (self._Time - self._StartTime).total_seconds() + def get_total_seconds(self): + return (self._time - self._start_time).total_seconds() - def GetDateTime(self): - return self._Time + def get_datetime(self): + return self._time - def GetStepResolutionSeconds(self): - return self._sStepRes + def get_step_resolution_seconds(self): + return self._step_resolution - def GetStepSizeSec(self): - return self._sStepRes + def get_step_size_sec(self): + return self._step_resolution - def getMode(self): - return self._dssSolution.ModeID() + def get_mode(self): + return self._dss_solution.ModeID() - def setMode(self, mode): - return self._dssInstance.utils.run_command('Set Mode={}'.format(mode)) + def set_mode(self, mode): + return self._dss_instance.utils.run_command("Set Mode={}".format(mode)) - def GetOpenDSSTime(self): - return self._dssSolution.DblHour() + def get_opendss_time(self): + return self._dss_solution.DblHour() def get_simulation_end_time(self): - return self._EndTime + return self._end_time @property - def MaxIterations(self): + def max_iterations(self): return self._settings.max_control_iterations @abc.abstractmethod - def SolveFor(self, mStartTime, mTimeStep): - """Run SolveFor""" + def solve_for(self, start_time, time_step): + """Run a solve for a time step.""" @abc.abstractmethod def reset(self): """Reset the solver""" @abc.abstractmethod - def reSolve(self): + def re_solve(self): """Run a SolveNoControl""" @abc.abstractmethod - def Solve(self): - """Run a Solve""" + def solve(self): + """Run a solve.""" @abc.abstractmethod - def IncStep(self): + def inc_step(self): """Increment the simulation time step""" + + +solver_base = SolverBase +SolverBase.setFrequency = SolverBase.set_frequency +SolverBase.getFrequency = SolverBase.get_frequency +SolverBase.SimulationSteps = SolverBase.simulation_steps +SolverBase.GetTotalSeconds = SolverBase.get_total_seconds +SolverBase.GetDateTime = SolverBase.get_datetime +SolverBase.GetStepResolutionSeconds = SolverBase.get_step_resolution_seconds +SolverBase.GetStepSizeSec = SolverBase.get_step_size_sec +SolverBase.getMode = SolverBase.get_mode +SolverBase.setMode = SolverBase.set_mode +SolverBase.GetOpenDSSTime = SolverBase.get_opendss_time +SolverBase.MaxIterations = SolverBase.max_iterations +SolverBase.SolveFor = SolverBase.solve_for +SolverBase.reSolve = SolverBase.re_solve +SolverBase.Solve = SolverBase.solve +SolverBase.IncStep = SolverBase.inc_step diff --git a/src/pydss/naerm.py b/src/pydss/naerm.py index a727a9dd..4bea2ba9 100644 --- a/src/pydss/naerm.py +++ b/src/pydss/naerm.py @@ -1,130 +1,127 @@ # Kapil Duwadi PYDSS_DICT = { - 'CurrentsMagAng': 'vector', - 'Currents': 'vector', - 'RatedCurrent': 'double', - 'EmergAmps': 'double', - 'NormalAmps': 'double', - 'normamps': 'double', - 'Losses': 'vector', - 'PhaseLosses': 'vector', - 'Powers': 'vector', - 'TotalPower': 'vector', - 'LineLosses': 'vector', - 'SubstationLosses': 'vector', - 'kV': 'double', - 'kVARated': 'double', - 'kvar': 'double', - 'kW': 'double', - 'kVABase': 'double', - 'kWh': 'double', - 'puVmagAngle': 'vector', - 'VoltagesMagAng': 'vector', - 'VMagAngle': 'vector', - 'Voltages': 'vector', - 'Vmaxpu': 'double', - 'Vminpu': 'double', - 'Frequency': 'double', - 'Taps': 'vector', - '%stored': 'double', - 'Distance': 'double' - } + "CurrentsMagAng": "vector", + "Currents": "vector", + "RatedCurrent": "double", + "EmergAmps": "double", + "NormalAmps": "double", + "normamps": "double", + "Losses": "vector", + "PhaseLosses": "vector", + "Powers": "vector", + "TotalPower": "vector", + "LineLosses": "vector", + "SubstationLosses": "vector", + "kV": "double", + "kVARated": "double", + "kvar": "double", + "kW": "double", + "kVABase": "double", + "kWh": "double", + "puVmagAngle": "vector", + "VoltagesMagAng": "vector", + "VMagAngle": "vector", + "Voltages": "vector", + "Vmaxpu": "double", + "Vminpu": "double", + "Frequency": "double", + "Taps": "vector", + "%stored": "double", + "Distance": "double", +} ASSETTYPEMAPDICT = { - 'Buses' : 'bus', - 'Lines' : 'line', - 'Loads' : 'load', - 'PVSystems' : 'pv', - 'Transformers': 'trans', - 'Faults' : 'fault', - 'Storages': 'storage', - 'Circuit': 'circuit' - + "Buses": "bus", + "Lines": "line", + "Loads": "load", + "PVSystems": "pv", + "Transformers": "trans", + "Faults": "fault", + "Storages": "storage", + "Circuit": "circuit", } PROPERTYMAPDICT = { - 'CurrentMagAng' : ['current double', 'current_angle double'], - 'TotalPower' : ['power_real double', 'power_imag double'], - + "CurrentMagAng": ["current double", "current_angle double"], + "TotalPower": ["power_real double", "power_imag double"], } -PARAMETERNAME2INDEX = { - 'power_real' : 0, - 'power_imag': 1 -} +PARAMETERNAME2INDEX = {"power_real": 0, "power_imag": 1} def isnaerm(name): - try: - if len(name.split('/')) == 5: + try: + if len(name.split("/")) == 5: return True else: return False - - except Exception as e: - return False + except Exception: + return False def get_naerm_value(value, naerm_name): - federate_name, asset_type, asset_name, parameter_name, parameter_unit = naerm_name.split('/') + federate_name, asset_type, asset_name, parameter_name, parameter_unit = naerm_name.split("/") if isinstance(value, list): try: value = value[PARAMETERNAME2INDEX[parameter_name]] return value - except Exception as e: - return 'Failed' - + except Exception: + return "Failed" + else: return value + def pydss_to_naerm(pydssname): try: - fed_name, class_name, object_name, ppty_name = pydssname.split('.') + fed_name, class_name, object_name, ppty_name = pydssname.split(".") asset_type = ASSETTYPEMAPDICT[class_name] parameter_name = PROPERTYMAPDICT[ppty_name] if isinstance(parameter_name, list): - naerm_name = [f"{fed_name}/{asset_type}/{object_name}/{param.split(' ')[0]}/{param.split(' ')[1]}" for param in parameter_name] - + naerm_name = [ + f"{fed_name}/{asset_type}/{object_name}/{param.split(' ')[0]}/{param.split(' ')[1]}" + for param in parameter_name + ] + else: naerm_name = f"{fed_name}/{asset_type}/{object_name}/{parameter_name.split(' ')[0]}/{parameter_name.split(' ')[1]}" return naerm_name - except Exception as e: - return 'Failed' + except Exception: + return "Failed" def naerm_to_pydss(naerm_name): try: - federate_name, asset_type, asset_name, parameter_name, parameter_unit = naerm_name.split('/') + federate_name, asset_type, asset_name, parameter_name, parameter_unit = naerm_name.split( + "/" + ) for keys, values in ASSETTYPEMAPDICT.items(): if values == asset_type: class_name = keys break - - for keys, values in PROPERTYMAPDICT.items(): + for keys, values in PROPERTYMAPDICT.items(): if isinstance(values, list): - if parameter_name + ' ' + parameter_unit in values: + if parameter_name + " " + parameter_unit in values: ppty_name = keys break else: if parameter_name == values: - pptty_name = keys break return f"{federate_name}.{class_name}.{asset_name}.{ppty_name}" - except Exception as e: - return 'Failed' - + except Exception: + return "Failed" -if __name__ == '__main__': - name = 'pydss_x/circuit/heco19021/power_real/double' - #print(get_naerm_value([34,45],'pydss_x/circuit/heco19021/power_imag/double' )) \ No newline at end of file +if __name__ == "__main__": + name = "pydss_x/circuit/heco19021/power_real/double" + # print(get_naerm_value([34,45],'pydss_x/circuit/heco19021/power_imag/double' )) diff --git a/src/pydss/node_voltage_metrics.py b/src/pydss/node_voltage_metrics.py index 724d5151..b326cb60 100644 --- a/src/pydss/node_voltage_metrics.py +++ b/src/pydss/node_voltage_metrics.py @@ -8,8 +8,16 @@ from pydss.utils.simulation_utils import CircularBufferHelper + class VoltageMetricsBaseModel(BaseModel): - model_config = ConfigDict(title="VoltageMetricsBaseModel", str_strip_whitespace=True, validate_assignment=True, validate_default=True, extra="forbid", use_enum_values=False) + model_config = ConfigDict( + title="VoltageMetricsBaseModel", + str_strip_whitespace=True, + validate_assignment=True, + validate_default=True, + extra="forbid", + use_enum_values=False, + ) class VoltageMetric1(VoltageMetricsBaseModel): @@ -19,14 +27,16 @@ class VoltageMetric1(VoltageMetricsBaseModel): None, title="time_points", description="time points that contain voltages between ANSI A and ANSI B thresholds", - )] + ), + ] duration: Annotated[ timedelta, Field( None, title="duration", description="amount of time where metric 1 existed (len(time_points) * resolution)", - )] + ), + ] class VoltageMetric2(VoltageMetricsBaseModel): @@ -35,13 +45,15 @@ class VoltageMetric2(VoltageMetricsBaseModel): Field( title="duration", description="amount of time where a node experienced ANSI A violations", - )] + ), + ] duration_percentage: Annotated[ float, Field( title="duration_percentage", description="percentage of overall time", - )] + ), + ] class VoltageMetric3(VoltageMetricsBaseModel): @@ -50,13 +62,15 @@ class VoltageMetric3(VoltageMetricsBaseModel): Field( title="time_points", description="time points where moving average voltages violated ANSI A thresholds", - )] - duration:Annotated[ + ), + ] + duration: Annotated[ timedelta, Field( title="duration", description="amount of time where metric 3 existed (len(time_points) * resolution)", - )] + ), + ] class VoltageMetric4(VoltageMetricsBaseModel): @@ -66,7 +80,8 @@ class VoltageMetric4(VoltageMetricsBaseModel): title="percent_node_ansi_a_violations", description="percent of nodes with ANSI A violations at time points. Excludes time " "points with no violations. Inner list is [timestamp, percent].", - )] + ), + ] class VoltageMetric5(VoltageMetricsBaseModel): @@ -75,13 +90,15 @@ class VoltageMetric5(VoltageMetricsBaseModel): Field( title="min_voltage_by_node", description="Mapping of node name to minimum voltage", - )] + ), + ] max_voltages: Annotated[ Dict, Field( title="max_voltage_by_node", description="Mapping of node name to maximum voltage", - )] + ), + ] class VoltageMetric6(VoltageMetricsBaseModel): @@ -90,19 +107,22 @@ class VoltageMetric6(VoltageMetricsBaseModel): Field( title="num_time_points", description="number of time points that violate ANSI B thresholds", - )] + ), + ] percent_time_points: Annotated[ float, Field( title="percent_time_points", description="percentage of time points that violate ANSI B thresholds", - )] + ), + ] duration: Annotated[ timedelta, Field( title="duration", description="amount of time where metric 6 existed (len(num_time_points) * resolution)", - )] + ), + ] class VoltageMetricsSummaryModel(VoltageMetricsBaseModel): @@ -111,73 +131,85 @@ class VoltageMetricsSummaryModel(VoltageMetricsBaseModel): Field( title="voltage_duration_between_ansi_a_and_b_minutes", description="time in minutes that contain voltages between ANSI A and ANSI B thresholds", - )] + ), + ] max_per_node_voltage_duration_outside_ansi_a_minutes: Annotated[ float, - Field( + Field( title="max_per_node_voltage_duration_outside_ansi_a_minutes", description="maximum time in minutes that a node was outside ANSI A thresholds", - )] + ), + ] moving_average_voltage_duration_outside_ansi_a_minutes: Annotated[ float, Field( title="moving_average_voltage_duration_outside_ansi_a_minutes", description="time in minutes the moving average voltage was outside ANSI A", - )] + ), + ] moving_window_minutes: Annotated[ int, Field( title="moving_window_minutes", description="window size in minutes for moving average metrics", - )] + ), + ] max_voltage: Annotated[ float, Field( title="max_voltage", description="maximum voltage seen on any node", - )] + ), + ] min_voltage: Annotated[ float, Field( title="min_voltage", description="minimum voltage seen on any node", - )] + ), + ] num_nodes_always_inside_ansi_a: Annotated[ int, - Field( + Field( title="num_nodes_always_inside_ansi_a", description="number of nodes always inside ANSI A thresholds", - )] + ), + ] num_nodes_any_outside_ansi_a_always_inside_ansi_b: Annotated[ int, Field( title="num_nodes_any_outside_ansi_a_always_inside_ansi_b", description="number of nodes with some ANSI A violations but no ANSI B violations", - )] + ), + ] num_nodes_any_outside_ansi_b: Annotated[ int, Field( title="num_nodes_always_outside_ansi_b", description="number of nodes with some ANSI B violations", - )] + ), + ] num_time_points_with_ansi_b_violations: Annotated[ int, Field( title="num_time_points_with_ansi_b_violations", description="number of time points with ANSI B violations", - )] + ), + ] total_num_time_points: Annotated[ int, Field( title="total_num_time_points", description="number of time points in the simulation", - )] + ), + ] total_simulation_duration: Annotated[ timedelta, Field( title="total_simulation_duration", description="total length of time of the simulation", - )] + ), + ] VOLTAGE_METRIC_FIELDS_TO_INCLUDE_AS_PASS_CRITERIA = ( @@ -199,60 +231,70 @@ class VoltageMetricsModel(VoltageMetricsBaseModel): Field( title="metric_1", description="metric 1", - )] + ), + ] metric_2: Annotated[ Dict[str, VoltageMetric2], Field( title="metric_2", description="metric 2", - )] + ), + ] metric_3: Annotated[ VoltageMetric3, Field( title="metric_3", description="metric 3", - )] + ), + ] metric_4: Annotated[ VoltageMetric4, Field( title="metric_4", description="metric 4", - )] + ), + ] metric_5: Annotated[ VoltageMetric5, Field( title="metric_5", description="metric 5", - )] + ), + ] metric_6: Annotated[ VoltageMetric6, Field( title="metric_6", description="metric 6", - )] + ), + ] summary: Annotated[ Union[VoltageMetricsSummaryModel, None], Field( title="summary", description="summary of metrics", - )] + ), + ] class VoltageMetricsByBusTypeModel(VoltageMetricsBaseModel): """Metrics separated by bus type""" + primaries: Annotated[ VoltageMetricsModel, Field( title="primaries", description="metrics for primary buses", - )] + ), + ] secondaries: Annotated[ Union[VoltageMetricsModel, None], Field( title="secondaries", description="metrics for secondary buses", default=None, - )] + ), + ] class SimulationVoltageMetricsModel(VoltageMetricsBaseModel): @@ -261,10 +303,13 @@ class SimulationVoltageMetricsModel(VoltageMetricsBaseModel): Field( title="scenarios", description="voltage metrics by pydss scenario name", - )] + ), + ] -def compare_voltage_metrics(metrics1: VoltageMetricsByBusTypeModel, metrics2: VoltageMetricsByBusTypeModel): +def compare_voltage_metrics( + metrics1: VoltageMetricsByBusTypeModel, metrics2: VoltageMetricsByBusTypeModel +): """Compares the values of two instances of VoltageMetricsModel. Returns @@ -328,9 +373,19 @@ def __init__(self, prop, start_time, resolution, window_size): self._num_time_points = 0 @staticmethod - def create_summary(metric_1, metric_2, metric_3, metric_5, metric_6, node_names, - num_time_points, resolution, range_a_limits, range_b_limits, - moving_window_minutes): + def create_summary( + metric_1, + metric_2, + metric_3, + metric_5, + metric_6, + node_names, + num_time_points, + resolution, + range_a_limits, + range_b_limits, + moving_window_minutes, + ): if not node_names: # There may not be any secondary nodes. return None @@ -347,15 +402,9 @@ def create_summary(metric_1, metric_2, metric_3, metric_5, metric_6, node_names, for node in node_names: min_voltage = metric_5.min_voltages[node] max_voltage = metric_5.max_voltages[node] - if ( - min_voltage < range_b_limits.min - or max_voltage > range_b_limits.max - ): + if min_voltage < range_b_limits.min or max_voltage > range_b_limits.max: num_nodes_any_outside_range_b += 1 - elif ( - min_voltage < range_a_limits.min - or max_voltage > range_a_limits.max - ): + elif min_voltage < range_a_limits.min or max_voltage > range_a_limits.max: num_nodes_any_outside_range_a_no_b += 1 else: num_nodes_always_inside_range_a += 1 @@ -400,24 +449,20 @@ def generate(self, store_per_element_data): ) metric_5 = VoltageMetric5( min_voltages={ - self._node_names[i]: x - for i, x in enumerate(self._metric_5_min_violations) + self._node_names[i]: x for i, x in enumerate(self._metric_5_min_violations) }, max_voltages={ - self._node_names[i]: x - for i, x in enumerate(self._metric_5_max_violations) + self._node_names[i]: x for i, x in enumerate(self._metric_5_max_violations) }, ) metric_6 = VoltageMetric6( num_time_points=self._num_metric_6_time_points_outside_range_b, percent_time_points=self._num_metric_6_time_points_outside_range_b - / self._num_time_points - * 100, + / self._num_time_points + * 100, duration=self._num_metric_6_time_points_outside_range_b * self._resolution, ) - moving_window_minutes = int( - (self._window_size * self._resolution).total_seconds() / 60 - ) + moving_window_minutes = int((self._window_size * self._resolution).total_seconds() / 60) metrics = VoltageMetricsModel( metric_1=metric_1, @@ -427,10 +472,18 @@ def generate(self, store_per_element_data): metric_5=metric_5, metric_6=metric_6, summary=self.create_summary( - metric_1, metric_2, metric_3, metric_5, metric_6, self._node_names, - self._num_time_points, self._resolution, self._range_a_limits, self._range_b_limits, + metric_1, + metric_2, + metric_3, + metric_5, + metric_6, + self._node_names, + self._num_time_points, + self._resolution, + self._range_a_limits, + self._range_b_limits, moving_window_minutes, - ) + ), ) if not store_per_element_data: @@ -449,7 +502,9 @@ def has_data(self): def update(self, time_step, voltages): cur_time = self._start_time + self._resolution * time_step if self._bufs is None: - self._bufs = [CircularBufferHelper(self._window_size) for _ in range(len(self._node_names))] + self._bufs = [ + CircularBufferHelper(self._window_size) for _ in range(len(self._node_names)) + ] self._metric_2_violation_counts = [0] * len(self._node_names) self._metric_5_min_violations = [None] * len(self._node_names) self._metric_5_max_violations = [None] * len(self._node_names) diff --git a/src/pydss/openmdao_components.py b/src/pydss/openmdao_components.py new file mode 100644 index 00000000..ae68d127 --- /dev/null +++ b/src/pydss/openmdao_components.py @@ -0,0 +1,213 @@ +"""OpenMDAO components used to couple PyDSS controllers and OpenDSS.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + +import numpy as np +import openmdao.api as om + + +@dataclass(frozen=True) +class VariableSpec: + """Description of one scalar or vector exchanged with an OpenMDAO component.""" + + name: str + shape: tuple[int, ...] = () + units: str | None = None + val: Any = 0.0 + + @property + def size(self) -> int: + return int(np.prod(self.shape)) if self.shape else 1 + + +class CircuitAdapter(ABC): + """Backend contract for a circuit component.""" + + @abstractmethod + def apply_commands(self, commands: Mapping[str, np.ndarray]) -> None: + """Apply controller and exogenous commands to the circuit.""" + + @abstractmethod + def solve(self) -> bool: + """Solve the circuit once and return backend convergence.""" + + @abstractmethod + def read_measurements(self, names: tuple[str, ...]) -> Mapping[str, Any]: + """Read measurements after a successful circuit solve.""" + + def commit_step(self) -> None: + """Commit backend state after an accepted OpenMDAO time step.""" + + def reset_step(self) -> None: + """Discard transient state before a new OpenMDAO time step.""" + + +class OpenDSSCircuitAdapter(CircuitAdapter): + """Adapt the existing OpenDSS instance and element wrappers to OpenMDAO.""" + + def __init__(self, dss_instance, solver, elements, command_map, measurement_map): + self._dss_instance = dss_instance + self._solver = solver + self._elements = elements + self._command_map = command_map + self._measurement_map = measurement_map + self._time_seconds = 0.0 + + def set_time(self, time_seconds: float) -> None: + self._time_seconds = float(time_seconds) + + def apply_commands(self, commands: Mapping[str, np.ndarray]) -> None: + for name, value in commands.items(): + element_name, parameter = self._command_map[name] + element = self._elements[element_name] + scalar = float(np.asarray(value).reshape(-1)[0]) + if parameter == "enabled": + scalar = "Yes" if scalar else "No" + element.SetParameter(parameter, scalar) + + def solve(self) -> bool: + self._solver.reSolve() + return bool(self._dss_instance.Solution.Converged()) + + def read_measurements(self, names: tuple[str, ...]) -> Mapping[str, Any]: + values = {} + for name in names: + element_name, measurement = self._measurement_map[name] + if measurement == "time_seconds": + values[name] = self._time_seconds + continue + if measurement == "measurement_frequency_hz": + values[name] = float(self._dss_instance.Solution.Frequency()) + continue + + element = self._elements[element_name] + if measurement == "measurement_voltage_pu": + self._dss_instance.Circuit.SetActiveBus(element.Bus[0]) + voltage_values = np.asarray(self._dss_instance.Bus.puVmagAngle()).reshape(-1) + values[name] = float(np.max(voltage_values[::2])) + elif measurement == "measurement_active_power": + values[name] = self._element_power(element, 0) + elif measurement == "measurement_reactive_power": + values[name] = self._element_power(element, 1) + else: + raise KeyError(f"Unsupported OpenDSS measurement {measurement}") + return values + + @staticmethod + def _element_power(element, index: int) -> float: + powers = element.get_value("Powers") + values = np.asarray(powers).reshape(-1) + return float(-np.sum(values[index::2])) + + +class CircuitExplicitComponent(om.ExplicitComponent): + """Explicit OpenMDAO representation of one OpenDSS circuit evaluation.""" + + def initialize(self) -> None: + self.options.declare("adapter", recordable=False) + self.options.declare("command_specs", types=(tuple, list), default=()) + self.options.declare("measurement_specs", types=(tuple, list), default=()) + + def setup(self) -> None: + self._command_specs = tuple(self.options["command_specs"]) + self._measurement_specs = tuple(self.options["measurement_specs"]) + for spec in self._command_specs: + self.add_input(spec.name, shape=spec.shape or None, units=spec.units, val=spec.val) + for spec in self._measurement_specs: + self.add_output(spec.name, shape=spec.shape or None, units=spec.units, val=spec.val) + + def compute(self, inputs, outputs) -> None: + commands = {spec.name: np.asarray(inputs[spec.name]).copy() for spec in self._command_specs} + adapter: CircuitAdapter = self.options["adapter"] + try: + adapter.apply_commands(commands) + if not adapter.solve(): + raise om.AnalysisError("OpenDSS circuit solve did not converge") + measurements = adapter.read_measurements( + tuple(spec.name for spec in self._measurement_specs) + ) + except om.AnalysisError: + raise + except Exception as exc: + raise om.AnalysisError(f"OpenDSS circuit evaluation failed: {exc}") from exc + for spec in self._measurement_specs: + if spec.name not in measurements: + raise om.AnalysisError(f"Circuit adapter did not provide measurement {spec.name}") + outputs[spec.name] = measurements[spec.name] + + def commit_step(self) -> None: + self.options["adapter"].commit_step() + + def reset_step(self) -> None: + self.options["adapter"].reset_step() + + +class ControllerExplicitComponent(om.ExplicitComponent, ABC): + """Base class for controllers with OpenMDAO-owned inputs and outputs.""" + + def initialize(self) -> None: + self.options.declare("input_specs", types=(tuple, list), default=()) + self.options.declare("output_specs", types=(tuple, list), default=()) + self.options.declare("settings", default=None, recordable=False) + self.options.declare("element_name", default="", types=str, recordable=False) + self.options.declare("rated_kva", default=1.0, types=(int, float), recordable=False) + + def setup(self) -> None: + self._input_specs = tuple(self.options["input_specs"]) or tuple(self.input_specs()) + self._output_specs = tuple(self.options["output_specs"]) or tuple(self.output_specs()) + for spec in self._input_specs: + self.add_input(spec.name, shape=spec.shape or None, units=spec.units, val=spec.val) + for spec in self._output_specs: + self.add_output(spec.name, shape=spec.shape or None, units=spec.units, val=spec.val) + + @staticmethod + def input_value(inputs, name: str) -> float: + """Return a scalar input from either a test value or OpenMDAO array.""" + return float(np.asarray(inputs[name]).reshape(-1)[0]) + + def compute(self, inputs, outputs) -> None: + values = self.compute_commands(inputs) + for spec in self._output_specs: + if spec.name not in values: + raise om.AnalysisError(f"Controller did not provide command {spec.name}") + outputs[spec.name] = values[spec.name] + + def input_specs(self) -> tuple[VariableSpec, ...]: + """Return the measurements and exogenous values consumed by the controller.""" + return () + + def output_specs(self) -> tuple[VariableSpec, ...]: + """Return the circuit commands and diagnostics produced by the controller.""" + return ( + VariableSpec("command.active_power"), + VariableSpec("command.reactive_power"), + VariableSpec("diagnostic.residual"), + ) + + def command_parameters(self, element_class: str) -> Mapping[str, str]: + """Return OpenDSS parameter names for this component's command outputs.""" + parameters = { + "command_active_power": "kW", + "command_reactive_power": "kvar", + "command_enabled": "enabled", + "command_state": "State", + "command_power_factor": "pf", + } + if element_class.lower() == "pvsystem": + parameters["command_active_power"] = "Pmpp" + return parameters + + @abstractmethod + def compute_commands(self, inputs) -> Mapping[str, Any]: + """Calculate controller commands without accessing OpenDSS or solving.""" + + def commit_step(self) -> None: + """Commit state after a successful nonlinear solve.""" + + def reset_step(self) -> None: + """Reset transient state before a new time step.""" diff --git a/src/pydss/openmdao_model.py b/src/pydss/openmdao_model.py new file mode 100644 index 00000000..8411e5f5 --- /dev/null +++ b/src/pydss/openmdao_model.py @@ -0,0 +1,94 @@ +"""OpenMDAO problem construction and accepted-time-step lifecycle.""" + +from __future__ import annotations + +from collections.abc import Iterable +from os import PathLike +from pathlib import Path + +import openmdao.api as om + +from pydss.openmdao_components import CircuitExplicitComponent, ControllerExplicitComponent + + +class PyDSSOpenMDAOGroup(om.Group): + """Circuit/controller group with OpenMDAO-owned nonlinear convergence.""" + + def initialize(self) -> None: + self.options.declare("circuit", recordable=False) + self.options.declare("controllers", types=(tuple, list), default=(), recordable=False) + self.options.declare("connections", types=(tuple, list), default=(), recordable=False) + + def setup(self) -> None: + circuit: CircuitExplicitComponent = self.options["circuit"] + for name, controller in self.options["controllers"]: + self.add_subsystem(name, controller) + # Controllers consume the previous OpenDSS measurements first. The + # circuit is last so one solve sees the complete command batch. + self.add_subsystem("circuit", circuit) + for source, target in self.options["connections"]: + self.connect(source, target) + + def commit_step(self) -> None: + self.circuit.commit_step() + for name, _ in self.options["controllers"]: + subsystem = self._get_subsystem(name) + subsystem.commit_step() + + def reset_step(self) -> None: + self.circuit.reset_step() + for name, _ in self.options["controllers"]: + subsystem = self._get_subsystem(name) + subsystem.reset_step() + + +def controller_circuit_variable(index: int, spec_name: str) -> str: + """Return the deterministic circuit-side name for one controller variable.""" + return f"controller_{index}__{spec_name}" + + +def build_openmdao_problem( + circuit: CircuitExplicitComponent, + controllers: Iterable[tuple[str, ControllerExplicitComponent]] = (), + connections: Iterable[tuple[str, str]] = (), + *, + max_iterations: int = 50, + tolerance: float = 1e-6, + reports: bool = False, + work_dir: str | PathLike[str] | None = None, + name: str | None = None, +) -> om.Problem: + """Build and configure a PyDSS OpenMDAO problem.""" + + if max_iterations < 1: + raise ValueError("max_iterations must be at least 1") + + model = PyDSSOpenMDAOGroup( + circuit=circuit, + controllers=tuple(controllers), + connections=tuple(connections), + ) + model.nonlinear_solver = om.NonlinearBlockGS( + maxiter=max_iterations, + atol=tolerance, + rtol=tolerance, + use_aitken=True, + err_on_non_converge=True, + ) + problem_options = {"reports": reports} + if name is not None: + problem_options["name"] = name + if work_dir is not None: + Path(work_dir).mkdir(parents=True, exist_ok=True) + problem_options["work_dir"] = work_dir + problem = om.Problem(model=model, **problem_options) + if reports: + recorder_path = Path(work_dir or ".") / "openmdao_convergence.sql" + if recorder_path.exists(): + recorder_path.unlink() + recorder = om.SqliteRecorder(str(recorder_path)) + model.nonlinear_solver.add_recorder(recorder) + model.nonlinear_solver.recording_options["record_abs_error"] = True + model.nonlinear_solver.recording_options["record_rel_error"] = True + problem.setup() + return problem diff --git a/src/pydss/profile_manager/__init__.py b/src/pydss/profile_manager/__init__.py new file mode 100644 index 00000000..3c5f904c --- /dev/null +++ b/src/pydss/profile_manager/__init__.py @@ -0,0 +1,5 @@ +from pydss.profile_manager import profile_interface + +ProfileInterface = profile_interface + +__all__ = ["ProfileInterface"] diff --git a/src/pydss/ProfileManager/base_definitions.py b/src/pydss/profile_manager/base_definitions.py similarity index 89% rename from src/pydss/ProfileManager/base_definitions.py rename to src/pydss/profile_manager/base_definitions.py index 07a81495..87320a0b 100644 --- a/src/pydss/ProfileManager/base_definitions.py +++ b/src/pydss/profile_manager/base_definitions.py @@ -1,5 +1,4 @@ from pydss.common import PROFILE_MAPPING -import datetime import toml import abc import os @@ -23,12 +22,10 @@ def __init__(self, sim_instance, solver, settings: SimulationSettingsModel, logg settings.profiles.source, ) self.mapping_file = os.path.join( - settings.project.active_project_path, - "Profiles", - PROFILE_MAPPING + settings.project.active_project_path, "Profiles", PROFILE_MAPPING ) self.Profiles = {} - self.mapping = toml.load(open(self.mapping_file , "r")) + self.mapping = toml.load(open(self.mapping_file, "r")) self.sTime = None self.eTime = None self.simRes = None diff --git a/src/pydss/ProfileManager/common.py b/src/pydss/profile_manager/common.py similarity index 59% rename from src/pydss/ProfileManager/common.py rename to src/pydss/profile_manager/common.py index 541789ee..a987538f 100644 --- a/src/pydss/ProfileManager/common.py +++ b/src/pydss/profile_manager/common.py @@ -1,6 +1,7 @@ from enum import IntEnum -class PROFILE_TYPES(IntEnum): + +class ProfileType(IntEnum): Load = 0 Generation = 1 Irradiance = 2 @@ -13,8 +14,11 @@ class PROFILE_TYPES(IntEnum): @staticmethod def names(): - return list(map(lambda c: c.name, PROFILE_TYPES)) + return list(map(lambda c: c.name, ProfileType)) @staticmethod def values(): - return list(map(lambda c: c.value, PROFILE_TYPES)) + return list(map(lambda c: c.value, ProfileType)) + + +PROFILE_TYPES = ProfileType diff --git a/src/pydss/profile_manager/hooks/__init__.py b/src/pydss/profile_manager/hooks/__init__.py new file mode 100644 index 00000000..3c5f904c --- /dev/null +++ b/src/pydss/profile_manager/hooks/__init__.py @@ -0,0 +1,5 @@ +from pydss.profile_manager import profile_interface + +ProfileInterface = profile_interface + +__all__ = ["ProfileInterface"] diff --git a/src/pydss/profile_manager/hooks/h5.py b/src/pydss/profile_manager/hooks/h5.py new file mode 100644 index 00000000..022c90d8 --- /dev/null +++ b/src/pydss/profile_manager/hooks/h5.py @@ -0,0 +1,225 @@ +from pydss.profile_manager.base_definitions import BaseProfileManager, BaseProfile +from pydss.profile_manager.common import ProfileType +from pydss.exceptions import InvalidParameter +from pydss.common import DATE_FORMAT +import pandas as pd +import numpy as np +import datetime +import h5py +import copy +import os + + +class ProfileManager(BaseProfileManager): + def __init__(self, sim_instance, solver, options, logger, **kwargs): + super(ProfileManager, self).__init__(sim_instance, solver, options, logger, **kwargs) + self.Objects = kwargs["objects"] + if os.path.exists(self.basepath): + self.logger.info("Loading existing h5 store") + self.store = h5py.File(self.basepath, "r+") + else: + self.logger.info("Creating new h5 store") + self.store = h5py.File(self.basepath, "w") + for profile_group in ProfileType.names(): + self.store.create_group(profile_group) + self.setup_profiles() + return + + def setup_profiles(self): + self.Profiles = {} + for group, profile_map in self.mapping.items(): + if group in self.store: + grp = self.store[group] + for profile_name, mapping_dict in profile_map.items(): + if profile_name in grp: + objects = {x["object"]: self.Objects[x["object"]] for x in mapping_dict} + self.Profiles[f"{group}/{profile_name}"] = Profile( + self.sim_instance, + grp[profile_name], + objects, + self.solver, + mapping_dict, + self.logger, + **self.kwargs, + ) + else: + self.logger.warning( + "Group {} / data set {} not found in the h5 store".format( + group, profile_name + ) + ) + else: + self.logger.warning("Group {} not found in the h5 store".format(group)) + return + + def create_dataset(self, dname, profile_type, data, start_time, resolution, units, info): + grp = self.store[profile_type] + if dname not in grp: + dset = grp.create_dataset( + dname, + data=data, + shape=(len(data),), + maxshape=(None,), + chunks=True, + compression="gzip", + compression_opts=4, + shuffle=True, + ) + self.create_metadata(dset, start_time, resolution, data, units, info) + else: + self.logger.error( + 'Dataset "{}" already exists in group "{}".'.format(dname, profile_type) + ) + raise Exception( + 'Dataset "{}" already exists in group "{}".'.format(dname, profile_type) + ) + + def add_from_arrays(self, data, name, profile_type, start_time, resolution, units="", info=""): + r, c = data.shape + if r > c: + for i in range(c): + d = data[:, i] + dname = name if i == 0 else "{}_{}".format(name, i) + self.create_dataset( + dname=dname, + profile_type=profile_type, + data=d, + start_time=start_time, + resolution=resolution, + units=units, + info=info, + ) + else: + for i in range(r): + d = data[i, :] + dname = name if i == 0 else "{}_{}".format(name, i) + self.create_dataset( + dname=dname, + profile_type=profile_type, + data=d, + start_time=start_time, + resolution=resolution, + units=units, + info=info, + ) + return + + def add_profiles_from_csv( + self, csv_file, name, profile_type, start_time, resolution_sec=900, units="", info="" + ): + data = pd.read_csv(csv_file).values + self.add_profiles( + data, + name, + profile_type, + start_time, + resolution_sec=resolution_sec, + units=units, + info=info, + ) + + def add_profiles( + self, data, name, profile_type, start_time, resolution_sec=900, units="", info="" + ): + if type(start_time) is not datetime.datetime: + raise InvalidParameter("startTime should be a python datetime object") + if profile_type not in ProfileType.names(): + raise InvalidParameter( + "Valid values for profile_type are {}".format(ProfileType.names()) + ) + if data: + self.add_from_arrays( + data, name, profile_type, start_time, resolution_sec, units=units, info=info + ) + self.store.flush() + return + + def create_metadata(self, dataset, start_time, resolution, data, units, info): + metadata = { + "sTime": str(start_time), + "eTime": str(start_time + datetime.timedelta(seconds=resolution * len(data))), + "resTime": resolution, + "npts": len(data), + "min": min(data), + "max": max(data), + "mean": np.mean(data), + "units": units, + "info": info, + } + for key, value in metadata.items(): + if isinstance(value, str): + value = np.string_(value) + dataset.attrs[key] = value + return + + def remove_profile(self, profile_type, profile_name): + return + + def update(self): + results = {} + for profile_name, profile in self.Profiles.items(): + result = profile.update() + results[profile_name] = result + return results + + +class Profile(BaseProfile): + DEFAULT_SETTINGS = {"multiplier": 1, "normalize": False, "interpolate": False} + + def __init__(self, sim_instance, dataset, devices, solver, mapping_dict, logger, **kwargs): + super(Profile, self).__init__( + sim_instance, dataset, devices, solver, mapping_dict, logger, **kwargs + ) + self.valueSettings = {x["object"]: {**self.DEFAULT_SETTINGS, **x} for x in mapping_dict} + + self.bufferSize = kwargs["bufferSize"] + self.buffer = np.zeros(self.bufferSize) + self.profile = dataset + self.neglectYear = kwargs["neglectYear"] + self.Objects = devices + + self.attrs = self.profile.attrs + self.sTime = datetime.datetime.strptime(self.attrs["sTime"].decode(), DATE_FORMAT) + self.eTime = datetime.datetime.strptime( + self.attrs["eTime"].decode(), "%Y-%m-%d %H:%M:%S.%f" + ) + self.simRes = solver.GetStepSizeSec() + self.Time = copy.deepcopy(solver.GetDateTime()) + return + + def update_profile_settings(self): + return + + def update(self, update_object_properties=True): + self.Time = copy.deepcopy(self.solver.GetDateTime()) + if self.Time < self.sTime or self.Time > self.eTime: + value = 0 + value1 = 0 + else: + delta_time = (self.Time - self.sTime).total_seconds() + n = int(delta_time / self.attrs["resTime"]) + value = self.profile[n] + interpolation_time = ( + self.Time + - (self.sTime + datetime.timedelta(seconds=int(n * self.attrs["resTime"]))) + ).total_seconds() + value1 = ( + self.profile[n] + + (self.profile[n + 1] - self.profile[n]) + * interpolation_time + / self.attrs["resTime"] + ) + if update_object_properties: + for object_name, obj in self.Objects.items(): + if self.valueSettings[object_name]["interpolate"]: + value = value1 + mult = self.valueSettings[object_name]["multiplier"] + if self.valueSettings[object_name]["normalize"]: + value_f = value / self.attrs["max"] * mult + else: + value_f = value * mult + obj.SetParameter(self.attrs["units"].decode(), value_f) + return value + + +ProfileManager.createMetadata = ProfileManager.create_metadata diff --git a/src/pydss/ProfileManager/hooks/MongoDB.py b/src/pydss/profile_manager/hooks/mongodb.py similarity index 55% rename from src/pydss/ProfileManager/hooks/MongoDB.py rename to src/pydss/profile_manager/hooks/mongodb.py index 0ff997b3..cb347c65 100644 --- a/src/pydss/ProfileManager/hooks/MongoDB.py +++ b/src/pydss/profile_manager/hooks/mongodb.py @@ -1,50 +1,41 @@ -from pydss.ProfileManager.base_definitions import BaseProfileManager, BaseProfile +from pydss.profile_manager.base_definitions import BaseProfileManager, BaseProfile from bson.objectid import ObjectId from pymongo import MongoClient -import json -from pydss.ProfileManager.common import PROFILE_TYPES -from pydss.exceptions import InvalidParameter -from pydss.common import DATE_FORMAT -from datetime import datetime -import pandas as pd -import numpy as np -import datetime -import copy -import os class ProfileManager(BaseProfileManager): - - def __init__(self, sim_instance, solver, options, logger, **kwargs): + def __init__(self, sim_instance, solver, options, logger, **kwargs): super(ProfileManager, self).__init__(sim_instance, solver, options, logger, **kwargs) self.Objects = kwargs["objects"] - usersettings = options["Profiles"]['settings'] + usersettings = options["Profiles"]["settings"] - #self.client = MongoClient(self.basepath, username=usersettings["username"], password=usersettings["password"]) - self.client = MongoClient("mongodb://user:password@bmather-131003:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false&connect=false") + # self.client = MongoClient(self.basepath, username=usersettings["username"], password=usersettings["password"]) + self.client = MongoClient( + "mongodb://user:password@bmather-131003:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false&connect=false" + ) self.db_name = usersettings["database"] self.db = self.client[usersettings["database"]] self.collections = self.db.collection_names() self.mapping = usersettings["mapping"] - #self.one_Time_fix(usersettings['collection']) + # self.one_Time_fix(usersettings['collection']) self.setup_profiles() pass - def one_Time_fix(self, collection_name): + def one_time_fix(self, collection_name): self.logger.info("Creating aggregated load profiles") - C = self.db[collection_name] - cursor = C.find({"name": "customer_metadata"}) + collection = self.db[collection_name] + cursor = collection.find({"name": "customer_metadata"}) customer_metadata = cursor.next() i = 0 total_xfmr_load = {} - for customerID, customerInfo in customer_metadata.items(): - if isinstance(customerInfo, dict): - agg_load = customerInfo['section_id'] + for customer_id, customer_info in customer_metadata.items(): + if isinstance(customer_info, dict): + agg_load = customer_info["section_id"] if agg_load not in total_xfmr_load: total_xfmr_load[agg_load] = [] - total_xfmr_load[agg_load].append(customerID) + total_xfmr_load[agg_load].append(customer_id) - record = C.find({"date": {"$exists" : 1}}) + record = collection.find({"date": {"$exists": 1}}) count = record.count() posts = [] self.logger.info(f"Count: {count}") @@ -66,21 +57,23 @@ def one_Time_fix(self, collection_name): if elm in loadlist: total_load[var][load_name] += val posts.append(total_load) - C.insert(total_load) - self.logger.info(f"Percentage complete: {(i+1)/count*100.0}") + collection.insert(total_load) + self.logger.info(f"Percentage complete: {(i + 1) / count * 100.0}") self.logger.debug("Insert successful") def setup_profiles(self): for element_name, collection_name in self.mapping.items(): if collection_name not in self.collections: - raise Exception(f"{collection_name} is not a valid collection for database {self.db_name}") + raise Exception( + f"{collection_name} is not a valid collection for database {self.db_name}" + ) self.logger.info(f"Reading collection: {collection_name}") - #for collection in self.collections: - C = self.db[collection_name] - record = C.find({"date": {"$exists" : 1}}) + # for collection in self.collections: + collection = self.db[collection_name] + record = collection.find({"date": {"$exists": 1}}) profile_data = record.next() - profile_data.pop('_id', None) - profile_data.pop('date', None) + profile_data.pop("_id", None) + profile_data.pop("date", None) for property, obj_info in profile_data.items(): if isinstance(obj_info, dict): @@ -95,50 +88,55 @@ def setup_profiles(self): model_found = True break if not model_found: - self.logger.warning(f"Profile {profile_name} could not be mapped to any element of type {collection_name} in the OpenDSS model") + self.logger.warning( + f"Profile {profile_name} could not be mapped to any element of type {collection_name} in the OpenDSS model" + ) for profile_name, model_list in profile_dict.items(): self.Profiles[f"{element_name}.{profile_name}.{property}"] = Profile( - self.sim_instance, - (collection_name, element_name, profile_name, property), - [self.Objects[x] for x in model_list], - self.solver, - None, - self.logger, - **self.kwargs - ) + self.sim_instance, + (collection_name, element_name, profile_name, property), + [self.Objects[x] for x in model_list], + self.solver, + None, + self.logger, + **self.kwargs, + ) def update(self): self.logger.debug(self.solver.GetDateTime()) for element_name, collection_name in self.mapping.items(): - C = self.db[collection_name] - record = C.find({'date': self.solver.GetDateTime()}) + collection = self.db[collection_name] + record = collection.find({"date": self.solver.GetDateTime()}) if not record.count(): - self.logger.warning(f"No record found for time period {self.solver.GetDateTime()} in the database") + self.logger.warning( + f"No record found for time period {self.solver.GetDateTime()} in the database" + ) else: profile_data = record.next() - profile_data.pop('_id', None) - profile_data.pop('date', None) + profile_data.pop("_id", None) + profile_data.pop("date", None) for profile in self.Profiles: self.Profiles[profile].update(profile_data) pass -class Profile(BaseProfile): - DEFAULT_SETTINGS = { - "multiplier": 1, - "normalize": False, - "interpolate": False - } +class Profile(BaseProfile): + DEFAULT_SETTINGS = {"multiplier": 1, "normalize": False, "interpolate": False} def __init__(self, sim_instance, dataset, devices, solver, mapping_dict, logger, **kwargs): - super(Profile, self).__init__(sim_instance, dataset, devices, solver, mapping_dict, logger, **kwargs) + super(Profile, self).__init__( + sim_instance, dataset, devices, solver, mapping_dict, logger, **kwargs + ) self.collection_name, self.class_name, self.profile_name, self.property = self.dataset def update_profile_settings(self): pass - def update(self, updateObjectProperties=True): - data = updateObjectProperties + def update(self, update_object_properties=True): + data = update_object_properties value = data[self.property][self.profile_name] for device in self.devices: device.SetParameter(self.property, value) + + +ProfileManager.one_Time_fix = ProfileManager.one_time_fix diff --git a/src/pydss/profile_manager/profile_interface.py b/src/pydss/profile_manager/profile_interface.py new file mode 100644 index 00000000..b8378033 --- /dev/null +++ b/src/pydss/profile_manager/profile_interface.py @@ -0,0 +1,35 @@ +from os.path import basename, isfile +from pydss.profile_manager import hooks +import importlib +import glob + + +modules = glob.glob(hooks.__path__[0] + "/*.py") +python_files = [ + (basename(f)[:-3], f) for f in modules if isfile(f) and not f.endswith("__init__.py") +] + +profile_interfaces = {} +modules_instances = {} +for module, file in python_files: + spec = importlib.util.spec_from_file_location(module, file) + modules_instances[module] = importlib.util.module_from_spec(spec) + spec.loader.exec_module(modules_instances[module]) + profile_interfaces[module] = modules_instances[module].ProfileManager + + +def create(sim_instance, solver, settings, logger, **kwargs): + source_type = settings.profiles.source_type.value + if source_type in profile_interfaces: + post_processor = profile_interfaces[source_type]( + sim_instance, solver, settings, logger, **kwargs + ) + else: + raise ModuleNotFoundError( + f"{source_type} is not a valid source type. Valid values are {','.join(list(profile_interfaces.keys()))}" + ) + return post_processor + + +ProfileInterfaces = profile_interfaces +Create = create diff --git a/src/pydss/pyControllers/Controllers/DynamicVoltageSupport.py b/src/pydss/pyControllers/Controllers/DynamicVoltageSupport.py deleted file mode 100644 index 0e1c2242..00000000 --- a/src/pydss/pyControllers/Controllers/DynamicVoltageSupport.py +++ /dev/null @@ -1,350 +0,0 @@ -#QSTS Model for Dynamic Voltage Support from Inverter Based Resources -''' -author: Erik Pohl -Version: 2.0 -''' - -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -import matplotlib.pyplot as plt -import scipy.signal as signal -import numpy as np -import math -import statistics as stats - -class DynamicVoltageSupport(ControllerAbstract): - """The controller implements Dynamic Voltage Support for Generator objects as loosely defined in 1547-2018 as "rapid reactive power exchanges during voltage excursions." - Subclass of the :class:`PyDSS.pyControllers. pyControllerAbstract.ControllerAbstract` abstract class. - - :param PvObj: A :class:`PyDSS.dssElement.dssElement` object that wraps around an OpenDSS 'Generator' element - :type PvObj: class:`PyDSS.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the DynamicVoltageSupport Controller. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit objects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classed defined in :mod:`PyDSS.SolveMode`. - :type dssSolver: :mod:`PyDSS.SolveMode` - :raises: AssertionError if 'PvObj' is not a wrapped OpenDSS Generator element - :raises: AssertionErrot if 'PvObj' is not a 3ph Generator element - - """ - def __init__(self, PvObj, Settings, dssInstance, ElmObjectList, dssSolver): - super(DynamicVoltageSupport, self).__init__(PvObj, Settings, dssInstance, ElmObjectList, dssSolver) - self._class, self._name = PvObj.GetInfo() - assert (self._class.lower()=='generator'), 'DynamicVoltageSupport works only with an OpenDSS generator element' - self.name = "Controller-{}-{}".format(self._class, self._name) - self._ControlledElm = PvObj - self.__Settings = Settings - self.__dssSolver = dssSolver - self.gen_Phases = float(PvObj.GetParameter('phases')) - assert(int(self.gen_Phases) == 3), 'DynamicVoltageSupport works only with a 3ph OpenDSS generator element. {ph} ph generator object was used.'.format( - ph=int(self.gen_Phases) - ) - - # GET GENERATOR ORIGINAL OBJECT PARAMETERS - # These will be used when resetting the generator behavior in post-fault periods - self.gen_BaseKV = float(PvObj.GetParameter('kv')) - self.gen_Srated = float(PvObj.GetParameter('kVA')) - self.gen_Kw = float(PvObj.GetParameter('kw')) - self.gen_Kvar = float(PvObj.GetParameter('kvar')) - self.prev_kvar=self.gen_Kvar - self.gen_Srated_adjusted=self.gen_Srated - self.kva_calc=math.sqrt((self.gen_Kw**2)+(self.gen_Kvar**2)) - - #GET TOML USER-DEFINED SETTINGS - self.Trv=self.__Settings['Trv'] #time-constant for voltage transducer - self.Tinv=self.__Settings['Tinv'] #time-constant for inverter controller - self.dbd1=self.__Settings['dbd1'] #single-sided lower deadband (delta V p.u) - self.dbd2=self.__Settings['dbd2'] #single-sided upper deadband (delta V p.u) - self.Kqv=self.__Settings['Kqv'] #proportional gain constant - self.iqh1=self.__Settings['iqh1'] #current limit for injection (p.u) - self.kvarmax_pu=self.__Settings['kvar_max'] #max kvar injection (p.u) (positive number) - self.kvarmin_pu=self.__Settings['kvar_min'] #max kvar absorption (p.u) (negative number) - self.post_fault_reset=self.__Settings['post_fault_reset'] #time (s) after fault before gen provides more support - self.capacitive_support=self.__Settings['capacitive_support'] #bool indicating if gen will provide capacitive support - self.inductive_support=self.__Settings['inductive_support'] #bool indicating if gen will provide inductive support - self.current_limited_error_tolerance=self.__Settings['current_limited_error_tolerance'] #error tolerance (%) when evaluating current limit adherance - self.overvoltage_kva_limited=self.__Settings['overvoltage_kva_limited'] #bool indicating if gen is kva limited - self.use_with_voltage_ride_through=self.__Settings['Use with Voltage Ride Through'] #bool indicating if DVS controller is used concurrently with VRT controller - - #CALC KVAR LIMITS - self.kvarmax=self.kvarmax_pu*self.gen_Srated - self.kvarmin=self.kvarmin_pu*self.gen_Srated - - #RESET GENERATOR MODEL - self.Model = int(PvObj.SetParameter('model',1)) #set generator model to model 1 to allow for PyDSS-controller current limit - self.vminpu = float(PvObj.SetParameter('Vminpu',0)) #using arbitrarily wide voltages so that it is always constant kW constant kVAR - self.vmaxpu = float(PvObj.SetParameter('Vmaxpu',2)) #using arbitrarily wide voltages so that it is always constant kW constant kVAR - - #SET GENERATOR CURRENT LIMITS - self.gen_imax_per_phase=self.iqh1*(self.gen_Srated/(self.gen_BaseKV*math.sqrt(3))) - - #SET UP VOLTAGE TRANSDUCER TRANSFER FUNCTION - self.V_filter_transfer_function = signal.TransferFunction([1], [self.Trv, 1]) - - #SET UP INVERTER CONTROLLER TRANSFER FUNCTION - self.inv_control_transfer_function=signal.TransferFunction([1],[self.Tinv, 1]) - - #GET/SET INITIAL CONDITIONS - self.dt = dssSolver.GetStepResolutionSeconds() - ave_v_pu = stats.mean(self._ControlledElm.sBus[0].GetVariable("puVmagAngle")[0:-2:2]) #using mean phase voltages - self.T=[0,self.dt] - self.V=[ave_v_pu,ave_v_pu] - self.V_filt=self.V[-1] - self.Vref=ave_v_pu - self.V_err=0 - self.inv_control_kw=np.array([self.gen_Kw,self.gen_Kw]) - self.inv_control_kvar=np.array([self.gen_Kvar,self.gen_Kvar]) - self.inv_out_kw_prev=self.gen_Kw*self.Tinv #spin up the inverter controller - self.inv_out_kvar_prev=self.gen_Kvar*self.Tinv #spin up the inverter controller - self.v_out_prev=ave_v_pu*self.Trv #spin up the voltage transducer - self.Fault_present=False - self.fault_reset_start=0 - self.first_step=True - self.time_advance=False - return - - def Name(self): - return self.name - - def ControlledElement(self): - return "{}.{}".format(self._class, self._name) - - def debugInfo(self): - return - - def Voltage_Transducer(self,ave_v_pu): - self.V=np.array([self.V[-1],ave_v_pu]) #update input array with the current time-step voltage - _,self.V_filt,vout=signal.lsim(self.V_filter_transfer_function,self.V,self.T,X0=self.v_out_prev) #run it through our voltage transducer to get response - self.v_out_prev=vout[-1] #record evolution of state vector for next step initial condition - - #CALCULATE V_ERROR - self.V_err=self.V_filt[-1]-self.Vref #positive V_err = overvoltage, negative V_err = undervoltage - return - - def Inverter_Controller(self,kw_kvar:str,new_setting): - if kw_kvar == 'kw': #if we are controlling kw - self.inv_control_kw=np.array([self.kw_setting_prev,new_setting]) # set to last step kvar and the desired next step - _,self.inv_control_filt_kw,inv_out_kw=signal.lsim(self.inv_control_transfer_function,self.inv_control_kw,self.T,X0=self.inv_out_kw_prev) #run through our transfer function to get response - self.inv_out_kw_prev=inv_out_kw[-1] #record evolution of state vector for next step initial conditioN - else: #if we are controlling kvar - self.inv_control_kvar=np.array([self.kvar_setting_prev,new_setting]) # set to last step kvar and the desired next step - _,self.inv_control_filt_kvar,inv_out_kvar=signal.lsim(self.inv_control_transfer_function,self.inv_control_kvar,self.T,X0=self.inv_out_kvar_prev) #run through our transfer function to get response - self.inv_out_kvar_prev=inv_out_kvar[-1] #record evolution of state vector for next step initial condition - return - - def Restore_Output(self): - new_kvar=self.gen_Kvar #set new_kvar to original q value - - #APPLY TRANSFER FUNCTION TO INVERTER CONTROLLER TO LIMIT SPEED OF KVAR CHANGES - self.Inverter_Controller('kvar',new_kvar) - new_kw=math.sqrt((self.kva_calc**2)-(self.inv_control_filt_kvar[-1]**2)) #based on the response change in kvar, we calculate the desired kw - - #APPLY TRANSFER FUNCTION TO INVERTER CONTROLLER TO LIMIT SPEED OF KW CHANGES - self.Inverter_Controller('kw',new_kw) - if round(self.inv_control_filt_kw[-1]) == round(self.gen_Kw) and round(self.inv_control_filt_kvar[-1]) == round(self.gen_Kvar): #once we get close to original value - self.Fault_present = False #end the faulted state - self.Vref=self.V_filt[-1] #reset the vref when not in a fault state - new_step_kw=self.gen_Kw #set generator kw to exactly the original value - new_step_kvar=self.gen_Kvar #set generator kvar to exactly the original value - error=0 - else: #if not close eneough, we will move to the next time step while maintaining a faulted state - new_step_kw=self.inv_control_filt_kw[-1] - new_step_kvar=self.inv_control_filt_kvar[-1] - error=0 - - return new_step_kw,new_step_kvar,error - - def Fault_Support(self): - if self.fault_reset_start == 0 or (self.T[-1]-self.fault_reset_start) >= self.post_fault_reset: #check if we have exceeded our reset timer - self.Fault_present=True - self.fault_reset_start=0 - - #UNDERVOLTAGE CONDITION - if self.V_err <= self.dbd1: - if self.capacitive_support: #check if user wants DER to provide capacitive support - if self.kvar_setting_prev < self.kvarmax: - new_kvar=self.kvar_setting_prev+(-self.V_err*self.Kqv) #calc new kvar value using gain constant - if new_kvar > self.kvarmax: #if it exceeds user-defined kvarmax - new_kvar=self.kvarmax #set to kvar_max - else: - new_kvar=self.kvarmax - else: - new_kvar=self.gen_Kvar #if no capacitive support, keep kvar value as defined in dss files - - self.gen_Srated_adjusted=self.gen_Srated - - #OVERVOLTAGE CONDITION - else: - if self.inductive_support and self.kvar_setting_prev > self.kvarmin: - new_kvar=self.kvar_setting_prev+(-self.V_err*self.Kqv) - if new_kvar < self.kvarmin: - new_kvar=self.kvarmin - elif self.inductive_support: - new_kvar=self.kvarmin - else: - new_kvar=self.gen_Kvar - - self.gen_Srated_adjusted=self.gen_Srated - - #APPLY TRANSFER FUNCTION TO INVERTER CONTROLLER TO LIMIT SPEED OF KVAR CHANGES - self.Inverter_Controller('kvar',new_kvar) #once we calculate the desired kvar response, we run it through out inverter controller to get the response - - if self.kw_setting_prev==0: #if not generating active power in previous step, keep this the same, because we can't curtail any more. - new_kw=self.kw_setting_prev - else: # else, we will assign kw value based on the desired kvar value (i.e will curtail if needed) - new_kw=math.sqrt(((self.gen_Srated_adjusted*self.iqh1)**2)-(self.inv_control_filt_kvar[-1]**2)) #calc new kw value using new kvar and the nameplate rating of gen. - #APPLY TRANSFER FUNCTION TO INVERTER CONTROLLER TO LIMIT SPEED OF KW CHANGES - self.Inverter_Controller('kw',new_kw) - - new_step_kw=self.inv_control_filt_kw[-1] - new_step_kvar=self.inv_control_filt_kvar[-1] - error=100#force another iteration to enforce a current limit when a fault is present and we are altering generator values - else: #if we haven't exceeded our reset timer, keep everything the same - self.Fault_present=False - self.Vref=self.V_filt[-1]#reset Vref to the current timestep voltage - new_step_kw=self.gen_Kw - new_step_kvar=self.gen_Kvar - error=0 - return new_step_kw, new_step_kvar, error - - def kVA_Limiter(self): - if self.kw_setting_prev>0:# if there active power generation - new_kw=math.sqrt((self.gen_Srated_adjusted**2)-(self.kvar_setting_prev**2))#calc new kw setting based on current kvar setting - if new_kw>0: #if the new kw settings does not cause kw to go negative - new_step_kw=new_kw - new_step_kvar=self.kvar_setting_prev - else: #otherwise just set kw to 0 - new_step_kw=0 - new_step_kvar=self.kvar_setting_prev - else: #if kw setting is already 0, we need to reduce kVAR - new_kvar=math.sqrt((self.gen_Srated_adjusted**2)-(self.kw_setting_prev**2)) - new_step_kw=self.kw_setting_prev #no need to calculate a new kW setting - new_step_kvar=new_kvar - error=100#force another iteration to check if we successfully enforced the kVA limit within tolerances - return new_step_kw, new_step_kvar, error - - def Current_Limiter(self,iter_v): - adjusted_kva=self.gen_imax_per_phase*(iter_v*math.sqrt(3)) #get the kva limit for the current timestep at the current voltage - if self.kw_setting_prev>0: # if there active power generation - if self.kvar_setting_prev>adjusted_kva: - new_step_kw=0 - new_step_kvar=self.kvar_setting_prev - else: - new_step_kw=math.sqrt((adjusted_kva**2)-(self.kvar_setting_prev**2))#calc new kw setting based on current kvar setting - new_step_kvar=self.kvar_setting_prev - # if new_kw>0: #if the new kw settings does not cause kw to go negative - # new_step_kw=new_kw - # new_step_kvar=self.kvar_setting_prev - # else: #otherwise just set kw to 0 - # new_step_kw=0 - # new_step_kvar=self.kvar_setting_prev - else: #if kw setting is already 0 - new_kvar=math.sqrt((adjusted_kva**2)-(self.kw_setting_prev**2)) - new_step_kw=self.kw_setting_prev #no need to calculate a new kW setting - new_step_kvar=new_kvar - error=100 #force another iteration to check if we successfully enforced the current limit within tolerances - return new_step_kw, new_step_kvar, error - - def Update(self, Priority, Time, UpdateResults): - error=0 - - if Priority == 1: - - #GET PREVIOUS STEP GENERATOR SETTINGS. THESE WILL GET UPDATED IF WARRANTED. - self.kw_setting_prev=float(self._ControlledElm.GetParameter('kw')) - self.kvar_setting_prev=float(self._ControlledElm.GetParameter('kvar')) - - #UPDATE TIME ARRAY - t=self.__dssSolver.GetTotalSeconds() - if t > self.T[-1]: #Check if this is a new timestep or a second control iteration - self.time_advance=True - self.T = np.array([self.T[-1],t]) #update time array - else: - self.time_advance=False - - #IF USING DVS WITH VRT WE WILL TRACK GENERATOR STATUS (1.0 = TRIPPED, 0.0 = IN-SERVICE) - if self.use_with_voltage_ride_through: - gen_trip_status=float(self._ControlledElm.GetParameter('Class')) #VRT controller uses opendss 'class' parameter to indicate trip status - else: - gen_trip_status=0.0 #if not using with VRT, set trip status to 0.0 (in-service) all the time. - #IF NOT USING WITH VRT OR IF USING WITH VRT AND GENERATOR IS NOT TRIPPED, THEN WE WILL EVALUATE IF WE NEED TO MAKE CHANGES TO GENERATOR KW/KVAR - if not self.use_with_voltage_ride_through or (self.use_with_voltage_ride_through and not bool(gen_trip_status)): - #COLLECT KEY QUANTITIES - ave_v_pu = stats.mean(self._ControlledElm.sBus[0].GetVariable("puVmagAngle")[0:-2:2]) - - #IF THERE IS A TIME ADVANCE, REEVALUATE GENERATOR SETTINGS - if self.time_advance or self.first_step: - self.first_step=False - #CALCULATE FILTERED VOLTAGE USING TRANSFER FUNCTION AT EACH NEW TIMESTEP - self.Voltage_Transducer(ave_v_pu) - - #GET CURRENT STEP OUTPUT FROM GENERATOR - iter_q=-1*sum(self._ControlledElm.GetVariable('Powers')[1:-1:2]) - iter_p=-1*sum(self._ControlledElm.GetVariable('Powers')[0:-1:2]) - - #NO_FAULT CONDITION: if V_err within of user-defined deadband - if self.V_err > self.dbd1 and self.V_err < self.dbd2: - if self.Fault_present: #if transitioning from a faulted state to an unfaulted state, start user-defined reset timer - self.fault_reset_start=self.T[-1] - # self.Fault_present=False - new_step_kw,new_step_kvar,error=self.Restore_Output() #start restoring output to pre-fault conditions - else: #if not transitioning from faulted state to unfaulted state, keep everything the same - self.Inverter_Controller('kw',self.gen_Kw) - self.Inverter_Controller('kvar',self.gen_Kvar) - # new_step_kw=self.inv_control_filt_kw[-1] - # new_step_kvar=self.inv_control_filt_kvar[-1] - new_step_kw=self.gen_Kw - new_step_kvar=self.gen_Kvar - error=0 - - #FAULT CONDITION: V_err is outside of user-defined deadband - else: - new_step_kw,new_step_kvar,error=self.Fault_Support() #provide fault support through DVS - - else: #RERUNNING TIMESTEP AGAIN AND ENFORCING CURRENT OR KVA LIMIT AT THE CURRENT VOLTAGE LEVEL - #GET CURRENT ITERATION KEY QUANTITIES - iter_curr=max(self._ControlledElm.GetVariable('CurrentsMagAng')[0:-2:2]) - iter_p=-1*sum(self._ControlledElm.GetVariable('Powers')[0:-1:2]) - iter_q=-1*sum(self._ControlledElm.GetVariable('Powers')[1:-1:2]) - iter_v=stats.mean(self._ControlledElm.sBus[0].GetVariable("puVmagAngle")[0:-2:2])*(self.gen_BaseKV) - - #CALCULATE IF WE OVERSHOT OR UNDERSHOT OUR CURRENT LIMIT AT THE CURRENT VOLTAGE LEVEL - curr_over_under=iter_curr-self.gen_imax_per_phase - curr_over_under_pct=curr_over_under/self.gen_imax_per_phase - - #CALCULATE IF WE OVERSHOT OUR GENERATOR KVA NAMEPLATE - kva_overshoot=(math.sqrt((iter_p**2)+(iter_q**2)))-self.gen_Srated - kva_overshoot_pct=kva_overshoot/self.gen_Srated - - #READJUST QUANTITIES AS NEEDED - if self.Fault_present: #Only adjust quantities if there is a fault present and we are changing generator behavior - if self.overvoltage_kva_limited and kva_overshoot_pct>self.current_limited_error_tolerance: #if user wants to enforce a kva limit and we have exceeded that - new_step_kw, new_step_kvar, error=self.kVA_Limiter() #run the kVA limiter - elif abs(curr_over_under_pct)>self.current_limited_error_tolerance:#if we haven't exceeded our kva limit, check if we have exceeded our current limit - if self.overvoltage_kva_limited and curr_over_under_pct<0:# if the DER is being kva limited, we allow for DER to undershoot its current limit - new_step_kw=self.kw_setting_prev - new_step_kvar=self.kvar_setting_prev - error=0 - else: #if it is not kva limited, then we will enfore only the current limit - new_step_kw, new_step_kvar, error=self.Current_Limiter(iter_v) #run the current-limiter - else: - new_step_kw=self.kw_setting_prev - new_step_kvar=self.kvar_setting_prev - error=0 - else: - new_step_kw=self.kw_setting_prev - new_step_kvar=self.kvar_setting_prev - error=0 - else: #if the generator is in a tripped state (when using with VRT controller) then keep kw and kvar at 0 - new_step_kw=0 - new_step_kvar=0 - error=0 - #CHANGE GENERATOR SETTINGS - self._ControlledElm.SetParameter('kw', new_step_kw) - self._ControlledElm.SetParameter('kvar', new_step_kvar) - - else: - error=0 - return error diff --git a/src/pydss/pyControllers/Controllers/FaultController.py b/src/pydss/pyControllers/Controllers/FaultController.py deleted file mode 100644 index bba317d7..00000000 --- a/src/pydss/pyControllers/Controllers/FaultController.py +++ /dev/null @@ -1,68 +0,0 @@ -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract - -class FaultController(ControllerAbstract): - """The class is used to induce faults on bus for dynamic simulation studies. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param FaultObj: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'Fault' element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the faul controller. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` instance - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit ojects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classes defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` - :raises: AssertionError if 'FaultObj' is not a wrapped OpenDSS Fault element - - """ - def __init__(self, FaultObj, Settings, dssInstance, ElmObjectList, dssSolver): - """Constructor method - """ - - super(FaultController, self).__init__(FaultObj, Settings, dssInstance, ElmObjectList, dssSolver) - self.P_old = 0 - self.Time = -1 - self.__Locked = False - self.__dssSolver = dssSolver - self.__FaultObj = FaultObj - self.__FaultEnabled = False - self.__Settings = Settings - nPhases = len(Settings['Bus1'].split('.')) - 1 - FaultObj.SetParameter('bus1', Settings['Bus1']) - FaultObj.SetParameter('bus2', Settings['Bus2']) - FaultObj.SetParameter('phases', nPhases) - FaultObj.SetParameter('r', Settings['Fault resistance']) - self.__Class, Name = self.__FaultObj.GetInfo() - assert (self.__Class.lower() == 'fault'), 'FaultController works only with an OpenDSS Fault element' - self.__Name = 'pyCont_' + self.__Class + '_' + Name - self.__init_time = dssSolver.GetDateTime() - self.__Settings['Fault end time (sec)'] = self.__Settings['Fault start time (sec)'] + \ - self.__Settings['Fault duration (sec)'] - return - - def Update(self, Priority, Time, UpdateResults): - """Induces and removes a fault as the simulation runs as per user defined settings. - """ - time = self.__dssSolver.GetTotalSeconds() - self.__dssSolver.GetStepSizeSec() - self.__FaultObj.SetParameter('enabled', 'yes') - if time >= self.__Settings['Fault start time (sec)'] and time < self.__Settings['Fault end time (sec)'] and\ - self.__FaultEnabled==False: - self.__FaultObj.SetParameter('enabled', 'yes') - self.__FaultEnabled = True - elif time >= self.__Settings['Fault end time (sec)'] and self.__FaultEnabled==True: - self.__FaultObj.SetParameter('enabled', 'no') - self.__FaultEnabled = False - return 0 - - def Name(self): - return self.__Name - - def ControlledElement(self): - return "{}.{}".format(self.__Class, self.__Name) - - def debugInfo(self): - return [self.__Settings['Control{}'.format(i+1)] for i in range(3)] - - - diff --git a/src/pydss/pyControllers/Controllers/GenController.py b/src/pydss/pyControllers/Controllers/GenController.py deleted file mode 100644 index 4bcf2833..00000000 --- a/src/pydss/pyControllers/Controllers/GenController.py +++ /dev/null @@ -1,148 +0,0 @@ -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -import math -import abc - -class GenController(ControllerAbstract): - """Implementation of smart control modes of modern inverter systems. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param PvObj: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'PVSystem' element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the PvController. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit objects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` - :raises: AssertionError if 'PvObj' is not a wrapped OpenDSS PVSystem element - - """ - - def __init__(self, GenObj, Settings, dssInstance, ElmObjectList, dssSolver): - """Constructor method - """ - super(GenController, self).__init__(GenObj, Settings, dssInstance, ElmObjectList, dssSolver) - self.TimeChange = False - self.Time = (-1, 0) - - self.oldPcalc = 0 - self.oldQcalc = 0 - - self.__vDisconnected = False - self.__pDisconnected = False - - self.__ElmObjectList = ElmObjectList - self.ControlDict = { - 'None' : lambda: 0, - 'VVar' : self.VVARcontrol, - } - - self.__ControlledElm = GenObj - self.ceClass, self.ceName = self.__ControlledElm.GetInfo() - - assert (self.ceClass.lower()=='generator'), 'GenController works only with an OpenDSS generator element' - self.__Name = 'pyCont_' + self.ceClass + '_' + self.ceName - if '_' in self.ceName: - self.Phase = self.ceName.split('_')[1] - else: - self.Phase = None - self.__ElmObjectList = ElmObjectList - self.__ControlledElm = GenObj - self.__dssInstance = dssInstance - self.__dssSolver = dssSolver - self.__Settings = Settings - - self.__BaseKV = float(GenObj.GetParameter('kv')) - self.__Srated = float(GenObj.GetParameter('kVA')) - self.__Prated = float(GenObj.GetParameter('kW')) - self.__Qrated = float(GenObj.GetParameter('maxkvar')) - GenObj.SetParameter('minkvar', -self.__Qrated) - - if Settings['Model as PVsystem']: - GenObj.SetParameter('model', 3) - - - self.__dampCoef = Settings['DampCoef'] - - self.__PFrated = Settings['PFlim'] - self.Pmppt = 100 - self.pf = 1 - - self.update = [self.ControlDict[Settings['Control' + str(i)]] for i in [1, 2, 3]] - self.QlimPU = min(self.__Qrated / self.__Srated, 1.0) - return - - - def Name(self): - return self.__Name - - def ControlledElement(self): - return "{}.{}".format(self.ceClass, self.ceName) - - def debugInfo(self): - return [self.__Settings['Control{}'.format(i+1)] for i in range(3)] - - def Update(self, Priority, Time, Update): - self.TimeChange = self.Time != (Priority, Time) - self.Time = (Priority, Time) - if not self.TimeChange: - self.Itr += 1 - else: - self.Itr = 0 - return self.update[Priority]() - - def VVARcontrol(self): - """Volt / var control implementation - """ - uMin = self.__Settings['uMin'] - uMax = self.__Settings['uMax'] - pfLim = self.__Settings['PFlim'] - uDbMin = self.__Settings['uDbMin'] - uDbMax = self.__Settings['uDbMax'] - Priority = self.__Settings['Priority'] - - uIn = max(self.__ControlledElm.sBus[0].GetVariable('puVmagAngle')[::2]) - - m1 = self.QlimPU / (uMin - uDbMin) - m2 = self.QlimPU / (uDbMax - uMax) - c1 = self.QlimPU * uDbMin / (uDbMin - uMin) - c2 = self.QlimPU * uDbMax / (uMax - uDbMax) - - phases = int(self.__ControlledElm.GetParameter('phases')) - S = self.__ControlledElm.GetVariable('Powers')[: 2*phases] - Ppv = abs(sum(S[::2])) - Pcalc = Ppv / self.__Srated - Qpv = -sum(S[1::2]) - Qpv = Qpv / self.__Srated - - Qcalc = 0 - if uIn <= uMin: - Qcalc = self.QlimPU - elif uIn <= uDbMin and uIn > uMin: - Qcalc = uIn * m1 + c1 - elif uIn <= uDbMax and uIn > uDbMin: - Qcalc = 0 - elif uIn <= uMax and uIn > uDbMax: - Qcalc = uIn * m2 + c2 - elif uIn >= uMax: - Qcalc = -self.QlimPU - - # adding heavy ball term to improve convergence - a = 0.7 + 0.5 * self.__dampCoef * (1 - self.Itr / self.__dssSolver.MaxIterations) - b = 0.1 / self.__dampCoef - Qcalc = Qpv + (Qcalc - Qpv) * a + (Qpv - self.oldQcalc) * b - dQ = Qcalc - Qpv - - #Qcalc = Qpv + (Qcalc - Qpv)# + (Qpv - self.oldQcalc) * 0.1 / self.__dampCoef - #dQ = Qcalc - Qpv - - Plim = 1 - if Priority == 'Var': - Plim = (1 - Qcalc ** 2) ** 0.5 - elif Priority == 'Watt': - Qlim = (1 - Ppv/self.__Prated ** 2) ** 0.5 - - self.__ControlledElm.SetParameter('kW', self.__Prated * Plim) - self.__ControlledElm.SetParameter('kvar', self.__Srated * Qcalc) - return abs(dQ) diff --git a/src/pydss/pyControllers/Controllers/MotorStall.py b/src/pydss/pyControllers/Controllers/MotorStall.py deleted file mode 100644 index ba8c77db..00000000 --- a/src/pydss/pyControllers/Controllers/MotorStall.py +++ /dev/null @@ -1,134 +0,0 @@ -#Algebraic model for Type D motor - residential air conditioner - -import scipy.signal as signal -import numpy as np -import math - -from pydss.pyControllers.models import MotorStallSettings -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract - -class MotorStall(ControllerAbstract): - def __init__(self, motor_obj, settings, dss_instance, elm_object_list, dss_solver): - super(MotorStall, self).__init__(motor_obj, settings, dss_instance, elm_object_list, dss_solver) - - self._class, self._name = motor_obj.GetInfo() - assert self._class == "Load", "Motor stall model can only be used with load models" - assert motor_obj.NumPhases == 1, "Motor stall model can only be used with single phase loads" - - self.name = "Controller-{}-{}".format(self._class, self._name) - self._controlled_element = motor_obj - self._settings = MotorStallSettings(**settings) - self._dss_solver = dss_solver - self.mode = 3 - self.model_mode = self._controlled_element.SetParameter('model', self.mode) # 3 - motor, 1 - Standard constant P+jQ - self._controlled_element.SetParameter('vminpu', 0.0) - - self.kw_rated = self._controlled_element.GetParameter('kw') - self.kvar_rated = self._controlled_element.GetParameter('kvar') - self.kva_rated = (self.kw_rated**2 + self.kvar_rated**2)**0.5 - - self.stall_time_start = 0 - self.stall = False - - self.r_stall_pu = self._settings.r_stall_pu - self.va_base = 100 * 1e6 - self.kvbase = self._controlled_element.sBus[0].GetVariable("kVBase") - self.i_base = 1e3 * self.kw_rated / 1e3 * self.kvbase - - self.dt = dss_solver.GetStepResolutionSeconds() - self.h = signal.TransferFunction([1], [self._settings.t_th, 1]) - self.r = signal.TransferFunction([1], [self._settings.t_restart, 1]) - - self.u = [0, 0] - self.t_arr = [0, self.dt] - self.x = 0 - - self.i2r = 0 - - self.p_stall = 0 - self.q_stall = 0 - - return - - def Name(self): - return self.name - - def ControlledElement(self): - return "{}.{}".format(self._class, self._name) - - def debugInfo(self): - return - - def Update(self, Priority, time, update_results): - self.t = self._dss_solver.GetTotalSeconds() - if self.i_base: - self.current_pu = self._controlled_element.GetVariable('CurrentsMagAng')[0] / self.i_base - self.voltage = self._controlled_element.GetVariable('VoltagesMagAng')[0] - self.p = self._controlled_element.GetVariable('Powers')[0] - self.q = self._controlled_element.GetVariable('Powers')[1] - self.voltage_pu = self._controlled_element.sBus[0].GetVariable("puVmagAngle")[0] - - if Priority == 0: - - i2r = self.current_pu ** 2 * self.r_stall_pu - self.i2r = max(self.i2r, i2r) - self.t = np.array([self.t_arr [-1], self.t]) - self.u = np.array([self.u[-1], self.i2r]) - tout, yout, xout = signal.lsim(self.h, self.u, self.t_arr, self.x) - self.x = xout[-1] - i2r_calc = yout[-1] / 50.0 - - - comp_lf = self.p / self.kw_rated - comp_pf = 0.75 #self.p / (self.p**2 + self.q**2)**0.5 - - v_stall_adj = self._settings.v_stall*(1 + self._settings.lf_adj * (comp_lf-1)) - v_break_adj = self._settings.v_break*(1 + self._settings.lf_adj * (comp_lf-1)) - - p0 = 1 - self._settings.k_p1 * (1-v_break_adj)**self._settings.n_p1 - q0 = ((1 - comp_pf**2)**0.5 / comp_pf)-self._settings.k_q1*(1-v_break_adj)**self._settings.n_q1 - - p = self.p / self.kw_rated - q = self.q / self.kvar_rated - - if self.voltage_pu > v_break_adj and not self.stall: - p = p0 + self._settings.k_p1*(self.voltage_pu-v_break_adj)**self._settings.n_p1 - q = q0 + self._settings.k_q1*(self.voltage_pu-v_break_adj)**self._settings.n_q1 - self._controlled_element.SetParameter('kw', self.kw_rated * p) - self._controlled_element.SetParameter('kvar', self.kvar_rated * q) - - elif self.voltage_pu <= v_break_adj and not self.stall: - p = p0 + self._settings.k_p2 * (v_break_adj - self.voltage_pu)**self._settings.n_p2 - q = q0 + self._settings.k_q2 * (v_break_adj - self.voltage_pu)**self._settings.n_q2 - - self._controlled_element.SetParameter('kw', self.kw_rated * p ) - self._controlled_element.SetParameter('kvar', self.kvar_rated * q) - - if self.voltage_pu < v_stall_adj and not self.stall: - self.p_stall = self._controlled_element.GetParameter('kw') - self.q_stall = self._controlled_element.GetParameter('kvar') - self.stall_time_start = self._dss_solver.GetTotalSeconds() - self.stall = True - - if self.voltage_pu > v_stall_adj and self.stall: - self.stall_time = self._dss_solver.GetTotalSeconds() - self.stall_time_start - if self.stall_time < self._settings.t_stall: - self._controlled_element.SetParameter('kw', self.p_stall) - self._controlled_element.SetParameter('kvar', self.q_stall) - else: - if i2r_calc < self._settings.t_th1t: - Kth = 1 - elif i2r_calc > self._settings.t_th2t: - Kth = 0 - else: - m = 1 / (self._settings.t_th1t - self._settings.t_th2t) - c = - m * self._settings.t_th2t - Kth = m * i2r_calc + c - - self._controlled_element.SetParameter('kw', self.p_stall * Kth ) - self._controlled_element.SetParameter('kvar', self.q_stall * Kth ) - - self.model_mode_old = self.model_mode - - return 0 - diff --git a/src/pydss/pyControllers/Controllers/MotorStallBackup.py b/src/pydss/pyControllers/Controllers/MotorStallBackup.py deleted file mode 100644 index 1f32ca12..00000000 --- a/src/pydss/pyControllers/Controllers/MotorStallBackup.py +++ /dev/null @@ -1,196 +0,0 @@ -#Algebraic model for Type D motor - Residential air conditioner -''' -author: Kapil Duwadi -Version: 1.0 -''' - -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -import matplotlib.pyplot as plt -import scipy.signal as signal -import numpy as np -import random -import math -import os - -class MotorStallBackup(ControllerAbstract): - """The controller locks a regulator in the event of reverse power flow. Subclass of the :class:`pydss.pyControllers. - pyControllerAbstract.ControllerAbstract` abstract class. - - :param RegulatorObj: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'Regulator' element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the PvController. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit objects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` - :raises: AssertionError if 'RegulatorObj' is not a wrapped OpenDSS Regulator element - - """ - - def __init__(self, MotorObj, Settings, dssInstance, ElmObjectList, dssSolver): - super(MotorStallBackup, self).__init__(MotorObj, Settings, dssInstance, ElmObjectList, dssSolver) - self._class, self._name = MotorObj.GetInfo() - assert self._class == "Load", "Motor stall model can only be used with load models" - assert MotorObj.NumPhases == 1, "Motor stall model can only be used with single phase loads" - - self.name = "Controller-{}-{}".format(self._class, self._name) - self._ControlledElm = MotorObj - self.__Settings = Settings - self.__dssSolver = dssSolver - self.mode = 3 - self.model_mode = self._ControlledElm.SetParameter('model', self.mode) # 3 - motor, 1 - Standard constant P+jQ - self._ControlledElm.SetParameter('vminpu', 0.0) - - self.kw = self.__Settings['ratedKW'] - S = self.kw / self.__Settings['ratedPF'] - self.kvar = math.sqrt(S**2 - self.kw**2) - mode = 2 - - if mode == 1: - self.kw_rated = self._ControlledElm.SetParameter('kw', self.kw) - self.kvar_rated = self._ControlledElm.SetParameter('kvar', self.kvar) - elif mode == 2: - self.kw_rated = self._ControlledElm.GetParameter('kw') - self.kvar_rated = self._ControlledElm.GetParameter('kvar') - self.kva_rated = (self.kw_rated**2 + self.kvar_rated**2)**0.5 - - self.stall_time_start = 0 - self.stall = False - self.disconnected =False - self.Tdisconnect_start = 0 - - self.R_stall_pu = self.__Settings['R_stall_pu'] - self.kvbase = self._ControlledElm.sBus[0].GetVariable("kVBase") - self.Ibase = 1e3 * self.kva_rated /1e3 * self.kvbase - - self.dt = dssSolver.GetStepResolutionSeconds() - self.H = signal.TransferFunction([1], [self.__Settings['Tth'], 1]) - self.R = signal.TransferFunction([1], [self.__Settings['Trestart'], 1]) - - self.U = [35.029919199211, 35.029919199211] - self.T = [0, self.dt] - self.X = 0 - - self.rU = [0, 0] - self.rT = [0, self.dt] - self.rX = 0 - self.v = 0 - - self.voltage_pu_old = 1.0 - # Delete after debug # - - self.file = open(r"C:\Users\alatif\Desktop\NAERM\models\17735\motor.csv", "w") - self.file.write("CompLF,CompPF,V_stall_adj,V_break_adj,P0,Q0,p_run_pu,q_run_pu,p_stall_pu,q_stall_pu\n") - - return - - def Name(self): - return self.name - - def ControlledElement(self): - return "{}.{}".format(self._class, self._name) - - def debugInfo(self): - return - - def Update(self, Priority, Time, UpdateResults): - - self.t = self.__dssSolver.GetTotalSeconds() - self.current_pu = self._ControlledElm.GetVariable('CurrentsMagAng')[0] / self.Ibase - self.voltage = self._ControlledElm.GetVariable('VoltagesMagAng')[0] - self.p = self._ControlledElm.GetVariable('Powers')[0] - self.q = self._ControlledElm.GetVariable('Powers')[1] - self.voltage_pu = self._ControlledElm.sBus[0].GetVariable("puVmagAngle")[0] - - if Priority == 0: - - u = self.current_pu ** 2 * self.R_stall_pu - - CompLF = 1.0 #self.p / self.kw_rated - CompPF = self.p / (self.p**2 + self.q**2)**0.5 - - V_stall_adj = self.__Settings['Vstall']*(1 + self.__Settings['LFadj'] * (CompLF-1)) - V_break_adj = self.__Settings['Vbreak']*(1 + self.__Settings['LFadj'] * (CompLF-1)) - - P0 = 1 - self.__Settings['Kp1'] * (1-V_break_adj)**self.__Settings['Np1'] - Q0 = ((1 - CompPF**2)**0.5 / CompPF)-self.__Settings['Kq1']*(1-V_break_adj)**self.__Settings['Nq1'] - - - p_run_pu = P0 + self.__Settings['Kp1']*(self.voltage_pu-V_break_adj)**self.__Settings['Np1'] - q_run_pu = Q0 + self.__Settings['Kq1']*(self.voltage_pu-V_break_adj)**self.__Settings['Nq1'] - - p_stall_pu = P0 + self.__Settings['Kp2'] * (V_break_adj - self.voltage_pu)**self.__Settings['Np2'] - q_stall_pu = Q0 + self.__Settings['Kq2'] * (V_break_adj - self.voltage_pu)**self.__Settings['Nq2'] - - self.file.write(f"{CompLF},{CompPF},{V_stall_adj},{V_break_adj},{P0},{Q0},{p_run_pu},{q_run_pu},{p_stall_pu},{q_stall_pu}\n") - self.file.flush() - - if self.voltage_pu < V_stall_adj and self.stall and self.model_mode == self.mode: - self.stall_time = self.__dssSolver.GetTotalSeconds() - self.stall_time_start - if self.stall_time > self.__Settings['Tstall']: - self._ControlledElm.SetParameter('kw', self.kw_rated * self.__Settings['Pfault']) - self._ControlledElm.SetParameter('kvar', self.kvar_rated * self.__Settings['Qfault']) - self.model_mode = self._ControlledElm.SetParameter('model', 2) - - if self.voltage_pu < V_stall_adj and not self.stall: - self.stall_time_start = self.__dssSolver.GetTotalSeconds() - self.stall = True - - u = 1 if self.model_mode == 2 else 0 - - self.T = np.array([self.T[-1], self.t]) - self.U = np.array([self.U[-1], u]) - tout, yout, xout = signal.lsim(self.H, self.U, self.T, self.X) - self.X = xout[-1] - theeta = yout[-1] - - if theeta < self.__Settings['Tth1t']: - Kth = 1 - elif theeta > self.__Settings['Tth2t']: - Kth = 0 - else: - m = 1 / (self.__Settings['Tth1t'] - self.__Settings['Tth2t']) - c = - m * self.__Settings['Tth2t'] - Kth = m * theeta + c - - if self.model_mode == 2: - p_stall = self.kw_rated * self.__Settings['Pfault'] - q_stall = self.kvar_rated * self.__Settings['Qfault'] - - - if p_stall * Kth > self.kw_rated: - self._ControlledElm.SetParameter('kw', p_stall * Kth) - else: - self._ControlledElm.SetParameter('kw', self.kw_rated * p_run_pu) - - #TODO: uncommect the line below and comment the if statements below to allow q to fall below self.kvar_rated - #self._ControlledElm.SetParameter('kvar', q_stall * Kth) - - if q_stall * Kth > self.kvar_rated: - self._ControlledElm.SetParameter('kvar', self.p * Kth) - else: - self._ControlledElm.SetParameter('kvar', self.kvar_rated* q_run_pu) - - if Kth==0 and self.voltage_pu > self.__Settings['Vrstrt']: - self.v = 1 - - self.rT = np.array([self.rT[-1], self.t]) - self.rU = np.array([self.rU[-1], self.v]) - _, yout_r, xout_r = signal.lsim(self.R, self.rU, self.rT, self.rX) - self.rX = xout_r[-1] - reconnect = yout_r[-1] - - set_p = self._ControlledElm.GetParameter("kw") - set_q = self._ControlledElm.GetParameter("kvar") - - if set_p == self.kw_rated and set_q== self.kvar_rated and self.stall and reconnect>0: - self.model_mode = self._ControlledElm.SetParameter('model', self.mode) - self.stall = False - - self.voltage_pu_old = self.voltage_pu - self.model_mode_old = self.model_mode - return 0 - diff --git a/src/pydss/pyControllers/Controllers/MotorStallSimple.py b/src/pydss/pyControllers/Controllers/MotorStallSimple.py deleted file mode 100644 index a81443f9..00000000 --- a/src/pydss/pyControllers/Controllers/MotorStallSimple.py +++ /dev/null @@ -1,80 +0,0 @@ -#Algebraic model for Type D motor - Residential air conditioner -''' -author: Aadil Latif -Version: 1.0 -''' - -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -from pydss.pyControllers.models import MotorStallSimpleSettings - -class MotorStallSimple(ControllerAbstract): - - def __init__(self, motor_obj, settings, dss_instance, elm_object_list, dss_solver): - super(MotorStallSimple, self).__init__(motor_obj, settings, dss_instance, elm_object_list, dss_solver) - self._class, self._name = motor_obj.GetInfo() - self.name = "Controller-{}-{}".format(self._class, self._name) - self._controlled_element = motor_obj - self._settings = MotorStallSimpleSettings(**settings) - self._dss_solver = dss_solver - - self._controlled_element.SetParameter('model', 3) - self._controlled_element.SetParameter('vminpu', 0.0) - self._controlled_element.SetParameter('vlowpu', 0.0) - self.kw = self._controlled_element.GetParameter('kw') - self.kvar = self._controlled_element.GetParameter('kvar') - - self.stall_time_start = 0 - self.stall = False - self.disconnected =False - self.t_disconnect_start = 0 - - return - - - def Name(self): - return self.name - - def ControlledElement(self): - return "{}.{}".format(self._class, self._name) - - def debugInfo(self): - return [] #[self._settings['Control{}'.format(i+1)] for i in range(3)] - - - def Update(self, priority, time, _): - assert priority in [0, 1, 2], "Valid control priorities can range from 0-2." - v_e_mags = 1.0 - if priority == 0: - v_base = self._controlled_element.sBus[0].GetVariable('kVBase') * 1000 - v_e_mags = max(self._controlled_element.GetVariable('VoltagesMagAng')[::2])/ v_base - if v_e_mags < self._settings.v_stall and not self.stall and not self.disconnected: - self._controlled_element.SetParameter('kw', self.kw * self._settings.p_fault) - self._controlled_element.SetParameter('kvar', self.kvar * self._settings.q_fault ) - self._controlled_element.SetParameter('model', 2) - self.stall = True - self.stall_time_start = self._dss_solver.GetTotalSeconds() - return 0.1 - return 0 - if priority == 1: - if self.stall: - - self.stall_time = self._dss_solver.GetTotalSeconds() - self.stall_time_start - if self.stall_time > self._settings.t_protection: - self.stall = False - self.disconnected = True - self._controlled_element.SetParameter('kw', 0) - self._controlled_element.SetParameter('kvar', 0) - self.t_disconnect_start = self._dss_solver.GetTotalSeconds() - return 0 - return 0 - if priority == 2: - if self.disconnected: - time = self._dss_solver.GetTotalSeconds() - self.t_disconnect_start - if time > self._settings.t_reconnect and v_e_mags > self._settings.v_stall: - self.disconnected = False - self._controlled_element.SetParameter('kw', self.kw) - self._controlled_element.SetParameter('kvar', self.kvar) - self._controlled_element.SetParameter('model', 3) - self._controlled_element.SetParameter('vminpu', 0.0) - return 0 - diff --git a/src/pydss/pyControllers/Controllers/PvController.py b/src/pydss/pyControllers/Controllers/PvController.py deleted file mode 100644 index dcd8f427..00000000 --- a/src/pydss/pyControllers/Controllers/PvController.py +++ /dev/null @@ -1,311 +0,0 @@ -from collections import namedtuple -import math - -from pydss.pyControllers.enumerations import SmartControls, ControlPriority, VoltWattCurtailmentStrategy, VoltageCalcModes -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -from pydss.pyControllers.models import PvControllerModel - - -class PvController(ControllerAbstract): - """Implementation of smart control modes of modern inverter systems. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param pv_obj: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'PVSystem' element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param settings: A dictionary that defines the settings for the PvController. - :type settings: dict - :param dss_instance: An :class:`opendssdirect` instance - :type dss_instance: :class:`opendssdirect` - :param element_object_list: Dictionary of all dssElement, dssBus and dssCircuit objects - :type element_object_list: dict - :param dss_solver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dss_solver: :mod:`pydss.SolveMode` - :raises: Assertionerror if 'pv_obj' is not a wrapped OpenDSS PVSystem element - - """ - - def __init__(self, pv_obj, settings, dss_instance, element_object_list, dss_solver): - """Constructor method - """ - - super(PvController, self).__init__(pv_obj, settings, dss_instance, element_object_list, dss_solver) - self.time_change = False - self.time = (-1, 0) - self.old_q_pv = 0 - self.old_p_calc = 0 - - self._v_disconnected = False - self._p_disconnected = False - - self._element_object_list = element_object_list - self.control_dict = { - SmartControls.NONE : lambda: 0, - SmartControls.CONSTANT_POWER_FACTOR : self.constant_powerfactor_control, - SmartControls.VARIABLE_POWER_FACTOR : self.variable_powerfactor_control, - SmartControls.VOLT_VAR : self.volt_var_control, - SmartControls.VOLT_WATT : self.volt_watt_control, - SmartControls.TRIP : self.cutoff_control, - } - - self._controlled_element = pv_obj - self.ce_class, self.ce_name = self._controlled_element.GetInfo() - - assert (self.ce_class.lower()=='pvsystem'), 'PvController works only with an OpenDSS PVSystem element' - self._name = 'pyCont_' + self.ce_class + '_' + self.ce_name - if '_' in self.ce_name: - self.phase = self.ce_name.split('_')[1] - else: - self.phase = None - self._element_object_list = element_object_list - self._controlled_element = pv_obj - self._dss_instance = dss_instance - self._dss_solver = dss_solver - self._settings = PvControllerModel(**settings) - - self._base_kv = float(pv_obj.GetParameter('kv')) - self._s_rated = float(pv_obj.GetParameter('kVA')) - self._p_rated = float(pv_obj.GetParameter('Pmpp')) - self._q_rated = float(pv_obj.GetParameter('kvarMax')) - self._cutin = float(pv_obj.SetParameter('%cutin', 0)) / 100 - self._cutout = float(pv_obj.SetParameter('%cutout', 0)) / 100 - self._damp_coef = self._settings.damp_coef - self._pf_rated = self._settings.pf_lim - self.p_mppt = 100 - self.pf = 1 - - self.update = [] - - for i in range(1, 4): - controller_type = getattr(self._settings, 'control' + str(i)) - self.update.append(self.control_dict[controller_type]) - - - if self._settings.priority == ControlPriority.VAR: - pv_obj.SetParameter('Wattpriority', "False") - elif self._settings.priority == ControlPriority.WATT: - pv_obj.SetParameter('Wattpriority', "True") - #pv_obj.SetParameter('VarFollowInverter', "False") - - #self.q_lim_pu = self._q_rated / self._s_rated if self._q_rated < self._s_rated else 1 - - self.q_lim_pu = min(self._q_rated / self._s_rated, self._settings.q_lim_pu, 1.0) - self.itr = 0 - return - - def Name(self): - return self._name - - def ControlledElement(self): - return "{}.{}".format(self.ce_class, self.ce_name) - - def debugInfo(self): - return [getattr(self._settings, 'control' + str(i)) for i in range(3)] - - def Update(self, priority, time, update): - self.time_change = self.time != (priority, time) - self.time = (priority, time) - p_pv = -sum(self._controlled_element.GetVariable('Powers')[::2]) / self._p_rated - - if self.time_change: - self.itr = 0 - else: - self.itr += 1 - - if self._p_disconnected: - if p_pv < self._cutin: - return 0 - else: - self._p_disconnected = False - else: - if p_pv < self._cutout: - self._p_disconnected = True - self._controlled_element.SetParameter('pf', 1) - return 0 - return self.update[priority]() - - def volt_watt_control(self): - """Volt / Watt control implementation - """ - u_min_c = self._settings.u_min_c - u_max_c = self._settings.u_max_c - p_min = self._settings.p_min_vw / 100 - - u_in = max(self._controlled_element.sBus[0].GetVariable('puVmagAngle')[::2]) - p_pv = -sum(self._controlled_element.GetVariable('Powers')[::2]) / self._s_rated - q_pv = -sum(self._controlled_element.GetVariable('Powers')[1::2]) / self._s_rated - - - #p_pvoutPU = p_pv / self._p_rated - - p_lim = (1 - q_pv ** 2) ** 0.5 if self._settings.vw_type == VoltWattCurtailmentStrategy.AVAILABLE_POWER else 1 - m = (1 - p_min) / (u_min_c - u_max_c) - #m = (p_lim - p_min) / (u_min_c - u_max_c) - c = ((p_min * u_min_c) - u_max_c) / (u_min_c - u_max_c) - - if u_in < u_min_c: - p_calc = p_lim - elif u_in < u_max_c and u_in > u_min_c: - p_calc = min(m * u_in + c, p_lim) - else: - p_calc = p_min - - if p_pv > p_calc or (p_pv > 0 and self.p_mppt < 100): - # adding heavy ball term to improve convergence - dp = (p_pv - p_calc) * 0.5 / self._damp_coef + (self.old_p_calc - p_pv) * 0.1 / self._damp_coef - p_calc = p_pv - dp - self.p_mppt = min(self.p_mppt * p_calc / p_pv, 100) - self._controlled_element.SetParameter('%Pmpp', self.p_mppt) - self.pf = math.cos(math.atan(q_pv / p_calc)) - if q_pv < 0: - self.pf = -self.pf - self._controlled_element.SetParameter('pf', self.pf) - else: - dp = 0 - - error = abs(dp) - self.old_p_calc = p_pv - return error - - def cutoff_control(self): - """Over voltage trip implementation - """ - u_in = max(self._controlled_element.sBus[0].GetVariable('puVmagAngle')[::2]) - u_cut = self._settings['%u_cutoff'] - if u_in >= u_cut: - self._controlled_element.SetParameter('%Pmpp', 0) - self._controlled_element.SetParameter('pf', 1) - if self._v_disconnected: - return 0 - else: - self._v_disconnected = True - return self._p_rated - - if self.time_change and self._v_disconnected and u_in < u_cut: - self._controlled_element.SetParameter('%Pmpp', self.p_mppt) - self._controlled_element.SetParameter('pf', self.pf) - self._v_disconnected = False - return self._p_rated - - return 0 - - def constant_powerfactor_control(self): - """Constant power factor implementation - """ - pf_set = self._settings.pf - pf_act = self._controlled_element.GetParameter('pf') - p_pv = abs(sum(self._controlled_element.GetVariable('Powers')[::2])) / self._s_rated - q_pv = -sum(self._controlled_element.GetVariable('Powers')[1::2]) / self._s_rated - - if self._settings.priority == ControlPriority.PF: - # if self.time_change: - p_lim = pf_set * 100 - self._controlled_element.SetParameter('%Pmpp', p_lim) - # else: - else: - if self._settings.priority == ControlPriority.VAR: - #add code for var priority here - p_lim = 0 - else: - p_lim = 1 - if self.time_change: - self.p_mppt = 100 - else: - self.p_mppt = p_lim * self._s_rated - - error = abs(pf_set + pf_act) - self._controlled_element.SetParameter('pf', str(-pf_set)) - return error - - def variable_powerfactor_control(self): - """Variable power factor control implementation - """ - p_min = self._settings.p_min - p_max = self._settings.p_max - pf_min = self._settings.pf_min - pf_max = self._settings.pf_max - self._dss_solver.reSolve() - p_calc = abs(sum(-(float(x)) for x in self._controlled_element.GetVariable('Powers')[0::2]) ) / self._s_rated - if p_calc > 0: - if p_calc < p_min: - pf = pf_max - elif p_calc > p_max: - pf = pf_min - else: - m = (pf_max - pf_min) / (p_min - p_max) - c = (pf_min * p_min - pf_max * p_max) / (p_min - p_max) - pf = p_calc * m + c - else: - pf = pf_max - - self._controlled_element.SetParameter('irradiance', 1) - self._controlled_element.SetParameter('pf', str(-pf)) - self._dss_solver.reSolve() - - for i in range(10): - error = pf + float(self._controlled_element.GetParameter('pf')) - if abs(error) < 1E-4: - break - p_irr = float(self._controlled_element.GetParameter('irradiance')) - self._controlled_element.SetParameter('pf', str(-pf)) - self._controlled_element.SetParameter('irradiance', p_irr * (1 + error*1.5)) - self._dss_solver.reSolve() - - return 0 - - def volt_var_control(self): - """Volt / var control implementation - """ - - u_mag = self._controlled_element.GetVariable('VoltagesMagAng')[::2] - u_mag = [i for i in u_mag if i != 0] - kv_base = self._controlled_element.sBus[0].GetVariable('kVBase') * 1000 - - if self._settings.voltage_calc_mode == VoltageCalcModes.MAX: - u_in = max(u_mag) / kv_base - elif self._settings.voltage_calc_mode == VoltageCalcModes.AVG: - u_in = sum(u_mag) / (len(u_mag) * kv_base) - elif self._settings.voltage_calc_mode == VoltageCalcModes.MIN: - u_in = min(u_mag) / kv_base - elif self._settings.voltage_calc_mode == VoltageCalcModes.A: - u_in = u_mag[0] / kv_base - elif self._settings.voltage_calc_mode == VoltageCalcModes.B: - u_in = u_mag[1] / kv_base - elif self._settings.voltage_calc_mode == VoltageCalcModes.C: - u_in = u_mag[3] / kv_base - else: - u_in = max(u_mag) / kv_base - - p_pv = abs(sum(self._controlled_element.GetVariable('Powers')[::2])) - p_calc = p_pv / self._s_rated - q_pv = -sum(self._controlled_element.GetVariable('Powers')[1::2]) - q_pv = q_pv / self._s_rated - - q_calc = 0 - if u_in <= self._settings.u_min: - q_calc = self.q_lim_pu - elif u_in <= self._settings.u_db_min and u_in > self._settings.u_min: - m1 = self.q_lim_pu / (self._settings.u_min - self._settings.u_db_min) - c1 = self.q_lim_pu * self._settings.u_db_min / (self._settings.u_db_min - self._settings.u_min) - q_calc = u_in * m1 + c1 - elif u_in <= self._settings.u_db_max and u_in > self._settings.u_db_min: - q_calc = 0 - elif u_in <= self._settings.u_max and u_in > self._settings.u_db_max: - m2 = self.q_lim_pu / (self._settings.u_db_max - self._settings.u_max) - c2 = self.q_lim_pu * self._settings.u_db_max / (self._settings.u_max - self._settings.u_db_max) - q_calc = u_in * m2 + c2 - elif u_in >= self._settings.u_max: - q_calc = -self.q_lim_pu - - q_calc = q_pv + (q_calc - q_pv) * 0.5 / self._damp_coef + (q_pv - self.old_q_pv) * 0.1 / self._damp_coef - - if p_calc > 0: - if self._controlled_element.NumPhases == 2: - self._controlled_element.SetParameter('kvar', q_calc * self._s_rated * 1.3905768334328491495461135972974) - else: - self._controlled_element.SetParameter('kvar', q_calc * self._s_rated) - else: - pass - - error = abs(q_pv- self.old_q_pv) - self.old_q_pv = q_pv - - return error diff --git a/src/pydss/pyControllers/Controllers/PvDynamic.py b/src/pydss/pyControllers/Controllers/PvDynamic.py deleted file mode 100644 index c39c98a7..00000000 --- a/src/pydss/pyControllers/Controllers/PvDynamic.py +++ /dev/null @@ -1,302 +0,0 @@ -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract - -try: - from pvder.simulation_utilities import SimulationResults - from pvder.dynamic_simulation import DynamicSimulation - from pvder.simulation_events import SimulationEvents - from pvder.grid_components import Grid - from pvder.DER_wrapper import DERModel -except ImportError: - raise ImportError(""" - This controller requires installation of the PVDER module. - Use 'pip install pvder' to install the module and try running the simulation again. - """ - ) -#from pvder import utility_functions - -import numpy as np -import math - -class PvDynamic(ControllerAbstract): - - DEBUG_SIMULATION = False - DEBUG_VOLTAGES = False - DEBUG_CURRENTS = False - DEBUG_POWER = False - DEBUG_CONTROLLERS = True - DEBUG_PLL = False - DEBUG_SOLVER = True - - def __init__(self, VSCObj, Settings, dssInstance, ElmObjectList, dssSolver): - super(PvDynamic, self).__init__(VSCObj, Settings, dssInstance, ElmObjectList, dssSolver) - self.solver = dssSolver - self.p_old = 0 - self.time = 0 - self.dt = dssSolver.GetStepResolutionSeconds() - self._settings = Settings - self._controlled_element = VSCObj - self._e_class, self._e_name = self._controlled_element.GetInfo() - self._name = 'pyCont_' + self._e_class + '_' + self._e_name - self.n_phases = self._controlled_element.NumPhases - self.freq = dssInstance.Solution.Frequency() - - self.events1 = SimulationEvents(verbosity = 'DEBUG') - #self.grid1 = Grid(events=self.events1, unbalance_ratio_b=1.0, unbalance_ratio_c=1.0) - self.voltage_base = Grid.Vbase#self._controlled_element.sBus[0].GetVariable('kVBase') * 1000 - stand_alone = False - steady_state = Settings["STEADY_STATE"] - rated_power_ac_va = Settings["RATED_POWER_AC_VA"] - rated_power_dc_watts = Settings["RATED_POWER_DC_WATTS"] - der_verbosity = 'DEBUG' - config_file = r"C:\Users\alatif\Documents\GitHub\pvder\config_der.json" - if self.n_phases == 1: - self._Va = self.Voltages() - self._Vrms = abs(self._Va )/math.sqrt(2) - #print(f"\nmodel inputs: \nVa - {self._Va}, \nVrms - {self._Vrms}\nFreq - {self.freq}\nvBase - {self.voltage_base}\n") - self._pv_model = DERModel( - modelType= 'SinglePhase', - powerRating = rated_power_dc_watts, - Sinverter_rated = rated_power_ac_va, - events=self.events1, - configFile=config_file, - Vrmsrated = self._Vrms, - gridVoltagePhaseA = self._Va, - gridFrequency=2 * math.pi * self.freq, - derId=Settings["DER_ID"], - standAlone = stand_alone, - steadyStateInitialization=steady_state, - verbosity = der_verbosity - ) - - elif self.n_phases == 3: - self._Va, self._Vb, self._Vc = self.Voltages() - self._Vrms = abs(self._Va ) / math.sqrt(2) - self._pv_model = DERModel( - modelType= 'ThreePhaseUnbalanced', - powerRating = 250000, - Sinverter_rated = 250000, - events=self.events1, - configFile=config_file, - Vrmsrated = self._Vrms, - gridVoltagePhaseA = self._Va, - gridVoltagePhaseB = self._Vb, - gridVoltagePhaseC = self._Vc, - gridFrequency=2 * math.pi * self.freq, - derId=Settings["DER_ID"], - standAlone = stand_alone, - steadyStateInitialization=steady_state, - verbosity = der_verbosity - ) - - self.sim1 = DynamicSimulation( - PV_model=self._pv_model.DER_model, - events = self.events1, - verbosity = 'INFO', - solverType='odeint', - LOOP_MODE=True - ) - - self.results1 = SimulationResults( - simulation = self.sim1, - PER_UNIT=True, - verbosity = 'INFO' - ) - - # self._pv_model.show_parameter_dictionaries() - # self._pv_model.show_parameter_types() - - self.update_model_parameters() - - self._pv_model.DER_model.MPPT_ENABLE = Settings["MPPT_ENABLE"] - self._pv_model.DER_model.RAMP_ENABLE = Settings["RAMP_ENABLE"] - self._pv_model.DER_model.VOLT_VAR_ENABLE = Settings["VOLT_VAR_ENABLE"] - self._pv_model.DER_model.LVRT_ENABLE = Settings["LVRT_ENABLE"] - self._pv_model.DER_model.HVRT_ENABLE = Settings["HVRT_ENABLE"] - self._pv_model.DER_model.LFRT_ENABLE = Settings["LFRT_ENABLE"] - self._pv_model.DER_model.DO_EXTRA_CALCULATIONS = Settings["DO_EXTRA_CALCULATIONS"] - self._pv_model.DER_model.use_frequency_estimate=Settings["use_frequency_estimate"] - self.sim1.jacFlag = Settings["jacFlag"] - self.sim1.DEBUG_SIMULATION = self.DEBUG_SIMULATION - self.sim1.DEBUG_VOLTAGES = self.DEBUG_VOLTAGES - self.sim1.DEBUG_CURRENTS = self.DEBUG_CURRENTS - self.sim1.DEBUG_POWER = self.DEBUG_POWER - self.sim1.DEBUG_CONTROLLERS = self.DEBUG_CONTROLLERS - self.sim1.DEBUG_PLL = self.DEBUG_PLL - self.sim1.PER_UNIT = True - self.sim1.DEBUG_SOLVER = self.DEBUG_SOLVER - self.sim1.tStop = dssSolver.get_simulation_end_time() - self.sim1.tInc = self.dt - self._pv_model._del_t_frequency_estimate = self.sim1.tInc - - self.results = [] - - return - - def update_model_parameters(self): - make_changes = any([ - self._settings["UPDATE_MODULE_PARAMETRS"], - self._settings["UPDATE_INVERTER_PARAMETRS"], - self._settings["UPDATE_CIRCUIT_PARAMETRS"], - self._settings["UPDATE_CONTROLLER_PARAMETRS"], - self._settings["UPDATE_STEADYSTATE_PARAMETRS"] - ]) - - module_parameters = { - 'Np': self._settings["Np"], - 'Ns': self._settings["Ns"], - 'Vdcmpp0': self._settings["Vdcmpp0"], - 'Vdcmpp_max': self._settings["Vdcmpp_max"], - 'Vdcmpp_min': self._settings["Vdcmpp_min"], - } - inverter_ratings = { - 'Vdcrated': self._settings["Vdcrated"], - 'Ioverload': self._settings["Ioverload"], - 'Vrmsrated': self._settings["Vrmsrated"], - 'Iramp_max_gradient_imag': self._settings["Iramp_max_gradient_imag"], - 'Iramp_max_gradient_real': self._settings["Iramp_max_gradient_real"], - } - circuit_parameters = { - 'Rf_actual': self._settings["Rf_actual"], - 'Lf_actual': self._settings["Lf_actual"], - 'C_actual': self._settings["C_actual"], - 'Z1_actual': self._settings["Z1_actual_real"] +self._settings["Z1_actual_imag"] *1j, - 'R1_actual': self._settings["R1_actual"], - 'X1_actual': self._settings["X1_actual"], - } - controller_gains = { - 'Kp_GCC': self._settings["Kp_GCC"], - 'Ki_GCC': self._settings["Ki_GCC"], - 'Kp_DC': self._settings["Kp_DC"], - 'Ki_DC': self._settings["Ki_DC"], - 'Kp_Q': self._settings["Kp_Q"], - 'Ki_Q': self._settings["Ki_Q"], - 'wp': self._settings["wp"] - } - steadystate_values = { - 'iaI0': self._settings["iaI0"], - 'iaR0': self._settings["iaR0"], - 'maI0': self._settings["maI0"], - 'maR0': self._settings["maR0"], - } - - if make_changes: - self._pv_model.DER_model.initialize_parameter_dict( - parameter_ID=self.ControlledElement(), - source_parameter_ID=self._settings["DER_ID"] - ) - - if self._settings["UPDATE_MODULE_PARAMETRS"]: - self._pv_model.DER_model.update_parameter_dict( - parameter_ID=self.ControlledElement(), - parameter_type='module_parameters', - parameter_dict= module_parameters - ) - if self._settings["UPDATE_INVERTER_PARAMETRS"]: - self._pv_model.DER_model.update_parameter_dict( - parameter_ID=self.ControlledElement(), - parameter_type='inverter_ratings', - parameter_dict= inverter_ratings - ) - if self._settings["UPDATE_CIRCUIT_PARAMETRS"]: - self._pv_model.DER_model.update_parameter_dict( - parameter_ID=self.ControlledElement(), - parameter_type='circuit_parameters', - parameter_dict= circuit_parameters - ) - if self._settings["UPDATE_CONTROLLER_PARAMETRS"]: - self._pv_model.DER_model.update_parameter_dict( - parameter_ID=self.ControlledElement(), - parameter_type='controller_gains', - parameter_dict= controller_gains - ) - if self._settings["UPDATE_STEADYSTATE_PARAMETRS"]: - self._pv_model.DER_model.update_parameter_dict( - parameter_ID=self.ControlledElement(), - parameter_type='steadystate_values', - parameter_dict= steadystate_values - ) - self._pv_model.DER_model.modify_DER_parameters(parameter_ID=self.ControlledElement()) - params = self._pv_model.DER_model.get_parameter_dictionary(parameter_type='all',parameter_ID=self.ControlledElement()) - return - - def Name(self): - return self.Name - - def ControlledElement(self): - return "{}.{}".format(self._e_class, self._e_name) - - def run_dynamic_model(self): - sim_time_sec = self.solver.GetTotalSeconds() - t_sim = [sim_time_sec,sim_time_sec + self.dt] - if self.n_phases == 1: - self._Va = self.Voltages() - - self.sim1.run_simulation( - gridVoltagePhaseA=self._Va / self.voltage_base , - y0= self.sim1.y0 , - t=t_sim - ) - elif self.n_phases == 3: - self._Va, self._Vb, self._Vc = self.Voltages() - self.sim1.run_simulation( - gridVoltagePhaseA=self._Va / self.voltage_base , - gridVoltagePhaseB=self._Vb / self.voltage_base , - gridVoltagePhaseC=self._Vc / self.voltage_base , - y0=self.sim1.y0, - t=t_sim - ) - - - S_PCC = self._pv_model.DER_model.S_PCC * self._pv_model.DER_model.Sbase / 1000 - #print(f"kW {S_PCC.real}\nkvar {S_PCC.imag}") - self._controlled_element.SetParameter("kw", S_PCC.real ) - self._controlled_element.SetParameter("kvar", S_PCC.imag) - self.results.append({ - "Vdc" : self._pv_model.DER_model.Vdc * self._pv_model.DER_model.Vbase, - "vta" : self._pv_model.DER_model.vta * self._pv_model.DER_model.Vbase, - "Vtrms" : self._pv_model.DER_model.Vtrms * self._pv_model.DER_model.Vbase, - "Vrms" : self._pv_model.DER_model.Vrms * self._pv_model.DER_model.Vbase, - "Irms" : self._pv_model.DER_model.Irms * self._pv_model.DER_model.Ibase, - "Ppv" : self._pv_model.DER_model.Ppv * self._pv_model.DER_model.Sbase /1000, - "S" : self._pv_model.DER_model.S * self._pv_model.DER_model.Sbase, - "S_PCC" : self._pv_model.DER_model.S_PCC * self._pv_model.DER_model.Sbase, - "active_power [kVA]": np.real(self._pv_model.DER_model.S_PCC * self._pv_model.DER_model.Sbase) / 1000.0, - "reactive_power [kVA]": np.imag(self._pv_model.DER_model.S_PCC * self._pv_model.DER_model.Sbase) / 1000.0, - "grid_voltage [p.u.]": self._Va / self.voltage_base, - }) - - def Update(self, Priority, Time, UpdateResults): - if Priority == 0 : - self.run_dynamic_model() - # if self.solver.is_last_timestep: - # import matplotlib.pyplot as plt - # import pandas as pd - - # fig, axs = plt.subplots(3, 2) - - # self.results = pd.DataFrame(self.results, index=self.solver.all_timestamps) - # print(self.results) - # self.results[["Ppv", "reactive_power [kVA]", "active_power [kVA]"]].plot(ax=axs[0, 0]) - # self.results[["Vdc", "Vrms", "vta", "Vtrms"]].plot(ax=axs[0, 1]) - # self.results[["Irms"]].plot(ax=axs[1, 0]) - # self.results[["S", "S_PCC"]].plot(ax=axs[1, 1]) - # self.results[["grid_voltage [p.u.]"]].plot(ax=axs[2, 0]) - # self.results.plot.scatter(x='grid_voltage [p.u.]', y='reactive_power [kVA]', c='DarkBlue', ax=axs[2, 1]) - - # plt.show() - # print(self.results) - return 0 - - def debugInfo(self): - return [self._Settings['Control{}'.format(i+1)] for i in range(3)] - - def Voltages(self): - self._V0 = self._controlled_element.GetVariable('Voltages', convert=False)[: 2 * self.n_phases] - self._V0 = np.array(self._V0) - self._V0 = self._V0[0::2] + 1j * self._V0[1::2] - if self.n_phases == 1: - return self._V0[0] - elif self.n_phases == 3: - return self._V0[0], self._V0[1], self._V0[2] - else: - raise Exception("Only single or three phase models can be used") \ No newline at end of file diff --git a/src/pydss/pyControllers/Controllers/PvFrequencyRideThru.py b/src/pydss/pyControllers/Controllers/PvFrequencyRideThru.py deleted file mode 100644 index 7ad3a866..00000000 --- a/src/pydss/pyControllers/Controllers/PvFrequencyRideThru.py +++ /dev/null @@ -1,462 +0,0 @@ -from ipaddress import v4_int_to_packed -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -from shapely.geometry import MultiPoint, Polygon, Point, MultiPolygon -from shapely.ops import triangulate, cascaded_union -import matplotlib.pyplot as plt -import datetime -import math -import os -import pdb - -class PvFrequencyRideThru(ControllerAbstract): - """Implementation of IEEE1547-2003 and IEEE1547-2018 frequency ride-through standards using the OpenDSS Generator model. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param PvObj: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'Generator' element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the PvController. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit objects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` - :raises: AssertionError if 'PvObj' is not a wrapped OpenDSS Generator element - - """ - - def __init__(self, PvObj, Settings, dssInstance, ElmObjectList, dssSolver): - super(PvFrequencyRideThru, self).__init__(PvObj, Settings, dssInstance, ElmObjectList, dssSolver) - - self.TimeChange = False - self.Time = (-1, 0) - - self.oldPcalc = 0 - self.oldQcalc = 0 - self.Qpvpu = 0 - self. __vDisconnected = False - self.__pDisconnected = False - - self._ElmObjectList = ElmObjectList - self.ControlDict = { - 'None': lambda: 0, - } - - self._ControlledElm = PvObj - self.__ElmObjectList = ElmObjectList - self.__dssInstance = dssInstance - self.__dssSolver = dssSolver - self.__Settings = Settings - - self.Class, self.Name = self._ControlledElm.GetInfo() - assert (self.Class.lower() == 'generator'), 'PvControllerGen works only with an OpenDSS Generator element' - self.__Name = 'pyCont_' + self.Class + '_' + self.Name - if '_' in self.Name: - self.Phase = self.Name.split('_')[1] - else: - self.Phase = None - - # Initializing the model - PvObj.SetParameter('kvar', 0) - self.__Srated = float(PvObj.SetParameter('kva', Settings['kVA'])) - self.__Prated = float(PvObj.SetParameter('kW', Settings['maxKW'])) - self.__minQ = float(PvObj.SetParameter('minkvar', -Settings['KvarLimit'])) - self.__maxQ = float(PvObj.SetParameter('maxkvar', Settings['KvarLimit'])) - - # MISC settings - self.__cutin = Settings['%PCutin'] - self.__cutout = Settings['%PCutout'] - self.__trip_deadtime_sec = Settings['Reconnect deadtime - sec'] - self.__Time_to_Pmax_sec = Settings['Reconnect Pmax time - sec'] - self.__Prated = Settings['maxKW'] - self.__priority = Settings['Priority'] - self.__enablePFlimit = Settings['Enable PF limit'] - self.__minPF = Settings['pfMin'] - self.__UcalcMode = Settings['UcalcMode'] - # initialize deadtimes and other variables - self.__initializeRideThroughSettings() - # self.__rVs, self.__rTs = self.__CreateOperationRegions() - self.__CreateOperationRegions() - # For debugging only - self.useAvgFrequency = True - cycleAvg = 5 #same for voltage and frequency. #see Table 3 of 1547-2018. Measurement window for frequency is 5 cycles - freq = dssSolver.getFrequency() - step = dssSolver.GetStepSizeSec() - hist_size = math.ceil(cycleAvg / (step * freq)) - self.frequency = [60.0 for i in range(hist_size)] - self.reactive_power = [0.0 for i in range(hist_size)] - self.__VoltVioM = False - self.__VoltVioP = False - - self.region = [3, 3, 3] - - self.frequency_hist = [] - self.power_hist = [] - self.timer_hist = [] - self.timer_act_hist = [] - - - self.u_ang = self._ControlledElm.GetVariable('VoltagesMagAng')[1::2][0] - self.df = 0 - self.freq_hist = [] - return - - def Name(self): - return self.__Name - - def ControlledElement(self): - return "{}.{}".format(self.Class, self.Name) - - def debugInfo(self): - return [self.__Settings['Control{}'.format(i+1)] for i in range(3)] - - def __initializeRideThroughSettings(self): - self.f_temp = 58.5 - self.__isConnected = True - self.__Plimit = self.__Prated - self.__CutoffTime = self.__dssSolver.GetDateTime() - self.__ReconnStartTime = self.__dssSolver.GetDateTime() - datetime.timedelta( - seconds=int(self.__Time_to_Pmax_sec)) - - self.__TrippedPmaxDelay = 0 - self.__NormOper = True - self.__NormOperStartTime = self.__dssSolver.GetDateTime() - self.__fViolationtime = 99999 - self.__TrippedStartTime = self.__dssSolver.GetDateTime() - self.__TrippedDeadtime = 0 - self.__faultCounter = 0 - self.__isinContinuousRegion = True - self.__FaultwindowClearingStartTime = self.__dssSolver.GetDateTime() - self.__continuous_f_upper = 61.2 - self.__continuous_f_lower = 58.8 - - return - - def __CreateOperationRegions(self): - fMaxTheo = 100 - tMax = 1e10 - # #deprecated... - # V = [1.10, 0.88, 0.5, 1.2, 1.2, 1.2, 0.0, 0.0] - # T = [21, 10, 13, 13, 13, 1, 1] - - #...deprecated - - #define the over frequency shall-trip region - OFtripPoints = [ - Point(fMaxTheo, self.__Settings['OF2 CT - sec']), - Point(self.__Settings['OF2 - Hz'], self.__Settings['OF2 CT - sec']), - Point(self.__Settings['OF2 - Hz'], self.__Settings['OF1 CT - sec']), - Point(self.__Settings['OF1 - Hz'], self.__Settings['OF1 CT - sec']), - Point(self.__Settings['OF1 - Hz'], tMax), - Point(fMaxTheo, tMax) - ] - OFtripRegion = Polygon([[p.y, p.x] for p in OFtripPoints]) - - #define the under frequency shall-trip region - UFtripPoints = [ - Point(0, self.__Settings['UF2 CT - sec']), - Point(self.__Settings['UF2 - Hz'], self.__Settings['UF2 CT - sec']), - Point(self.__Settings['UF2 - Hz'], self.__Settings['UF1 CT - sec']), - Point(self.__Settings['UF1 - Hz'], self.__Settings['UF1 CT - sec']), - Point(self.__Settings['UF1 - Hz'], tMax), - Point(0, tMax) - ] - UFtripRegion = Polygon([[p.y, p.x] for p in UFtripPoints]) - - if self.__Settings['Ride-through Category'] in ['Category I', 'Category II', 'Category III']: - - #check over frequency points - if self.__Settings['OF2 - Hz'] < 61.8 or self.__Settings['OF2 - Hz'] > 66: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.__Settings['OF2 CT - sec'] < 0.16 or self.__Settings['OF2 CT - sec'] > 1000: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.__Settings['OF1 - Hz'] < 61.0 or self.__Settings['OF1 - Hz'] > 66.0: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.__Settings['OF1 CT - sec'] <180 or self.__Settings['OF1 CT - sec'] > 1000: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - #check under frequency points - if self.__Settings['UF2 - Hz'] > 57.0 or self.__Settings['UF2 - Hz'] < 50: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.__Settings['UF2 CT - sec'] < 0.16 or self.__Settings['UF2 CT - sec'] > 1000: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.__Settings['UF1 - Hz'] < 50 or self.__Settings['UF1 - Hz'] > 59.0: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.__Settings['UF1 CT - sec'] < 180 or self.__Settings['UF1 CT - sec'] > 1000: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - - # F = [1.10, 0.88, 0.7, 1.20, 1.175, 1.15, 0.50, 0.5] - # T = [1.5, 0.7, 0.2, 0.50, 1.000, 0.16, 0.16] - self.__faultCounterMax = 2 - self.__faultCounterClearingTimeSec = 20 - - else: - assert False - - self._ControlledElm.SetParameter('Model', '7') - # self._ControlledElm.SetParameter('Vmaxpu', V[0]) - # self._ControlledElm.SetParameter('Vminpu', V[1]) - - - ContinuousPoints = [Point(self.__continuous_f_upper, 0), Point(self.__continuous_f_upper, tMax), Point(self.__continuous_f_lower, tMax), Point(self.__continuous_f_lower, 0)] - ContinuousRegion = Polygon([[p.y, p.x] for p in ContinuousPoints]) - - MandatoryPoints1 = [Point(61.8, 0), Point(61.8, 299), Point(self.__continuous_f_upper, 299), Point(self.__continuous_f_upper, 0)] - MandatoryRegion1 = Polygon([[p.y, p.x] for p in MandatoryPoints1]) - - MandatoryPoints2 = [Point(self.__continuous_f_lower, 0), Point(self.__continuous_f_lower, 299), Point(57.0, 299), Point(57.0, 0)] - MandatoryRegion2 = Polygon([[p.y, p.x] for p in MandatoryPoints2]) - - # PermissiveOVPoints = [Point(V[3], 0), Point(V[3], T[2]), Point(V[4], T[2]), Point(V[4], T[3]), - # Point(V[5], T[3]), - # Point(V[5], T[4]), Point(V[0], T[4]), Point(V[0], 0.0)] - # PermissiveOVRegion = Polygon([[p.y, p.x] for p in PermissiveOVPoints]) - - # PermissiveUVPoints = [Point(V[2], 0), Point(V[2], T[5]), Point(V[6], T[5]), Point(V[6], T[6]), - # Point(V[7], T[6]), Point(V[7], 0)] - # PermissiveUVRegion = Polygon([[p.y, p.x] for p in PermissiveUVPoints]) - - ActiveRegion = MultiPolygon([OFtripRegion, UFtripRegion, ContinuousRegion, - MandatoryRegion1,MandatoryRegion2]) - - - TotalPoints = [Point(fMaxTheo, 0), Point(fMaxTheo, tMax), Point(0, tMax), Point(0, 0)] - TotalRegion = Polygon([[p.y, p.x] for p in TotalPoints]) - intersection = TotalRegion.intersection(ActiveRegion) - MayTripRegion = TotalRegion.difference(intersection) #everything not in the active region is the white "may trip" - - if self.__Settings['May trip operation'] == 'Trip': - self.CurrLimRegion = cascaded_union([MandatoryRegion1, MandatoryRegion2]) #Naming probably not appropriate with frequency variation. - self.TripRegion = cascaded_union([OFtripRegion, UFtripRegion, MayTripRegion]) - elif self.__Settings['May trip operation'] == 'Ride-Through': - self.CurrLimRegion = cascaded_union([MandatoryRegion1, MandatoryRegion2, MayTripRegion]) - self.TripRegion = cascaded_union([OFtripRegion, UFtripRegion]) - else: - assert False - self.ContinuousRegion = ContinuousRegion - - - return - - def calculate_frequency(self, priority, time): - vsrc = self._ElmObjectList["Vsource.source"] - u_ang = vsrc.GetParameter("angle") - u_ang = u_ang * math.pi / 180 - - if priority == 2: - if time <= 1: - bus_freq = self.__dssSolver.getFrequency() - self.u_ang = u_ang - else: - h = self.__dssSolver.GetStepSizeSec() - tau = h * 4 - dphi = (u_ang - self.u_ang) / (2 * math.pi * h) - self.df = (dphi + self.df * (tau / h) ) / ( 1 + tau / h ) - base_freq = self.__dssSolver.getFrequency() - bus_freq = base_freq + self.df - self.u_ang = u_ang - - self.freq_hist.append(bus_freq) - return bus_freq - - - def Update(self, Priority, Time, Update): - Error = 0 - self.TimeChange = self.Time != (Priority, Time) - self.Time = Time - - self.freq = self.calculate_frequency(Priority, Time) - - - if Priority == 0: - if self.Time == 0: - self.u_ang = self._ControlledElm.GetVariable('VoltagesMagAng')[1::2][0] - self.__isConnected = self.__Connect() - - if Priority == 2: - - if self.Time == 719: - fig, ax = plt.subplots() - ax.plot(self.freq_hist[:-3]) - plt.show() - - # fIn = self.__UpdateViolatonTimers() - # if self.__Settings["Follow standard"] == "1547-2018": - # self.FrequencyRideThrough(fIn) - # elif self.__Settings["Follow standard"] == "1547-2003": - # self.Trip(fIn) - # else: - # raise Exception("Valid standard setting defined. Options are: 1547-2003, 1547-2018") - - # P = -sum(self._ControlledElm.GetVariable('Powers')[::2]) - # self.power_hist.append(P) - # self.frequency_hist.append(fIn) - # self.timer_hist.append(self.__fViolationtime) - # self.timer_act_hist.append(self.__dssSolver.GetTotalSeconds()) - - # if self.Time == 39 and Priority==2: # Time is the time step, - # import matplotlib.pyplot as plt - # fig, (ax1, ax2) = plt.subplots(2,1) - - # models = [self.CurrLimRegion, self.TripRegion, MultiPolygon([self.ContinuousRegion])] - # models = [i for i in models if i is not None] - - # colors = ["orange", "red", "green"] - # for m, c in zip(models, colors): - # for geom in m.geoms: - # xs, ys = geom.exterior.xy - # ax1.fill(xs, ys, alpha=0.35, fc=c, ec='none') - - - # ax1.set_xlim(0, 2) - # ax1.set_ylim(55, 65) - # # ax1.scatter( self.timer_hist, self.frequency_hist) - # ax1.scatter( self.timer_act_hist, self.frequency_hist) - # ax3 = ax2.twinx() - # ax2.set_ylabel('Power (kW) in green') - # ax3.set_ylabel('Frequency in red') - # ax2.plot(self.timer_act_hist[1:], self.power_hist[1:], c="green") - # ax3.plot(self.timer_act_hist[1:], self.frequency_hist[1:], c="red") - # fig.savefig(f"C:/Users/jkeen/Desktop/{self.__Name}_{self.__Settings['Ride-through Category']}_test.png") - - return Error - - def Trip(self, fIn): - """ Implementation of the IEEE1587-2003 voltage ride-through requirements for inverter systems - """ - if fIn < 59.3 or fIn > 60.5: # see page 19 of 1547-2003. #todo: more parameters are possible. - if self.__isConnected: - self.__Trip(30.0, 0.4, False) #__Trip(self, Deadtime, Time2Pmax, forceTrip, permissive_to_trip=False) - return - - def FrequencyRideThrough(self, fIn): - """ Implementation of the IEEE1587-2018 voltage ride-through requirements for inverter systems - """ - self.__faultCounterClearingTimeSec = 1 - Pm = Point(self.__fViolationtime, fIn) - if Pm.within(self.CurrLimRegion): - region = 0 - isinContinuousRegion = False - - elif Pm.within(self.TripRegion): - region = 2 - isinContinuousRegion = False - if self.region == [3, 1, 1]: #Why? What is this region logic? - self.__Trip(self.__trip_deadtime_sec, self.__Time_to_Pmax_sec, False, True) - else: - self.__Trip(self.__trip_deadtime_sec, self.__Time_to_Pmax_sec, False) - else: - isinContinuousRegion = True - region = 3 - - self.region = self.region[1:] + self.region[:1] - self.region[0] = region - - #if we were not originally in a continous region and we transitioned to a continous region, reset the fault timer counter - if isinContinuousRegion and not self.__isinContinuousRegion: - self.__FaultwindowClearingStartTime = self.__dssSolver.GetDateTime() - #Keep track of time under fault conditions. - clearingTime = (self.__dssSolver.GetDateTime() - self.__FaultwindowClearingStartTime).total_seconds() - - #if we were in a continuous region and transition to fault region - if self.__isinContinuousRegion and not isinContinuousRegion: - if clearingTime <= self.__faultCounterClearingTimeSec: # faultCounterClearingTimeSec is set to 1 in this function. Is this logic right?? - self.__faultCounter += 1 - if self.__faultCounter > self.__faultCounterMax: - if self.__Settings['Multiple disturbances'] == 'Trip': - self.__Trip(self.__trip_deadtime_sec, self.__Time_to_Pmax_sec, True) - self.__faultCounter = 0 - else: - pass - if clearingTime > self.__faultCounterClearingTimeSec and self.__faultCounter > 0: - self.__faultCounter = 0 - self.__isinContinuousRegion = isinContinuousRegion - return - - def __Connect(self): - if not self.__isConnected: - aIn = self._ControlledElm.GetVariable('VoltagesMagAng')[::2] - fIn = self.f_temp #62.5 #should be some function of aIn - if self.useAvgFrequency: - self.frequency = self.frequency[1:] + self.frequency[:1] #WHY?? - self.frequency[0] = fIn - fIn = sum(self.frequency) / len(self.frequency) - deadtime = (self.__dssSolver.GetDateTime() - self.__TrippedStartTime).total_seconds() - if fIn < self.__continuous_f_upper and fIn > self.continuous_f_lower and deadtime >= self.__TrippedDeadtime: - self._ControlledElm.SetParameter('enabled', True) - self.__isConnected = True - self._ControlledElm.SetParameter('kw', 0) - self.__ReconnStartTime = self.__dssSolver.GetDateTime() - else: - conntime = (self.__dssSolver.GetDateTime() - self.__ReconnStartTime).total_seconds() - self.__Plimit = conntime / self.__TrippedPmaxDelay * self.__Prated if conntime < self.__TrippedPmaxDelay \ - else self.__Prated - self._ControlledElm.SetParameter('kw', self.__Plimit) - return self.__isConnected - - def __Trip(self, Deadtime, Time2Pmax, forceTrip, permissive_to_trip=False): - #Why? Is this logic right? They look the same? Change to: "if self.__isConnected or forceTrip or permissive_to_trip" - if self.__isConnected or forceTrip: - - self._ControlledElm.SetParameter('enabled', False) - - self.__isConnected = False - self.__TrippedStartTime = self.__dssSolver.GetDateTime() - self.__TrippedPmaxDelay = Time2Pmax - self.__TrippedDeadtime = Deadtime - - elif permissive_to_trip: - - self._ControlledElm.SetParameter('enabled', False) - - self.__isConnected = False - self.__TrippedStartTime = self.__dssSolver.GetDateTime() - self.__TrippedPmaxDelay = Time2Pmax - self.__TrippedDeadtime = Deadtime - return - - - def __UpdateViolatonTimers(self): - aIn = self._ControlledElm.GetVariable('VoltagesMagAng')[::2] - fIn = self.f_temp #62.5 #should be some function of aIn - - if self.useAvgFrequency: - self.frequency = self.frequency[1:] + self.frequency[:1] #WHY?? I think we're trying to insert new element at first position and remove last - self.frequency[0] = fIn - fIn = sum(self.frequency) / len(self.frequency) - - #track how long we've been operating under normal or abnormal conditions - if fIn < self.__continuous_f_upper and fIn > self.__continuous_f_lower: - if not self.__NormOper: - self.__NormOper = True - self.__NormOperStartTime = self.__dssSolver.GetDateTime() - self.__NormOperTime = 0 - else: - self.__NormOperTime = (self.__dssSolver.GetDateTime() - self.__NormOperStartTime).total_seconds() - self.__FreqVioM = False - # self.__VoltVioP = False - - else: #not in continuous region - if not self.__FreqVioM: - self.__FreqVioM = True - self.__fViolationstartTime = self.__dssSolver.GetDateTime() - self.__fViolationtime = 0 - else: - self.__fViolationtime = (self.__dssSolver.GetDateTime() - self.__fViolationstartTime).total_seconds() - - - return fIn diff --git a/src/pydss/pyControllers/Controllers/PvVoltageRideThru.py b/src/pydss/pyControllers/Controllers/PvVoltageRideThru.py deleted file mode 100644 index ca285a6d..00000000 --- a/src/pydss/pyControllers/Controllers/PvVoltageRideThru.py +++ /dev/null @@ -1,445 +0,0 @@ -from shapely.geometry import MultiPoint, Polygon, Point, MultiPolygon -from shapely.ops import triangulate, unary_union -import datetime -import math - -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -from pydss.pyControllers.models import PvVoltageRideThruModel -from pydss.pyControllers.enumerations import PvStandard, VoltageCalcModes, RideThroughCategory, PermissiveOperation, MayTripOperation, MultipleDisturbances - - -class PvVoltageRideThru(ControllerAbstract): - """Implementation of IEEE1547-2003 and IEEE1547-2018 voltage ride-through standards using the OpenDSS Generator model. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param pv_object: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'Generator' element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param settings: A dictionary that defines the settings for the PvController. - :type settings: dict - :param dss_instance: An :class:`opendssdirect` instance - :type dss_instance: :class:`opendssdirect` - :param elm_object_list: Dictionary of all dssElement, dssBus and dssCircuit objects - :type elm_object_list: dict - :param dss_solver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dss_solver: :mod:`pydss.SolveMode` - :raises: Assertionerror if 'pv_object' is not a wrapped OpenDSS Generator element - - """ - - def __init__(self, pv_object, settings, dss_instance, elm_object_list, dss_solver): - super(PvVoltageRideThru, self).__init__(pv_object, settings, dss_instance, elm_object_list, dss_solver) - - self.model = PvVoltageRideThruModel(**settings) - - self.time_change = False - self.time = (-1, 0) - - self.ControlDict = { - 'None': lambda: 0, - } - - self._controlled_element = pv_object - self.__dss_solver = dss_solver - - self.object_type, self.object_name = self._controlled_element.GetInfo() - assert (self.object_type.lower() == 'generator'), 'PvControllerGen works only with an OpenDSS Generator element' - self._name = 'pyCont_' + self.object_type + '_' + self.object_name - if '_' in self.object_name: - self.phase = self.object_name.split('_')[1] - else: - self.phase = None - - # Initializing the model - #pv_object.SetParameter('kvar', 0) - #pv_object.SetParameter('kva', self.model.kva) - self._p_rated = float(pv_object.GetParameter('kW')) - - # MISC settings - self._trip_deadtime_sec = self.model.reconnect_deadtime_sec - self._time_to_p_max_sec = self.model.reconnect_pmax_time_sec - #self._p_rated = self.model.max_kw - self._voltage_calc_mode = self.model.voltage_calc_mode - # initialize deadtimes and other variables - self._initialize_ride_through_settings() - if self.model.follow_standard == PvStandard.IEEE_1547_2003: - self._rvs = [1.10, 0.88] - else: - self._rvs, _= self._create_operation_regions() - - # For debugging only - self.use_avg_voltage = False - cycle_average = 5 - freq = dss_solver.getFrequency() - step = dss_solver.GetStepSizeSec() - hist_size = math.ceil(cycle_average / (step * freq)) - self.voltage = [1.0 for i in range(hist_size)] - self.reactive_power = [0.0 for i in range(hist_size)] - self._voltage_violation_m = False - - self.region = [3, 3, 3] - - # self.voltage_hist = [] - # self.power_hist = [] - # self.timer_hist = [] - # self.timer_act_hist = [] - - return - - def Name(self): - return self._name - - def ControlledElement(self): - return "{}.{}".format(self.object_type, self.object_name) - - def debugInfo(self): - return [] - - def _initialize_ride_through_settings(self): - self._is_connected = True - self._p_limit = self._p_rated - self._reconnect_start_time = self.__dss_solver.GetDateTime() - datetime.timedelta( - seconds=int(self._time_to_p_max_sec)) - - self._tripped_p_max_delay = 0 - self._normal_operation = True - self._normal_operation_start_time = self.__dss_solver.GetDateTime() - self._u_violation_time = 99999 - self._tripped_start_time = self.__dss_solver.GetDateTime() - self._tripped_dead_time = 0 - self._fault_counter = 0 - self._is_in_contioeous_region = True - self._fault_window_clearing_start_time = self.__dss_solver.GetDateTime() - - return - - def _create_operation_regions(self): - u_max_theo = 10 - t_max = 1e10 - - ov_trip_points = [ - Point(u_max_theo, self.model.ov_2_ct_sec), - Point(self.model.ov_2_pu, self.model.ov_2_ct_sec), - Point(self.model.ov_2_pu, self.model.ov_1_ct_sec), - Point(self.model.ov_1_pu, self.model.ov_1_ct_sec), - Point(self.model.ov_1_pu, t_max), - Point(u_max_theo, t_max) - ] - ov_trip_region = Polygon([[p.y, p.x] for p in ov_trip_points]) - - UVtripPoints = [ - Point(0, self.model.uv_2_ct_sec), - Point(self.model.uv_2_pu, self.model.uv_2_ct_sec), - Point(self.model.uv_2_pu, self.model.uv_1_ct_sec), - Point(self.model.uv_1_pu, self.model.uv_1_ct_sec), - Point(self.model.uv_1_pu, t_max), - Point(0, t_max) - ] - uv_trip_region = Polygon([[p.y, p.x] for p in UVtripPoints]) - - if self.model.ride_through_category == RideThroughCategory.CATEGORY_I: - ov2pu_eq = 1.20 - ov2sec_eq = 0.16 - ov1pu_min = 1.1 - ov1pu_max = 1.2 - ov1sec_min = 1 - ov1sec_max = 13 - uv2pu_min = 0.0 - uv2pu_max = 0.5 - uv2sec_min = 0.16 - uv2sec_max = 2.0 - uv1pu_min = 0.0 - uv1pu_max = 0.88 - uv1sec_min = 2.0 - uv1sec_max = 21 - - V = [1.10, 0.88, 0.7, 1.20, 1.175, 1.15, 0.5, 0.5] - T = [1.5, 0.7, 0.2, 0.5, 1, 0.16, 0.16] - self._fault_counter_max = 2 - self._fault_counter_clearing_time_sec = 20 - - elif self.model.ride_through_category == RideThroughCategory.CATEGORY_II: - ov2pu_eq = 1.20 - ov2sec_eq = 0.16 - ov1pu_min = 1.1 - ov1pu_max = 1.2 - ov1sec_min = 1 - ov1sec_max = 13 - uv2pu_min = 0.0 - uv2pu_max = 0.45 - uv2sec_min = 0.16 - uv2sec_max = 2.0 - uv1pu_min = 0.0 - uv1pu_max = 0.88 - uv1sec_min = 2.0 - uv1sec_max = 21 - - V = [1.10, 0.88, 0.65, 1.20, 1.175, 1.15, 0.45, 0.30] - T = [5, 3, 0.2, 0.5, 1, 0.32, 0.16] - self._fault_counter_max = 2 - self._fault_counter_clearing_time_sec = 10 - - elif self.model.ride_through_category == RideThroughCategory.CATEGORY_I: - ov2pu_eq = 1.20 - ov2sec_eq = 0.16 - ov1pu_min = 1.1 - ov1pu_max = 1.2 - ov1sec_min = 1 - ov1sec_max = 13 - uv2pu_min = 0.0 - uv2pu_max = 0.5 - uv2sec_min = 2.0 - uv2sec_max = 21.0 - uv1pu_min = 0.0 - uv1pu_max = 0.88 - uv1sec_min = 2.0 - uv1sec_max = 21.0 - - V = [1.10, 0.88, 0.5, 1.2, 1.2, 1.2, 0.0, 0.0] - T = [21, 10, 13, 13, 13, 1, 1] - self._fault_counter_max = 3 - self._fault_counter_clearing_time_sec = 5 - - #check overvoltage points - if self.model.ov_2_pu != ov2pu_eq: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - - if self.model.ov_2_ct_sec != ov2sec_eq: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.model.ov_1_pu < ov1pu_min and self.model.ov_1_pu > ov1pu_max: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.model.ov_1_ct_sec < ov1sec_min and self.model.ov_1_ct_sec > ov1sec_max: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - #check undervoltage points - if self.model.uv_2_pu < uv2pu_min and self.model.uv_2_pu > uv2pu_max: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.model.uv_2_ct_sec < uv2sec_min and self.model.uv_2_ct_sec > uv2sec_max: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.model.uv_1_pu < uv1pu_min and self.model.uv_1_pu > uv1pu_max: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - if self.model.uv_1_ct_sec uv1sec_max: - #print("User defined setting outside of IEEE 1547 acceptable range.") - assert False - - self._controlled_element.SetParameter('Model', '7') - self._controlled_element.SetParameter('Vmaxpu', V[0]) - self._controlled_element.SetParameter('Vminpu', V[1]) - - contineous_points = [Point(V[0], 0), Point(V[0], t_max), Point(V[1], t_max), Point(V[1], 0)] - contineous_region = Polygon([[p.y, p.x] for p in contineous_points]) - - mandatory_points = [Point(V[1], 0), Point(V[1], T[0]), Point(V[2], T[1]), Point(V[2], 0)] - mandatory_region = Polygon([[p.y, p.x] for p in mandatory_points]) - - permissive_ov_points = [Point(V[3], 0), Point(V[3], T[2]), Point(V[4], T[2]), Point(V[4], T[3]), - Point(V[5], T[3]), - Point(V[5], T[4]), Point(V[0], T[4]), Point(V[0], 0.0)] - permissive_ov_region = Polygon([[p.y, p.x] for p in permissive_ov_points]) - - permissive_uv_points = [Point(V[2], 0), Point(V[2], T[5]), Point(V[6], T[5]), Point(V[6], T[6]), - Point(V[7], T[6]), Point(V[7], 0)] - permissive_uv_region = Polygon([[p.y, p.x] for p in permissive_uv_points]) - - active_region = MultiPolygon([ov_trip_region, uv_trip_region, contineous_region, mandatory_region, - permissive_ov_region, permissive_uv_region]) - - total_points = [Point(u_max_theo, 0), Point(u_max_theo, t_max), Point(0, t_max), Point(0, 0)] - total_region = Polygon([[p.y, p.x] for p in total_points]) - intersection = total_region.intersection(active_region) - may_trip_region = total_region.difference(intersection) - - if self.model.ride_through_category in [RideThroughCategory.CATEGORY_I, RideThroughCategory.CATEGORY_II]: - if self.model.permissive_operation == PermissiveOperation.CURRENT_LIMITED: - if self.model.may_trip_operation == MayTripOperation.PERMISSIVE_OPERATION: - self.curr_lim_region = unary_union( - [permissive_ov_region, permissive_uv_region, mandatory_region, may_trip_region]) - self.momentary_sucession_region = None - self.trip_region = unary_union([ov_trip_region, uv_trip_region]) - else: - self.curr_lim_region = unary_union([permissive_ov_region, permissive_uv_region, mandatory_region]) - self.momentary_sucession_region = None - self.trip_region = unary_union([ov_trip_region, uv_trip_region, may_trip_region]) - else: - if self.model.may_trip_operation == MayTripOperation.PERMISSIVE_OPERATION: - self.curr_lim_region = mandatory_region - self.momentary_sucession_region = unary_union([permissive_ov_region, permissive_uv_region, may_trip_region]) - self.trip_region = unary_union([ov_trip_region, uv_trip_region]) - else: - self.curr_lim_region = mandatory_region - self.momentary_sucession_region = unary_union([permissive_ov_region, permissive_uv_region]) - self.trip_region = unary_union([ov_trip_region, uv_trip_region, may_trip_region]) - else: - if self.model.may_trip_operation == MayTripOperation.PERMISSIVE_OPERATION: - self.curr_lim_region = mandatory_region - self.momentary_sucession_region = unary_union([permissive_ov_region, permissive_uv_region, may_trip_region]) - self.trip_region = unary_union([ov_trip_region, uv_trip_region]) - else: - self.curr_lim_region = mandatory_region - self.momentary_sucession_region = unary_union([permissive_ov_region, permissive_uv_region]) - self.trip_region = unary_union([ov_trip_region, uv_trip_region, may_trip_region]) - self.normal_region = contineous_region - - - - return V, T - - def Update(self, priority, time, update_results): - - error = 0 - self.time_change = self.time != (priority, time) - self.time = time - if priority == 0: - self._is_connected = self._connect() - - if priority == 2: - u_in = self._update_violaton_timers() - if self.model.follow_standard == PvStandard.IEEE_1547_2018: - self.voltage_ride_through(u_in) - elif self.model.follow_standard == PvStandard.IEEE_1547_2003: - self.trip(u_in) - else: - raise Exception("Valid standard setting defined. Options are: 1547-2003, 1547-2018") - - P = -sum(self._controlled_element.GetVariable('Powers')[::2]) - # self.power_hist.append(P) - # self.voltage_hist.append(u_in) - # self.timer_hist.append(self._u_violation_time) - # self.timer_act_hist.append(self.__dss_solver.GetTotalSeconds()) - - return error - - def trip(self, u_in): - """ Implementation of the IEEE1587-2003 voltage ride-through requirements for inverter systems - """ - if u_in < 0.88: - if self._is_connected: - self._trip(30.0, 0.4, False) - return - - def voltage_ride_through(self, u_in): - """ Implementation of the IEEE1587-2018 voltage ride-through requirements for inverter systems - """ - self._fault_counter_clearing_time_sec = 1 - - Pm = Point(self._u_violation_time, u_in) - if Pm.within(self.curr_lim_region): - region = 0 - is_in_contioeous_region = False - elif self.momentary_sucession_region and Pm.within(self.momentary_sucession_region): - region = 1 - is_in_contioeous_region = False - self._trip(self.__dss_solver.GetStepSizeSec(), 0.4, False) - elif Pm.within(self.trip_region): - region = 2 - is_in_contioeous_region = False - if self.region == [3, 1, 1]: - self._trip(self._trip_deadtime_sec, self._time_to_p_max_sec, False, True) - else: - self._trip(self._trip_deadtime_sec, self._time_to_p_max_sec, False) - else: - is_in_contioeous_region = True - region = 3 - - self.region = self.region[1:] + self.region[:1] - self.region[0] = region - - if is_in_contioeous_region and not self._is_in_contioeous_region: - self._fault_window_clearing_start_time = self.__dss_solver.GetDateTime() - clearing_time = (self.__dss_solver.GetDateTime() - self._fault_window_clearing_start_time).total_seconds() - - if self._is_in_contioeous_region and not is_in_contioeous_region: - if clearing_time <= self._fault_counter_clearing_time_sec: - self._fault_counter += 1 - if self._fault_counter > self._fault_counter_max: - if self.model.multiple_disturdances == MultipleDisturbances.TRIP: - self._trip(self._trip_deadtime_sec, self._time_to_p_max_sec, True) - self._fault_counter = 0 - else: - pass - if clearing_time > self._fault_counter_clearing_time_sec and self._fault_counter > 0: - self._fault_counter = 0 - self._is_in_contioeous_region = is_in_contioeous_region - return - - def _connect(self): - if not self._is_connected: - u_in = self._controlled_element.GetVariable('VoltagesMagAng')[::2] - u_base = self._controlled_element.sBus[0].GetVariable('kVBase') * 1000 - u_in = max(u_in) / u_base if self._voltage_calc_mode == VoltageCalcModes.MAX else sum(u_in) / (u_base * len(u_in)) - if self.use_avg_voltage: - self.voltage = self.voltage[1:] + self.voltage[:1] - self.voltage[0] = u_in - u_in = sum(self.voltage) / len(self.voltage) - deadtime = (self.__dss_solver.GetDateTime() - self._tripped_start_time).total_seconds() - if u_in < self._rvs[0] and u_in > self._rvs[1] and deadtime >= self._tripped_dead_time: - - self._controlled_element.SetParameter('enabled', True) - self._is_connected = True - self._controlled_element.SetParameter('kw', 0) - self._reconnect_start_time = self.__dss_solver.GetDateTime() - else: - conntime = (self.__dss_solver.GetDateTime() - self._reconnect_start_time).total_seconds() - self._p_limit = conntime / self._tripped_p_max_delay * self._p_rated if conntime < self._tripped_p_max_delay \ - else self._p_rated - self._controlled_element.SetParameter('kw', self._p_limit) - return self._is_connected - - def _trip(self, Deadtime, time2Pmax, forceTrip, permissive_to_trip=False): - - u_in = self._controlled_element.GetVariable('VoltagesMagAng')[::2] - u_base = self._controlled_element.sBus[0].GetVariable('kVBase') * 1000 - u_in = max(u_in) / u_base if self._voltage_calc_mode == VoltageCalcModes.MAX else sum(u_in) / (u_base * len(u_in)) - - if self._is_connected or forceTrip: - self._controlled_element.SetParameter('kw', 0) - - self._is_connected = False - self._tripped_start_time = self.__dss_solver.GetDateTime() - self._tripped_p_max_delay = time2Pmax - self._tripped_dead_time = Deadtime - - elif permissive_to_trip: - self._controlled_element.SetParameter('kw', 0) - - self._is_connected = False - self._tripped_start_time = self.__dss_solver.GetDateTime() - self._tripped_p_max_delay = time2Pmax - self._tripped_dead_time = Deadtime - return - - def _update_violaton_timers(self): - u_in = self._controlled_element.GetVariable('VoltagesMagAng')[::2] - u_base = self._controlled_element.sBus[0].GetVariable('kVBase') * 1000 - u_in = max(u_in) / u_base if self._voltage_calc_mode == VoltageCalcModes.MAX else sum(u_in) / (u_base * len(u_in)) - if self.use_avg_voltage: - self.voltage = self.voltage[1:] + self.voltage[:1] - self.voltage[0] = u_in - u_in = sum(self.voltage) / len(self.voltage) - - if u_in < self._rvs[0] and u_in > self._rvs[1]: - if not self._normal_operation: - self._normal_operation = True - self._normal_operation_start_time = self.__dss_solver.GetDateTime() - self._normal_operation_time = 0 - else: - self._normal_operation_time = (self.__dss_solver.GetDateTime() - self._normal_operation_start_time).total_seconds() - self._voltage_violation_m = False - else: - if not self._voltage_violation_m: - self._voltage_violation_m = True - self.__uViolationstarttime = self.__dss_solver.GetDateTime() - self._u_violation_time = 0 - else: - self._u_violation_time = (self.__dss_solver.GetDateTime() - self.__uViolationstarttime).total_seconds() - return u_in diff --git a/src/pydss/pyControllers/Controllers/Settings/__init__.py b/src/pydss/pyControllers/Controllers/Settings/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pydss/pyControllers/Controllers/SocketController.py b/src/pydss/pyControllers/Controllers/SocketController.py deleted file mode 100644 index ba07a97a..00000000 --- a/src/pydss/pyControllers/Controllers/SocketController.py +++ /dev/null @@ -1,77 +0,0 @@ -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -import socket -import struct - -class SocketController(ControllerAbstract): - """Allows pydss object to inteface with external software using socket communication. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param ElmObject: A :class:`pydss.dssElement.dssElement` object that wraps around any OpenDSS element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the PvController. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit objects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` - :raises: socket.error if the connection fails - - """ - - def __init__(self, ElmObject, Settings, dssInstance, ElmObjectList,dssSolver): - super(SocketController, self).__init__(ElmObject, Settings, dssInstance, ElmObjectList, dssSolver) - self.Time = -1 - self.__ControlledElm = ElmObject - self.Class, self.Name = self.__ControlledElm.GetInfo() - self.__Name = 'pyCont_' + self.Class + '_' + self.Name - #assert (Class.lower() == 'regulator'), 'PvControllerGen works only with an OpenDSS Regulator element' - self.IP = Settings['IP'] - self.Port = Settings['Port'] - self.Encoding = Settings['Encoding'] - self.BufferSize = Settings['Buffer'] - self.Index = Settings['Index'].split(',') if ',' in Settings['Index'] else [Settings['Index']] - self.Inputs = Settings['Inputs'].split(',') if ',' in Settings['Inputs'] else [Settings['Inputs']] - self.Outputs = Settings['Outputs'].split(',') if ',' in Settings['Outputs'] else [Settings['Outputs']] - self.Socket = self.__CreateClient() - return - - def __CreateClient(self): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Create a socket object - s.connect((self.IP, self.Port)) - return s - - def Update(self, Priority, Time, UpdateResults): - - # self.TimeChange = self.Time != (Priority, Time) - # self.Time = (Priority, Time) - self.TimeChange = self.Time != Time - self.Time = Time - - if self.TimeChange and Priority==0: - Values = [] - for Variable in self.Inputs: - Val = self.__ControlledElm.GetValue(Variable) - if isinstance(Val, list): - Values.extend(Val) - else: - Values.extend([Val]) - self.Socket.sendall(struct.pack('%sd' % len(Values), *Values)) - if self.TimeChange and Priority == 1: - Data = self.Socket.recv(self.BufferSize) - if Data: - numDoubles = int(len(Data) / 8) - tag = str(numDoubles) + 'd' - Data = list(struct.unpack(tag, Data)) - for i, Variable in enumerate(self.Outputs): - self.__ControlledElm.SetParameter(Variable, Data[0]) - return 0 - - def Name(self): - return self.__Name - - def ControlledElement(self): - return "{}.{}".format(self.Class, self.Name) - - def debugInfo(self): - pass diff --git a/src/pydss/pyControllers/Controllers/StorageController.py b/src/pydss/pyControllers/Controllers/StorageController.py deleted file mode 100644 index baee0c7e..00000000 --- a/src/pydss/pyControllers/Controllers/StorageController.py +++ /dev/null @@ -1,553 +0,0 @@ -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -import calendar -import math -import ast - -class StorageController(ControllerAbstract): - """Numerous control implementation for a storage system from both behind-the-meter and front-of- meter applications. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param StorageObj: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS Storage element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the PvController. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit objects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` - :raises: AssertionError if 'StorageObj' is not a wrapped OpenDSS Storage element - - """ - def __init__(self, StorageObj, Settings, dssInstance, ElmObjectList, dssSolver): - self.Time = -1 - super(StorageController, self).__init__(StorageObj, Settings, dssInstance, ElmObjectList, dssSolver) - - self.__ControlledElm = StorageObj - self.ceClass, self.ceName = self.__ControlledElm.GetInfo() - assert (self.ceClass.lower() == 'storage'), 'StorageController works only with an OpenDSS Storage element' - self.__Name = 'pyCont_' + self.ceClass + '_' + self.ceName - - self.Time = (-1, 0) - self.__Pin = 0 - self.Pbatt = 0 - self.__PinOld = 0 - self.PbattOld = 0 - self.oldQcalc = 0 - self.ExportOld = False - self.__ElmObjectList = ElmObjectList - - self.ControlDict = { - 'None' : lambda: 0, - 'PS' : self.PeakShavingControl, - 'CF' : self.CapacityFirmimgControl, - 'TT' : self.TimeTriggeredControl, - 'RT' : self.RealTimeControl, - 'SH' : self.ScheduledControl, - 'NETT' : self.NonExportTimeTriggered, - 'TOU' : self.TimeOfUse, - 'DemChg' : self.DemandCharge, - 'CPF' : self.ConstantPowerFactorControl, - 'VPF' : self.VariablePowerFactorControl, - 'VVar' : self.VoltVarControl, - } - - self.__a = Settings['alpha'] - self.__b = Settings['beta'] - self.__ElmObjectList = ElmObjectList - self.__ControlledElm = StorageObj - self.__dssInstance = dssInstance - self.__dssSolver = dssSolver - self.__Settings = Settings - - self.__Srated = float(StorageObj.GetParameter('kVA')) - self.__Prated = float(StorageObj.GetParameter('kWrated')) - self.__Pbatt = float(StorageObj.GetParameter('kW')) - self.__dampCoef = Settings['DampCoef'] - self.update = [self.ControlDict[Settings['Control' + str(i)]] for i in [1, 2, 3]] - return - - def Name(self): - return self.__Name - - def ControlledElement(self): - return "{}.{}".format(self.ceClass, self.ceName) - - def debugInfo(self): - return [self.__Settings['Control{}'.format(i+1)] for i in range(3)] - - def Update(self, Priority, Time, Update): - self.TimeChange = self.Time != (Time, Priority) - self.Time = (Time, Priority) - return self.update[Priority]() - - def SetSetting(self, Property, Value): - self.__Settings[Property] = Value - return - - def GetSetting(self, Property): - return self.__Settings[Property] - - def __parseRatePlan(self, tarrif): - self.touTarrif = ast.literal_eval(tarrif) - CurrDateTime = self.__dssSolver.GetDateTime() - DayOfYear = CurrDateTime.timetuple().tm_yday - weekno = CurrDateTime.weekday() - currentDay = calendar.day_name[weekno] - - for period, touDetails in self.touTarrif.items(): - if touDetails['ED'] < touDetails['SD']: - TOUday = [i for j in (range(1, touDetails['ED']), range(touDetails['SD'], 365)) for i in j] - else: - TOUday = range(touDetails['SD'], touDetails['ED'] + 1) - if DayOfYear in TOUday: - if currentDay in touDetails['TOW']: - for st, et in zip(touDetails['ST'], touDetails['ET']): - if et < st: - TOUtime = [i for j in (range(1, et), range(st, 25)) for i in j] - else: - TOUtime = range(st, et) - if CurrDateTime.hour in TOUtime: - return True - else: - pass - return False - - def TimeOfUse(self): - """ Implementation of a time of use controller for behind the meter applications - """ - dP = 0 - Pub = self.__Settings['touLoadLim'] - touCharge = self.__Settings['%touCharge'] - tarrif = self.__Settings['touTarrifStructure'] - isTOU = self.__parseRatePlan(tarrif) - Pbatt = float(self.__ControlledElm.GetParameter('kw')) - if self.__Settings['PowerMeaElem'] == 'Total': - Sin = self.__dssInstance.Circuit.TotalPower() - Pin = -sum(Sin[0:5:2]) - else: - Sin = self.__ElmObjectList[self.__Settings['PowerMeaElem']].GetVariable('Powers') - Pin = sum(Sin[0:5:2]) - - if isTOU: - if Pin > Pub: - dP = Pin - Pub - Pbatt = Pbatt + dP * self.__a - (Pbatt - self.PbattOld) * self.__b - else: - Pbatt = 0 - else: - Pbatt = -touCharge * self.__Prated / 100 - - if Pbatt >= 0: - pctdischarge = Pbatt / (self.__Prated) * 100 - pctdischarge = 100 if pctdischarge > 100 else pctdischarge - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(pctdischarge)) - if Pbatt < 0: - pctcharge = -Pbatt / (self.__Prated) * 100 - pctcharge = 100 if pctcharge > 100 else pctcharge - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(pctcharge)) - - Error = abs(Pbatt - self.PbattOld) - self.PbattOld = Pbatt - self.dPold = dP - return Error - - - def DemandCharge(self): - """ Implementation of a demand charge controller for behind the meter applications - """ - - self.Demand = 0 - dP = 0 - DemandChgThreh = self.__Settings['DemandChgThreh[kWh]'] - Pub = self.__Settings['touLoadLim'] - touCharge = self.__Settings['%touCharge'] - tarrif = self.__Settings['touTarrifStructure'] - isTOU = self.__parseRatePlan(tarrif) - Pbatt = float(self.__ControlledElm.GetParameter('kw')) - if self.__Settings['PowerMeaElem'] == 'Total': - Sin = self.__dssInstance.Circuit.TotalPower() - Pin = -sum(Sin[0:5:2]) - else: - Sin = self.__ElmObjectList[self.__Settings['PowerMeaElem']].GetVariable('Powers') - Pin = sum(Sin[0:5:2]) - DateAndTime = self.__dssSolver.GetDateTime() - CurrMin = DateAndTime.minute - if isTOU: - i = CurrMin % 30 - if i == 0: - self.__EnergyCounter = [0 for i in range(30)] - self.__EnergyCounter[i] = Pin - self.Demand = sum(self.__EnergyCounter) / (60 / self.__dssSolver.GetStepResolutionMinutes()) - - if self.Demand >= 0.9 * DemandChgThreh: - if Pin > Pub: - dP = Pin - Pub - Pbatt = Pbatt + (dP) * self.__a - (Pbatt - self.PbattOld) * self.__b - else: - Pbatt = 0 - else: - Pbatt = 0 - else: - Pbatt = -touCharge * self.__Prated / 100 - - if Pbatt >= 0: - pctdischarge = Pbatt / (self.__Prated) * 100 - pctdischarge = 100 if pctdischarge > 100 else pctdischarge - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(pctdischarge)) - if Pbatt < 0: - pctcharge = -Pbatt / (self.__Prated) * 100 - pctcharge = 100 if pctcharge > 100 else pctcharge - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(pctcharge)) - - Error = abs(Pbatt - self.PbattOld) - self.PbattOld = Pbatt - self.dPold = dP - return Error - - def ScheduledControl(self): - """ Implementation of a fixed schedule controller. Used to implemented predefined dispatch signals - """ - P_profile = self.__Settings['Schedule'] - Days = self.__Settings['Days'] - LenSchedule = len(P_profile) - TotalSeconds = 24*60*60*Days - TimeStepPerSample = int(TotalSeconds / LenSchedule) - - CurrentTime = int(self.__dssInstance.Solution.Hour()) * 60 * 60 + \ - int(self.__dssInstance.Solution.Seconds()) - - Index = int(CurrentTime / TimeStepPerSample) - Pout = P_profile[Index] - if Pout > 0: - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(Pout * 100)) - elif Pout < 0: - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(-Pout * 100)) - else: - self.__ControlledElm.SetParameter('State', 'IDLE') - return 0 - - def NonExportTimeTriggered(self): - """ Implementation of a smart non-export controller. Makes use of TOU window to optimize charging - """ - # self.dPold = 0 - dP = 0 - Plb = self.__Settings['BaseLoadLim'] - sTime = self.__Settings['ExpWindowStart'] - eTime = self.__Settings['ExpWindowEnd'] - KWHrated = float(self.__ControlledElm.GetParameter('kWhrated')) - perIdle = float(self.__ControlledElm.GetParameter('%IdlingkW')) - effDchg = float(self.__ControlledElm.GetParameter('%EffDischarge')) - - Minutes = int(self.__dssInstance.Solution.Seconds() / 60) - Hour = self.__dssInstance.Solution.Hour() % 24 - - if sTime.hour < eTime.hour: - Twindow = ((eTime.hour * 60 + eTime.minute) - (sTime.hour * 60 + sTime.minute)) / 60 - if (Hour * 60 + Minutes) > (sTime.hour * 60 + sTime.minute) and (Hour * 60 + Minutes) < (eTime.hour * 60 + eTime.minute): - Export = True - else: - Export = False - else: - Twindow = 24 - ((sTime.hour * 60 + sTime.minute) - (eTime.hour * 60 + eTime.minute)) / 60 - if (Hour * 60 + Minutes) > (sTime.hour * 60 + sTime.minute) or \ - (Hour * 60 + Minutes) < (eTime.hour * 60 + eTime.minute): - Export = True - else: - Export = False - - if self.ExportOld == False and Export == True: - perKWHstored = float(self.__ControlledElm.GetParameter('%stored')) - kWhrem = KWHrated * (perKWHstored / 100) - self.Pbatt = kWhrem / (Twindow) * effDchg / 100 - perIdle * self.__Prated / 100 - self.ExportOld = Export - - if Export: - pctcharge = self.Pbatt / (self.__Prated) * 100 - rT = ((eTime.hour * 60 + eTime.minute) - (Hour * 60 + Minutes)) / 60 - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(pctcharge)) - Error = 0 - else: - if self.__Settings['PowerMeaElem'] == 'Total': - Sin = self.__dssInstance.Circuit.TotalPower() - Pin = -sum(Sin[0:5:2]) - else: - Sin = self.__ElmObjectList[self.__Settings['PowerMeaElem']].GetVariable('Powers') - Sin2 = Sin[:int(len(Sin) / 2)] - Pin = sum(Sin2[0::2]) - Pbatt = float(self.__ControlledElm.GetParameter('kw')) - if Pin < Plb: - dP = Plb - Pin - Pbatt = Pbatt - (dP) * self.__a - (Pbatt - self.PbattOld) * self.__b - else: - Pbatt = 0 - - if Pbatt >= 0: - pctdischarge = Pbatt / (self.__Prated) * 100 - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(pctdischarge)) - if Pbatt < 0: - pctcharge = -Pbatt / (self.__Prated) * 100 - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(pctcharge)) - - Error = abs(Pbatt - self.PbattOld) - self.PbattOld = Pbatt - self.dPold = dP - return Error - - def PeakShavingControl(self): - """ Implementation of a peak shaving / base loading controller. Setting both peak shaving and base loading - limits to zero will make the storage work in "SELF CONSUMPTION" mode - """ - Pub = self.__Settings['PS_ub'] - Plb = self.__Settings['PS_lb'] - IdlingkWPercent = float(self.__ControlledElm.GetParameter('%IdlingkW')) - IdlingkW = -IdlingkWPercent/100*self.__Prated - if self.__Settings['PowerMeaElem'] == 'Total': - Sin = self.__dssInstance.Circuit.TotalPower() - Pin = -sum(Sin[0:5:2]) - else: - Sin = self.__ElmObjectList[self.__Settings['PowerMeaElem']].GetVariable('Powers') - Pin = sum(Sin[0:int(len(Sin)/2):2]) - - Pbatt = float(self.__ControlledElm.GetParameter('kw')) - - if Pin > Pub: - dP = Pin - Pub - Pbatt = Pbatt + dP * self.__dampCoef - elif Pin < Plb: - dP = Pin - Plb - Pbatt = Pbatt + dP * self.__dampCoef - else: - Pbatt = Pbatt * self.__dampCoef - - if Pbatt >= 0: - pctdischarge = min(100, Pbatt / self.__Prated* 100) - pctdischarge = 100 if pctdischarge > 100 else pctdischarge - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(pctdischarge)) - - elif Pbatt < 0: - pctcharge = min(100, -Pbatt / (self.__Prated)* 100) - pctcharge = 100 if pctcharge > 100 else pctcharge - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(pctcharge)) - - Error = abs(Pbatt - self.PbattOld) / self.__Srated - self.PbattOld = Pbatt - return Error - - def RealTimeControl(self): - kWOut = self.__Settings['%kWOut'] - if kWOut > 0: - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(kWOut)) - elif kWOut < 0: - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(-kWOut)) - else: - self.__ControlledElm.SetParameter('State', 'IDLE') - return 0 - - def TimeTriggeredControl(self): - HrCharge = self.__Settings['HrCharge'] - HrDischarge = self.__Settings['HrDischarge'] - rateCharge = self.__Settings['%rateCharge'] - rateDischarge = self.__Settings['%rateDischarge'] - - HrC = int(HrCharge) - MnC = int((HrCharge - HrC) * 60) - HrD = int(HrDischarge) - MnD = int((HrDischarge - HrD) * 60) - - Minutes = int(self.__dssInstance.Solution.Seconds()/60) - Hour = self.__dssInstance.Solution.Hour() % 24 - if Hour == HrC and Minutes == MnC: - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(rateCharge)) - elif Hour == HrD and Minutes == MnD: - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(rateDischarge)) - return 0 - - def CapacityFirmimgControl(self): - """ Implementation of a capacity firming algorithm - """ - dPub = self.__Settings['CF_dP_ub'] - dPlb = self.__Settings['CF_dP_lb'] - - if self.Time[0] == 0: - self.__PinOld = self.__Pin - - if self.__Settings['PowerMeaElem'] == 'Total': - Sin = self.__dssInstance.Circuit.TotalPower() - Pin = -sum(Sin[0:5:2]) - else: - Sin = self.__ElmObjectList[self.__Settings['PowerMeaElem']].GetVariable('Powers') - Pin = sum(Sin[0:5:2]) - Pbatt = -float(self.__ControlledElm.GetVariable('Powers')[0]) * 3 - ramp = (Pin - self.__PinOld) - if self.Time[0] > 1: - if ramp >= dPub: - dPbatt = self.__dampCoef * (ramp - dPub) - Pbatt += dPbatt - elif ramp <= dPlb: - dPbatt = self.__dampCoef * (ramp - dPlb) - Pbatt += dPbatt - else: - dPbatt = 0 - Pbatt = 0 - if Pbatt > 0: - pctdischarge = Pbatt / self.__Prated * 100 - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(pctdischarge)) - elif Pbatt < 0: - pctcharge = -Pbatt / self.__Prated * 100 - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(pctcharge)) - elif Pbatt == 0: - self.__ControlledElm.SetParameter('State', 'IDLING') - self.__Pin = Pin - else: - self.__PinOld = Pin - self.__Pin = Pin - return 0 - - Error = abs(dPbatt) - return Error - - def ConstantPowerFactorControl(self): - """ Implementation of a constant power factor algorithm. In all cases of reactive power support, active power - will be prioritized over reactive power. - """ - PF = self.__Settings['pf'] - self.__dssSolver.reSolve() - Pcalc = float(self.__ControlledElm.GetParameter('kw')) / self.__Prated - - if Pcalc > 0: - Qcalc = float(self.__ControlledElm.GetParameter('kvar')) / self.__Prated - - Scalc = (Pcalc ** 2 + Qcalc ** 2) ** (0.5) - if Scalc > 1: - Scaler = (1 - (Scalc - 1) / Scalc) - Pcalc = Pcalc * Scaler - if Pcalc > 0: - self.__ControlledElm.SetParameter('%Discharge', Pcalc * 100) - elif Pcalc < 0: - self.__ControlledElm.SetParameter('%charge', -Pcalc * 100) - - self.__ControlledElm.SetParameter('pf', str(-PF)) - else: - self.__ControlledElm.SetParameter('pf', str(1)) - - return 0 - - def VariablePowerFactorControl(self): - """ Implementation of a variable power factor algorithm. In all cases of reactive power support, active power - will be prioritized over reactive power. - """ - - pMin = self.__Settings['Pmin'] - pMax = self.__Settings['Pmax'] - pfMin = self.__Settings['pfMin'] - pfMax = self.__Settings['pfMax'] - - self.__dssSolver.reSolve() - Pcalc = float(self.__ControlledElm.GetParameter('kw')) / self.__Prated - - if Pcalc > 0: - if Pcalc < pMin: - PF = pfMax - elif Pcalc > pMax: - PF = pfMin - else: - m = (pfMax - pfMin) / (pMin - pMax) - c = (pfMin * pMin - pfMax * pMax) / (pMin - pMax) - PF = Pcalc * m + c - else: - PF = pfMax - - Qcalc = (Pcalc / PF) * math.sin(math.acos(PF)) - - Scalc = (Pcalc ** 2 + Qcalc ** 2) ** (0.5) - if Scalc > 1: - Scaler = (1 - (Scalc - 1) / Scalc) - Qcalc = Qcalc * Scaler - PF = math.cos(math.atan(Qcalc/Pcalc)) - - self.__ControlledElm.SetParameter('pf', str(-PF)) - if Pcalc > 0: - self.__ControlledElm.SetParameter('%Discharge', Pcalc*100) - elif Pcalc < 0: - self.__ControlledElm.SetParameter('%charge', -Pcalc*100) - return 0 - - def VoltVarControl(self): - """ Implementation of a Volt / var algorithm. Enables the storage to stack multiple services. In all cases of - reactive power support, active power will be prioritized over reactive power. - """ - uMin = self.__Settings['uMin'] - uMax = self.__Settings['uMax'] - uDbMin = self.__Settings['uDbMin'] - uDbMax = self.__Settings['uDbMax'] - QlimPU = self.__Settings['QlimPU'] - PFlim = self.__Settings['PFlim'] - - uIn = max(self.__ControlledElm.sBus[0].GetVariable('puVmagAngle')[::2]) - - m1 = QlimPU / (uMin-uDbMin) - m2 = QlimPU / (uDbMax-uMax) - c1 = QlimPU * uDbMin / (uDbMin-uMin) - c2 = QlimPU * uDbMax / (uMax-uDbMax) - - Ppv = float(self.__ControlledElm.GetParameter('kw')) - Pcalc = Ppv / self.__Srated - Qpv = sum(self.__ControlledElm.GetVariable('Powers')[1::2]) - Qpv = Qpv / self.__Srated - - Qcalc = 0 - if uIn <= uMin: - Qcalc = QlimPU - elif uIn <= uDbMin and uIn > uMin: - Qcalc = uIn * m1 + c1 - elif uIn <= uDbMax and uIn > uDbMin: - Qcalc = 0 - elif uIn <= uMax and uIn > uDbMax: - Qcalc = uIn * m2 + c2 - elif uIn >= uMax: - Qcalc = -QlimPU - - # adding heavy ball term to improve convergence - Qcalc = Qpv + (Qcalc - Qpv) * 0.5 / self.__dampCoef + (Qpv - self.oldQcalc) * 0.1 / self.__dampCoef - Qlim = (1 - Pcalc ** 2) ** 0.5 if abs(Pcalc) < 1 else 0 # note - this is watt priority - if self.__Settings['Enable PF limit']: - Qlim = min(Qlim, abs(Pcalc * math.tan(math.acos(PFlim)))) - if abs(Qcalc) > Qlim: - Qcalc = Qlim if Qcalc > 0 else -Qlim - - dQ = abs(Qcalc - Qpv) - pct = min((Qcalc**2 + Pcalc**2) ** 0.5 * self.__Srated / self.__Prated * 100, 100) - pf = math.cos(math.atan(Qcalc / Pcalc)) if Pcalc != 0 else 1 - pf = -pf if Qcalc * Pcalc < 0 else pf - if Pcalc > 0: - self.__ControlledElm.SetParameter('pf', pf) - self.__ControlledElm.SetParameter('State', 'DISCHARGING') - self.__ControlledElm.SetParameter('%Discharge', str(pct)) - elif Pcalc < 0: - self.__ControlledElm.SetParameter('pf', pf) - self.__ControlledElm.SetParameter('State', 'CHARGING') - self.__ControlledElm.SetParameter('%charge', str(pct)) - else: - dQ = 0 - - Error = abs(dQ) - self.oldQcalc = Qcalc - return Error diff --git a/src/pydss/pyControllers/Controllers/ThermostaticLoad.py b/src/pydss/pyControllers/Controllers/ThermostaticLoad.py deleted file mode 100644 index a578f32a..00000000 --- a/src/pydss/pyControllers/Controllers/ThermostaticLoad.py +++ /dev/null @@ -1,66 +0,0 @@ -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract -from math import * -import random - -class ThermostaticLoad(ControllerAbstract): - - def __init__(self, LoadObj, Settings, dssInstance, ElmObjectList, dssSolver): - super(ThermostaticLoad, self).__init__(LoadObj, Settings, dssInstance, ElmObjectList, dssSolver) - - self.TimeChange = False - self.Time = (-1, 0) - - self.__ControlledElm = LoadObj - self.dssSolver = dssSolver - - self.eClass, self.eName = self.__ControlledElm.GetInfo() - self.__Name = 'pyCont_' + self.eClass + '_' + self.eName - self.Tmax = Settings["Tmax"] - self.Tmin = Settings["Tmin"] - self.T = random.random() * (self.Tmax - self.Tmin) + self.Tmin - self.On = True if random.random() > 0.5 else False - self.__ControlledElm.SetParameter("kw", Settings["kw"]) - self.Prated = LoadObj.GetParameter("kw") - - - self.a = 1 / (Settings["R"] * Settings["C"]) - self.b = Settings["mu"] / Settings["C"] - self.w = 0 - return - - @property - def Name(self): - return self.__Name - - def debugInfo(self): - return - - @property - def ControlledElement(self): - return "{}.{}".format(self.eClass, self.eName) - - def Update(self, Priority, Time, UpdateResults): - self.TimeChange = self.Time != (Priority, Time) - self.Time = (Priority, Time) - if self.TimeChange: - timePeriods = 24 * 60 * 60 - Tsec = self.dssSolver.GetOpenDSSTime() * 60 * 50 - Ta = sin(Tsec * 2 * pi * (1 / timePeriods)) * 10 + 30 - - if self.On: - self.dT = -self.a * (self.T - Ta) - self.b * self.Prated + self.w - else: - self.dT = -self.a * (self.T - Ta) + self.w - - self.T += self.dT - - if self.T > self.Tmax: - self.On = True - self.__ControlledElm.SetParameter("kw", self.Prated) - elif self.T < self.Tmin: - self.On = False - self.__ControlledElm.SetParameter("kw", 0) - return 0 - - def __del__(self): - self.f.close() diff --git a/src/pydss/pyControllers/Controllers/xfmrController.py b/src/pydss/pyControllers/Controllers/xfmrController.py deleted file mode 100644 index de180491..00000000 --- a/src/pydss/pyControllers/Controllers/xfmrController.py +++ /dev/null @@ -1,63 +0,0 @@ -from pydss.pyControllers.pyControllerAbstract import ControllerAbstract - -class xfmrController(ControllerAbstract): - """The controller locks a regulator in the event of reverse power flow. Subclass of the :class:`pydss.pyControllers.pyControllerAbstract.ControllerAbstract` abstract class. - - :param RegulatorObj: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'Regulator' element - :type FaultObj: class:`pydss.dssElement.dssElement` - :param Settings: A dictionary that defines the settings for the PvController. - :type Settings: dict - :param dssInstance: An :class:`opendssdirect` instance - :type dssInstance: :class:`opendssdirect` - :param ElmObjectList: Dictionary of all dssElement, dssBus and dssCircuit objects - :type ElmObjectList: dict - :param dssSolver: An instance of one of the classed defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` - :raises: AssertionError if 'RegulatorObj' is not a wrapped OpenDSS Regulator element - - """ - - def __init__(self, RegulatorObj, Settings, dssInstance, ElmObjectList, dssSolver): - super(xfmrController).__init__(RegulatorObj, Settings, dssInstance, ElmObjectList, dssSolver) - self.P_old = 0 - self.Time = -1 - self.__Locked = False - self.__ControlledElm = RegulatorObj - self.__ConnTransformerName = 'Transformer.' + self.__ControlledElm.GetParameter('transformer').lower() - self.__ConnTransformer = ElmObjectList[self.__ConnTransformerName] - self.__ElmObjectList = ElmObjectList - self.__RPFlocking = Settings['RPF locking'] - Class, Name = self.__ControlledElm.GetInfo() - - self.__Name = 'pyCont_' + Class + '_' + Name - return - - @property - def Name(self): - return self.__Name - - @property - def ControlledElement(self): - return "{}.{}".format(self.Class, self.Name) - - def Update(self, Priority, Time, UpdateResults): - Powers = self.__ConnTransformer.GetVariable('CurrentsMagAng') - Powers = Powers[:int(len(Powers)/2)][::2] - P_new = sum((float(x)) for x in Powers) - if self.__RPFlocking and self.P_old < 0: - self.__Locked = self.__EnableLock() - elif self.__RPFlocking and self.P_old > 0: - self.__Locked = self.__DisableLock() - else: - pass - self.P_old = P_new - return 0 - - def __EnableLock(self): - self.__ControlledElm.SetParameter('enabled','False') - return True - - def __DisableLock(self): - self.__ControlledElm.SetParameter('enabled', 'True') - return False - diff --git a/src/pydss/pyControllers/__init__.py b/src/pydss/pyControllers/__init__.py deleted file mode 100644 index b575f6b8..00000000 --- a/src/pydss/pyControllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pydss.pyControllers import pyController diff --git a/src/pydss/pyControllers/pyController.py b/src/pydss/pyControllers/pyController.py deleted file mode 100644 index 21d22351..00000000 --- a/src/pydss/pyControllers/pyController.py +++ /dev/null @@ -1,35 +0,0 @@ -from os.path import dirname, basename, isfile -import glob - -from pydss.pyControllers import Controllers - -modules = glob.glob(Controllers.__path__[0]+"/*.py") -pythonFiles = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py') ] - -from pydss.dssElement import dssElement -ControllerTypes = {} - -for file in pythonFiles: - exec('from pydss.pyControllers.Controllers import {}'.format(file)) - exec('ControllerTypes["{}"] = {}.{}'.format(file, file, file)) - -def Create(ElmName, ControllerType, Settings, ElmObjectList, dssInstance, dssSolver): - - assert (ControllerType in ControllerTypes), "Definition for '{}' controller not found. \n " \ - "Please define the controller in ~pydss\pyControllers\Controllers".format( - ControllerType - ) - - assert (ElmName in ElmObjectList), "'{}' does not exist in the pydss master object dictionary.".format(ElmName) - relObject = ElmObjectList[ElmName] - - # except: - # Index = dssInstance.Circuit.SetActiveElement(ElmName) - # if int(Index) >= 0: - # ElmObjectList[ElmName] = dssElement(dssInstance) - # relObject = ElmObjectList[ElmName] - # else: - # return -1 - - ObjectController = ControllerTypes[ControllerType](relObject, Settings, dssInstance, ElmObjectList, dssSolver) - return ObjectController diff --git a/src/pydss/pyControllers/pyControllerAbstract.py b/src/pydss/pyControllers/pyControllerAbstract.py deleted file mode 100644 index dbb79dd8..00000000 --- a/src/pydss/pyControllers/pyControllerAbstract.py +++ /dev/null @@ -1,24 +0,0 @@ -import abc - - -class ControllerAbstract(abc.ABC): - - def __init__(self, controlledObj, Settings, dssInstance, ElmObjectList, dssSolver): - """Abstract class CONSTRUCTOR.""" - pass - - @abc.abstractmethod - def Update(self, Priority, Time, UpdateResults): - pass - - @abc.abstractmethod - def Name(self): - pass - - @abc.abstractmethod - def ControlledElement(self): - pass - - @abc.abstractmethod - def debugInfo(self): - pass \ No newline at end of file diff --git a/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer.py b/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer.py deleted file mode 100644 index d2dabeb7..00000000 --- a/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer.py +++ /dev/null @@ -1,439 +0,0 @@ -from pydss.pyPostprocessor.pyPostprocessAbstract import AbstractPostprocess -from pydss.pyPostprocessor.PostprocessScripts.DERMSOptimizer_helper_modules.opt_funcs import * -from scipy.sparse import lil_matrix -import scipy.sparse.linalg as sp -import scipy.sparse as sparse -from scipy import stats -import pydss.dssInstance as dI -import pandas as pd -from math import * -import numpy as np -import os - -class DERMSOptimizer(AbstractPostprocess): - REQUIRED_INPUT_FIELDS_AND_DEFAULTS = { - "control_flag": True, - "control_all_flag": True, - "num_DERMS": 1, - "Vlower": 0.955, - "Vupper": 1.038, - "coeff_p": 0.0005, - "coeff_q": 0.000001, - "stepsize_xp": 1, - "stepsize_xq": 5, - "stepsize_mu": 10, - "opf_iteration": 60, - "max_iterations": 50, - 'time_trigger_sec': 900, - "measurements_noises_flag": True, - "implementation_mode": "Voltage triggered", - 'Distributions' : { - "Vmes": ["norm", [0.0, 0.01]], - "Imes": ["norm", [0.0, 0.01]], - }, - } - - IMPLEMENTATION_MODES = ["Continuous", "Voltage triggered", "Time triggered", "Off"] - - def __init__(self, project, scenario, inputs, dssInstance, dssSolver, dssObjects, dssObjectsByClass, simulationSettings, Logger): - """Constructor method - """ - super(DERMSOptimizer, self).__init__(project, scenario, inputs, dssInstance, dssSolver, dssObjects, dssObjectsByClass, simulationSettings, Logger) - self.Options = {**self.REQUIRED_INPUT_FIELDS_AND_DEFAULTS, **inputs} - self.Settings = simulationSettings - self.Objects = dssObjectsByClass - - self.dssSolver = dssSolver - self.dss = dssInstance - self.Logger = Logger - self.logger.info('Creating DERMS Optimizer module') - - self.rootPath = simulationSettings['Project']['Project Path'] - self.dssPath = os.path.join(self.rootPath, simulationSettings['Project']['Active Project'], 'DSSfiles') - - self.Buses = [] - self.BusDistance = [] - self.Vbase_allnode = [] - - self.initialize_optimizer() - self.ExportCSVfiles = True - self.sim_start = False - self.DERMS_trigger_fail_count = 0 - self.DERMS_trigger_count = 0 - self.DERMS_trigger_success_count = 0 - - PVdata = self.getDERdata() - - assert self.Options["implementation_mode"] in self.IMPLEMENTATION_MODES, 'valid implementation modes are ()'.fomrmat( - self.IMPLEMENTATION_MODES - ) - self.dermsController = DERMS( - PVdata, self.controlbus, self.controlelem, self.BEcapacity, self.Nodes[self.nSlack:], self.BEname - ) - - def initialize_optimizer(self): - self.dss.utils.run_command('calcV') - self.Nodes = self.dss.Circuit.YNodeOrder() - self.nNodes = len(self.Nodes) - - self.Slack = self.Objects['Vsources']["Vsource.source"] - self.nSlack = int(self.Slack.GetValue("phases")) - - for node in self.Nodes: - node = node.lower() - bus, phaseInfo = node.split(".", 1) - self.Vbase_allnode.append(self.Objects['Buses'][bus].GetVariable("kVBase") * 1000) - self.BusDistance.append(self.Objects['Buses'][bus].GetVariable("Distance")) - - for x in self.Nodes: - bName = x.split('.')[0] - if bName not in self.Buses: - self.Buses.append(bName) - self.nBus = len(self.Buses) - - Y00, Y01, Y10, Y11, Y11_sparse, Y11_inv, Ybus = self.ExtractImpednceMatrix() - self.getBranchInfo() - - self.dssSolver.Solve() - V1, V1_pu = self.get_voltage_Yorder() - - capNames = self.dss.Capacitors.AllNames() - hCapNames = ",".join(capNames) - - loadData = self.get_elem_data( - "Loads", - ["Name", "kV", "kW", "pf", "phase", "bus", "phases", "VoltagesMagAng", "Powers", "conn"] - ) - total_load = sum([L["kW"] for L in loadData]) - #Do we want profile implementation here or a dedicatied profile manager? - - self.PVSystem = self.get_elem_data( - "Generators", - ["Name", "bus", "busFull", "phase", "kVA", "kW", "phases", "conn"] - ) - self.PVSystem_1phase = self.convert_3phasePV_to_1phasePV(self.PVSystem) - self.PVSystem_1phaseDF = pd.DataFrame(self.PVSystem_1phase) - if self.Options["control_flag"]: - PVlocation = [] - NPV = len(self.PVSystem_1phase) - nodeIndex_withPV = [] - PV_inverter_size = [] - for pv in self.PVSystem_1phase: - allpvbus = pv["bus"].split('.') - PVlocation.append(allpvbus[0]) - if len(allpvbus) == 1: - allpvbus = allpvbus + ['1', '2', '3'] - for ii in range(len(allpvbus) - 1): - pvbus = allpvbus[0] + '.' + allpvbus[ii + 1] - nodeIndex_withPV.append(self.Nodes.index(pvbus.upper())) - PV_inverter_size.append(float(pv['kVA']) / (len(allpvbus) - 1)) - - capacitors = self.get_elem_data("Capacitors", ["Name", "bus", "phase", "phases", "kV", "kvar"]) - PQ_load, PQ_PV, PQ_node, Qcap = self.calc_node_PQ() - - if self.Options["control_all_flag"]: - self.controlbus = self.Nodes[self.nSlack:] - else: - self.controlbus = self.getContolBuses(["Generators", "Capacitors"]) - - self.controlelem = [] - self.mu0 = [ - np.zeros(len(self.controlbus)), - np.zeros(len(self.controlbus)), - np.zeros(len(self.controlelem)) - ] - - self.power_flow_data = linear_powerflow_model(Y00, Y01, Y10, Y11_inv, [], V1, self.nSlack) - - self.stepsize_control = [self.Options["stepsize_xp"], self.Options["stepsize_xq"], - self.Options["stepsize_mu"]] - self.Vlimit = [self.Options["Vupper"], self.Options["Vlower"]] - return - - def calc_node_PQ(self): - PQ_load = self.get_PQ_by_class("Loads") - PQ_PV = self.get_PQ_by_class("Generators") - PQ_CAP = self.get_PQ_by_class("Capacitors") - Qcap = PQ_CAP.imag - PQ_node = - PQ_load + PQ_PV - 1j * np.array(Qcap) # power injection - return PQ_load, PQ_PV, PQ_node, Qcap - - def get_PQ_by_class(self, className): - P = [0] * len(self.Nodes) - Q = [0] * len(self.Nodes) - for name, obj in self.Objects[className].items(): - power = obj.GetValue("Powers") - bus = obj.GetValue("BusNames")[0] - nodes = bus.split(".")[1:] if len(bus.split(".")[1:]) else [1, 2, 3] - - for i, ii in enumerate(nodes): - nodeName = "{}.{}".format(bus.split(".")[0], ii) - index = self.Nodes.index(nodeName.upper()) - P[index] = power[2 * i] - Q[index] = power[2 * i + 1] - PQ = np.array(P) + 1j * np.array(Q) - return PQ - - def get_elem_data(self, ElementClass, Properties): - data = [] - for name, obj in self.Objects[ElementClass].items(): - datum = {} - for ppty in Properties: - if ppty == "bus": - datum[ppty] = obj.GetValue("BusNames")[0].split(".")[0] - elif ppty == "phase": - phases = obj.GetValue("BusNames")[0].split(".")[1:] - datum[ppty] = phases if len(phases) else [1, 2, 3] - elif ppty == "busFull": - datum[ppty] = obj.GetValue("BusNames")[0] - else: - datum[ppty] = obj.GetValue(ppty) - data.append(datum) - return data - - def convert_3phasePV_to_1phasePV(self, PVSystems): - # convert multi-phase PV into multiple 1-phase PVs for control implementation purpose - PVSystem_1phase = [] - for pv in PVSystems: - for i, ii in enumerate(pv["phase"]): - pv_perphase = {} - pv_perphase["Name"] = pv["Name"] # +'_node'+bus[ii+1] - pv_perphase["bus"] = pv["bus"] + '.' + ii - pv_perphase["kW"] = pv["kW"] / pv["phases"] - pv_perphase["kVA"] = pv["kVA"] / pv["phases"] - PVSystem_1phase.append(pv_perphase) - return PVSystem_1phase - - def get_incidence_matrix(self): - #self.dss.utils.run_command('solve mode=fault') dont think this is needed. May be wrong - Ybranch_prim, branch_node_incidence, nNeutral, record_index = self.construct_Yprime() - current_coeff_matrix = np.dot(Ybranch_prim, branch_node_incidence) - current_coeff_matrix = current_coeff_matrix[record_index, :-nNeutral] - branch_node_incidence = branch_node_incidence[record_index, :-nNeutral] - return current_coeff_matrix, branch_node_incidence, current_coeff_matrix - - def ExtractImpednceMatrix(self): - Y = self.dss.Circuit.SystemY() - Ybus = self.to_complex(Y) - Y00 = Ybus[0:self.nSlack, 0:self.nSlack] - Y01 = Ybus[0:self.nSlack, self.nSlack:] - Y10 = Ybus[self.nSlack:, 0:self.nSlack] - Y11 = Ybus[self.nSlack:, self.nSlack:] - Y11_sparse = lil_matrix(Y11) - Y11_sparse = Y11_sparse.tocsr() - a_sps = sparse.csc_matrix(Y11) - lu_obj = sp.splu(a_sps) - Y11_inv = lu_obj.solve(np.eye(self.nNodes - self.nSlack)) - return Y00, Y01, Y10, Y11, Y11_sparse, Y11_inv, Ybus - - def get_voltage_Yorder(self): - temp_Vbus = self.dss.Circuit.YNodeVArray() - voltage = [complex(0, 0)] * self.nNodes - for ii in range(self.nNodes): - voltage[ii] = complex(temp_Vbus[ii * 2], temp_Vbus[ii * 2 + 1]) - voltage_pu = list(map(lambda x: abs(x[0]) / x[1], zip(voltage, self.Vbase_allnode))) - return voltage, voltage_pu - - def to_complex(self, values): - Ydim = int(sqrt(len(values) / 2)) - Yreal = np.array(values[0::2]).reshape((Ydim, Ydim)) - Yimag = np.array(values[1::2]).reshape((Ydim, Ydim)) - Ycmplx = Yreal + 1j * Yimag - return Ycmplx - - def construct_Yprime(self): - Ybranch_prim = np.array([[complex(0, 0)] * 2 * self.nBarnchElements] * 2 * self.nBarnchElements) - branch_node_incidence = np.zeros([2 * self.nBarnchElements, self.nNodes + 422]) #TODO fix hard coded 422 for Green, 104 for Diamond - - record_index = [] - nNeutral = 0 - start_no = 0 - count = 0 - - temp_AllNodeNames = self.dss.Circuit.YNodeOrder() - for elemName, elmObj in self.branchElements.items(): - values = elmObj.GetValue('YPrim') - Yprim = self.to_complex(values) - buses = [ii.split('.')[0] for ii in elmObj.GetValue('BusNames')] - nodes = elmObj.GetValue('NodeOrder') - nPhases = int(len(nodes) / 2) - nNeutral += nodes.count(0) - for ii in range(nPhases): - from_node = buses[0] + '.' + str(nodes[ii]) - to_node = buses[1] + '.' + str(nodes[ii + nPhases]) - if nodes[ii] == 0: - temp_AllNodeNames.append(from_node.upper()) - if nodes[ii + nPhases] == 0: - temp_AllNodeNames.append(to_node.upper()) - from_node_index = temp_AllNodeNames.index(from_node.upper()) - to_node_index = temp_AllNodeNames.index(to_node.upper()) - branch_node_incidence[2 * count + ii, from_node_index] = 1 - branch_node_incidence[2 * count + nPhases + ii, to_node_index] = 1 - record_index.append(2 * count + ii) - count = count + nPhases - end_no = start_no + 2 * nPhases - Ybranch_prim[start_no:end_no, start_no:end_no] = Yprim - start_no = end_no - - return Ybranch_prim, branch_node_incidence, nNeutral, record_index - - def getContolBuses(self, classNames): - controlbus = [] - for className in classNames: - for elm in self.get_elem_data(className, ["bus", "phases"]): - for phase in elm["phases"]: - controlbus.append("{}.{}".format(elm["bus"], phase)) - return controlbus - - @staticmethod - def _get_required_input_fields(): - return {} - - def getBranchInfo(self): - self.branchElements = {**self.Objects["Lines"], **self.Objects["Transformers"]} - self.nBarnchElements = len(self.branchElements) - self.BEindex = [] - self.BEcapacity = [] - self.BEname = [] - - for elmName, elmObj in self.branchElements.items(): - nTerms = elmObj.GetValue("NumTerminals") - Nodes = elmObj.GetValue("NodeOrder") - nPhases = int(len(Nodes) / nTerms) - self.BEindex.append(list(range(nPhases))) - for ii in range(nPhases): - self.BEcapacity.append(elmObj.GetValue("NormalAmps")) - self.BEname.append("{}.{}".format(elmName, Nodes[ii])) - - def getDERdata(self): - data = self.get_elem_data("Generators", ["Name", "busFull", "phase", "kVA", "kW", "phases", "conn"]) - data = pd.DataFrame(data) - data.index = data["Name"] - PVdata = { - "pvName": [], - "pvLocation": [], - "bus": [], - "pvSize": [], - "inverterSize": [], - } - for pv1P in self.PVSystem_1phaseDF["Name"].tolist(): - pv = data.loc[pv1P] - PVdata["pvName"].append(pv['Name']) - PVdata["bus"].append(pv['busFull']) - PVdata["pvSize"].append(pv['kW']) - PVdata["inverterSize"].append(pv['kVA']) - for b in self.buses(pv): - if b.upper() not in PVdata["pvLocation"]: - PVdata["pvLocation"].append(b.upper()) - break - return PVdata - - def buses(self, pv): - bus = pv['busFull'].split(".") - if len(bus) == 1: - bus += ["1", "2", "3"] - return [f"{bus[0]}.{i}" for i in bus[1:]] - - def run(self, step, stepMax, simulation=None): - """Induces and removes a fault as the simulation runs as per user defined settings. - """ - self.logger.info('Running DERMS optimization module') - opt_iter = 0 - - #Really bad codeing form - mu0 = [ - [0 for x in range(len(self.controlbus))], - [0 for x in range(len(self.controlbus))], - [0 for x in range(len(self.controlelem))] - ] - - PVmax = [pv["kW"] if pv["kW"] < pv["kVA"] else pv["kVA"] for pv in self.PVSystem_1phase] - - while opt_iter < self.Options["max_iterations"]: - self.dssSolver.reSolve() - PVlocation, PVpower, Vmes, Imes = self.dermsController.monitor(self._dssInstance, self.Objects, - self.PVSystem_1phaseDF) - self.Logger.info('Voltage measurements: {}'.format(len(Vmes))) - self.Logger.info('Maximum voltage: {}'.format(max(Vmes))) - - if self.Options["measurements_noises_flag"]: - distName, distParams = self.Options['Distributions']["Vmes"] - dist = getattr(stats, distName) - Vmes = Vmes + dist.rvs(*distParams, size=len(Vmes)) - distName, distParams = self.Options['Distributions']["Imes"] - dist = getattr(stats, distName) - Imes = Imes + dist.rvs(*distParams, size=len(Imes)) - - if self.Options["implementation_mode"] == "Continuous": - DERMS_trigger = 1 - elif self.Options["implementation_mode"] == "Voltage triggered": - if max(Vmes) > self.Options["Vupper"] or min(Vmes) < self.Options["Vlower"]: - DERMS_trigger = 1 - #TODO: Should DERMS trigger count be in this if statement? - elif max(Vmes) <= self.Options["Vupper"] and min(Vmes) >= self.Options["Vlower"] and opt_iter >= 1: - DERMS_trigger = 0 - self.DERMS_trigger_count += 1 - self.DERMS_trigger_success_count += 1 - self.Logger.info('DERMS OPF successfully triggered, maxV: {}'.fotmat(max(Vmes))) - break - else: - DERMS_trigger = 0 - elif self.Options["implementation_mode"] == "Time triggered": - #TODO: Is step * stepsize_sim >= time_mode_resolution required? Is the idea to ensure that the controller does not run on the first iteration? - if step > 0 and self.dssSolver.GetTotalSeconds() % self.Options["time_trigger_sec"] == 0: - DERMS_trigger = 1 - else: - DERMS_trigger = 0 - elif self.Options["implementation_mode"] == "Off": - DERMS_trigger = 0 - - if DERMS_trigger == 1: - - [x1, mu1] = self.dermsController.control( - self.power_flow_data, - self.Options, - self.stepsize_control, - mu0, - self.Vlimit, - PVpower, - Imes, - Vmes, - PVmax - ) - # ------ apply the setpoint ------ - nPV_1_phs = len(self.PVSystem_1phase) - for pv in self.PVSystem: - idx = [i for i, x in enumerate(self.PVSystem_1phase) if x["Name"] == pv["Name"]] - self.Objects["Generators"][pv["Name"]].SetParameter("kW", sum([x1[ii] for ii in idx])) - self.Objects["Generators"][pv["Name"]].SetParameter("kvar", sum([x1[ii + nPV_1_phs] for ii in idx])) - - mu0 = mu1 - # resV_record.append(max(Vmes)) - opt_iter = opt_iter + 1 - if max(Vmes) <= self.Options["Vupper"] and min(Vmes) >= self.Options["Vlower"]: - self.DERMS_trigger_count += 1 - self.DERMS_trigger_success_count += 1 - self.Logger.info('DERMS OPF successfully triggered') - break - elif DERMS_trigger == 0: - # TODO: Is this piece of code required? - # for pv in self.PVSystem: - # PVgen = float(pv['kW']) * PVshape[ - # int((present_step - 1) * stepsize_sim / data_resolution + 3600 / data_resolution * startH)] - # if PVgen > float(pv["kVA"]): - # PVgen = float(pv["kVA"]) - # dss.run_command('edit ' + str(pv["name"]) + ' kW=' + str(PVgen) + ' pf=' + str(pf_auto)) - break - if opt_iter == self.Options["max_iterations"]: - self.DERMS_trigger_count += 1 - if max(Vmes) > self.Options["Vupper"] or min(Vmes) < self.Options["Vlower"]: - self.DERMS_trigger_fail_count += 1 - self.Logger.warning('DERMS OPF triggered and failed') - - - opt_iter += 1 - #step-=1 # uncomment the line if the post process needs to rerun for the same point in time - return step - - def getPreoptimizationResults(self): - return \ No newline at end of file diff --git a/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer_helper_modules/__init__.py b/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer_helper_modules/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer_helper_modules/opt_funcs.py b/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer_helper_modules/opt_funcs.py deleted file mode 100644 index e3852716..00000000 --- a/src/pydss/pyPostprocessor/PostprocessScripts/DERMSOptimizer_helper_modules/opt_funcs.py +++ /dev/null @@ -1,439 +0,0 @@ -import numpy as np -from scipy.sparse import lil_matrix -import scipy.sparse.linalg as sp -import scipy.sparse as sparse -import math -import csv -import matplotlib.pyplot as plt - -def linear_powerflow_model(Y00,Y01,Y10,Y11_inv,I_coeff,V1,slack_no): - # voltage linearlization - V1_conj = np.conj(V1[slack_no:]) - V1_conj_inv = 1 / V1_conj - coeff_V = Y11_inv * V1_conj_inv - coeff_V_P = coeff_V - coeff_V_Q = -1j*coeff_V - coeff_Vm = -np.dot(Y11_inv,np.dot(Y10,V1[:slack_no])) - - # voltage magnitude linearization - m = coeff_Vm - m_inv = 1 / coeff_Vm - coeff_Vmag_k = abs(m) - A = (np.multiply(coeff_V.transpose(),m_inv)).transpose() - coeff_Vmag_P = (np.multiply(A.real.transpose(),coeff_Vmag_k)).transpose() - coeff_Vmag_Q = (np.multiply((-1j*A).real.transpose(),coeff_Vmag_k)).transpose() - - # current linearization - if len(I_coeff): - coeff_I_P = np.dot(I_coeff[:,slack_no:],coeff_V_P) - coeff_I_Q = np.dot(I_coeff[:,slack_no:],coeff_V_Q) - coeff_I_const = np.dot(I_coeff[:,slack_no:],coeff_Vm) + np.dot(I_coeff[:,:slack_no],V1[:slack_no]) - else: - coeff_I_P = [] - coeff_I_Q = [] - coeff_I_const = [] - - #=========================================Yiyun's Notes===========================================# - # Output relations: Vmag = coeff_Vmag_P * Pnode + coeff_Vmag_Q * Qnode + coeff_Vm - # I = coeff_I_P * Pnode + coeff_I_Q * Qnode + coeff_I_const (complex value) - # ================================================================================================# - - return coeff_V_P, coeff_V_Q, coeff_Vm, coeff_Vmag_P, coeff_Vmag_Q, coeff_Vmag_k, coeff_I_P, coeff_I_Q, coeff_I_const - -def validate_linear_model(coeff_Vp,coeff_Vq,coeff_Vm,PQ_node,slack_number): - V_cal = coeff_Vm + np.dot(coeff_Vp,np.array([np.real(ii)*1000 for ii in PQ_node[slack_number:]])) + np.dot(coeff_Vq,np.array([np.imag(ii)*1000 for ii in PQ_node[slack_number:]])) - v_cal_1 = coeff_Vm + np.dot(coeff_Vp,np.conj(PQ_node[slack_number:]*1000)) - #coeff_Vp*Pnode + coeff_Vq*Qnode + coeff_Vm - - # =========================================Yiyun's Notes===========================================# - # 1000 should be the S base - # =================================================================================================# - - return [V_cal,v_cal_1] - -def check_VI_correct(V1,PQ_node,slack_number,coeff_V,coeff_Vm,coeff_Vmag_P,coeff_Vmag_Q,coeff_Vmag_k,Y10,Y11,coeff_I_P, coeff_I_Q, coeff_I_const,I_coeff): - V1_linear = np.dot(coeff_V,np.conj(PQ_node[slack_number:]*1000)) + coeff_Vm - V1_linear = list(V1_linear) - Vdiff = list(map(lambda x: abs(x[0]-x[1])/abs(x[0])*100,zip(V1[slack_number:],V1_linear))) - with open('voltage_diff.csv','w') as f: - csvwriter = csv.writer(f) - csvwriter.writerow(Vdiff) - f.close() - - V1_mag_linear = np.dot(coeff_Vmag_P,(PQ_node[slack_number:]*1000).real) + np.dot(coeff_Vmag_Q,(PQ_node[slack_number:]*1000).imag) + coeff_Vmag_k - V1_mag_linear = list(V1_mag_linear) - Vdiff = list(map(lambda x: abs(abs(x[0])-x[1])/abs(x[0])*100,zip(V1[slack_number:],V1_mag_linear))) - with open('voltageMag_diff.csv','w') as f: - csvwriter = csv.writer(f) - csvwriter.writerow(Vdiff) - f.close() - - # get Ibus - Ibus = list(map(lambda x: (x[0]*1000/x[1]).conjugate(),zip(list(PQ_node)[slack_number:],V1[slack_number:]))) - Ibus_cal_0 = np.dot(Y10,V1[0:slack_number]) - Ibus_cal_1 = np.dot(Y11,V1[slack_number:]) - Ibus_cal = list(map(lambda x: x[0]+x[1],zip(Ibus_cal_0,Ibus_cal_1))) - Idiff = list(map(lambda x: abs(x[0]-x[1]),zip(Ibus,Ibus_cal))) - with open('currentBus_diff.csv','w') as f: - csvwriter = csv.writer(f) - csvwriter.writerow(Idiff) - f.close() - - # get Ibranch - Ibranch = np.dot(I_coeff,V1) - Ibranch_cal = np.dot(I_coeff[:,slack_number:],V1_linear)+np.dot(I_coeff[:,0:slack_number],V1[:slack_number]) - Ibranch_diff = list(map(lambda x: abs(x[0]-x[1]),zip(Ibranch,Ibranch_cal))) - with open('current_diff.csv','w') as f: - csvwriter = csv.writer(f) - csvwriter.writerow(Ibranch_diff) - f.close() - -def costFun(x,dual_upper,dual_lower,v1_pu,Ppv_max,coeff_p,coeff_q,NPV,control_bus_index,Vupper,Vlower,dual_current,ThermalLimit,I1_mag): - # cost_function = coeff_p*(Pmax-P)^2+coeff_q*Q^2+dual_upper*(v1-1.05)+dual_lower*(0.95-v1) - f1 = 0 - for ii in range(NPV): - f1 = f1 + coeff_p*(Ppv_max[ii]-x[ii])*(Ppv_max[ii]-x[ii])+coeff_q*x[ii+NPV]*x[ii+NPV] - #f = f1 + np.dot(dual_upper,(np.array(v1_pu)[control_bus_index]-Vupper)) + np.dot(dual_lower,(Vlower-np.array(v1_pu)[control_bus_index])) - v_evaluate = [v1_pu[ii] for ii in control_bus_index] - f2 = f1 + np.dot(dual_upper,np.array([max(ii-Vupper,0) for ii in v_evaluate])) + np.dot(dual_lower,np.array([max(Vlower-ii,0) for ii in v_evaluate])) - f3 = np.dot(dual_current,np.array([max(ii,0) for ii in list(map(lambda x: x[0]*x[0]-x[1]*x[1],zip(I1_mag,ThermalLimit)))])) - f = f2+f3 - - # =========================================Yiyun's Notes===========================================# - # f1 is the quadratic PV curtailment plus quadratic reactive power injection - # f2 is the Lagrangian term for voltage violations and line current violations - # ===> Note the "control_bus_index" might be the index for measurement sensitivity analysis - # =================================================================================================# - - return [f1,f] - -def PV_costFun_gradient(x, coeff_p, coeff_q, Pmax): - grad = np.zeros(len(x)) - for ii in range(int(len(x)/2)): - grad[ii] = -2*coeff_p*(Pmax[ii]*1000-x[ii]*1000) - grad[ii+int(len(x)/2)] = 2*coeff_q*x[ii+int(len(x)/2)]*1000 - #grad[ii + int(len(x) / 2)] = 0 - - # =========================================Yiyun's Notes===========================================# - # x is the decision vector [P,Q] - # =================================================================================================# - - return grad - -def voltage_constraint_gradient(AllNodeNames,node_withPV, dual_upper, dual_lower, coeff_Vmag_p, coeff_Vmag_q): - node_noslackbus = AllNodeNames - node_noslackbus[0:3] = [] - - # =========================================Yiyun's Notes===========================================# - # remove the slack bus - # =================================================================================================# - - grad_upper = np.matrix([0] * len(node_noslackbus)*2).transpose() - grad_lower = np.matrix([0] * len(node_noslackbus)*2).transpose() - count = 0 - for node in node_noslackbus: - if node in node_withPV: - grad_upper[count] = dual_upper.transpose()*coeff_Vmag_p[:,count] - grad_upper[count+len(node_noslackbus)] = dual_upper.transpose() * coeff_Vmag_q[:,count] - grad_lower[count] = -dual_lower.transpose() * coeff_Vmag_p[:, count] - grad_lower[count + len(node_noslackbus)] = -dual_lower.transpose() * coeff_Vmag_q[:, count] - count = count + 1 - return [grad_upper,grad_lower] - -def current_constraint_gradient(AllNodeNames,node_withPV, dual_upper,coeff_Imag_p, coeff_Imag_q): - node_noslackbus = AllNodeNames - node_noslackbus[0:3] = [] - grad_upper = np.matrix([0] * len(node_noslackbus)*2).transpose() - count = 0 - for node in node_noslackbus: - if node in node_withPV: - grad_upper[count] = dual_upper.transpose()*coeff_Imag_p[:,count] - grad_upper[count+len(node_noslackbus)] = dual_upper.transpose() * coeff_Imag_q[:,count] - count = count + 1 - return grad_upper - - # =========================================Yiyun's Notes===========================================# - # PV_costFun_gradient, voltage_constraint_gradient, current_constraint_gradient and project_PV.. - # ... are set up for updating the PV decision variables in eq(10) - # =================================================================================================# - -def voltage_constraint(V1_mag): - g = V1_mag-1.05 - g.append(0.95-V1_mag) - return g - -def current_constraint(I1_mag,Imax): - g = [] - g.append(I1_mag-Imax) - - # =========================================Yiyun's Notes===========================================# - # assume single directional power flow - # voltage_constraint, current_constraint, and project_dualvariable are set up for updating the dual... - # ... variables in eq (11) - # =================================================================================================# - - return g - -def project_dualvariable(mu): - for ii in range(len(mu)): - mu[ii] = max(mu[ii],0) - - # =========================================Yiyun's Notes===========================================# - # If the corresponding constraints in primal problem is in canonical form, then dual variable is >=0 - # =================================================================================================# - - return mu - -def project_PV(x,Pmax,Sinv): - Qavailable = 0 - Pavailable = 0 - num = len(Sinv) - for ii in range(num): - if x[ii] > Pmax[ii]: - x[ii] = Pmax[ii] - elif x[ii] < 0: - x[ii] = 0 - - if Sinv[ii] > x[ii]: - Qmax = math.sqrt(Sinv[ii]*Sinv[ii]-x[ii]*x[ii]) - else: - Qmax = 0 - if x[ii+num] > Qmax: - x[ii+num] = Qmax - # elif x[ii + num] < 0: - # x[ii + num] = 0 - elif x[ii+num] < -Qmax: - x[ii+num] = -Qmax - - Pavailable = Pavailable + Pmax[ii] - Qavailable = Qavailable + Qmax - return [x,Pavailable,Qavailable] - -def dual_update(mu,coeff_mu,constraint): - mu_new = mu + coeff_mu*constraint - mu_new = project_dualvariable(mu_new) - - # =========================================Yiyun's Notes===========================================# - # normal way for update Lagrangian variable is by the sub-gradient of cost function - # Here is the equation (11) in the draft paper - # =================================================================================================# - - return mu_new - -def matrix_cal_for_subPower(V0, Y00, Y01, Y11, V1_noload): - diag_V0 = np.matrix([[complex(0, 0)] * 3] * 3) - diag_V0[0, 0] = V0[0] - diag_V0[1, 1] = V0[1] - diag_V0[2, 2] = V0[2] - K = diag_V0 * Y01.conj() * np.linalg.inv(Y11.conj()) - g = diag_V0 * Y00.conj() * np.matrix(V0).transpose().conj() + diag_V0 * Y01.conj() * V1_noload.conj() - return[K,g] - -def subPower_PQ(V1, PQ_node, K, g): - diag_V1 = np.matrix([[complex(0, 0)] * len(V1)] * len(V1)) - for ii in range(len(V1)): - diag_V1[ii, ii] = V1[ii] - M = K * np.linalg.inv(diag_V1) - MR = M.real - MI = M.imag - P0 = g.real + (MR.dot(PQ_node.real)*1000 - MI.dot(PQ_node.imag)*1000) - Q0 = g.imag + (MR.dot(PQ_node.imag)*1000 + MI.dot(PQ_node.real)*1000) - - P0 = P0/1000 - Q0 = Q0/1000 # convert to kW/kVar - - # =========================================Yiyun's Notes===========================================# - # Power injection at substation/feeder head - # =================================================================================================# - - return [P0, Q0, M] - -def sub_costFun_gradient(x, sub_ref, coeff_sub, sub_measure, M, node_withPV): - grad_a = np.matrix([0] * len(x)).transpose() - grad_b = np.matrix([0] * len(x)).transpose() - grad_c = np.matrix([0] * len(x)).transpose() - - MR = M.real - MI = M.imag - count = 0 - for node in node_withPV: - grad_a[count] = -MR[0, int(node)] - grad_b[count] = -MR[1, int(node)] - grad_c[count] = -MR[2, int(node)] - - grad_a[count + len(node_withPV)] = MI[0, int(node)] - grad_b[count + len(node_withPV)] = MI[1, int(node)] - grad_c[count + len(node_withPV)] = MI[2, int(node)] - - count = count + 1 - - res = coeff_sub * ((sub_measure[0] - sub_ref[0]) *1000* grad_a + (sub_measure[1] - sub_ref[1])*1000 * grad_b - + (sub_measure[2] - sub_ref[2])*1000 * grad_c) - res = res/1000 - - return res - -def projection(x,xmax,xmin): - for ii in range(len(x)): - if x.item(ii) > xmax[ii]: - x[ii] = xmax[ii] - if x.item(ii) < xmin[ii]: - x[ii] = xmin[ii] - return x - -class DERMS: - def __init__(self, pvData,controlbus,controlelem,controlelem_limit,sub_node_names,sub_elem_names): - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # PV_name: names of all PVs in the zone - # PV_size: sizes of all PVs in the zone - # PV_location: busnames of all PVs in the zone - # controlbus: names of all controlled nodes - # sub_node_names: names of all nodes in the zone - # sub_node_names "include" controlbus - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - self.PV_name = pvData["pvName"] - self.PV_location = pvData["pvLocation"] - self.PV_size = pvData["pvSize"] - self.inverter_size = pvData["inverterSize"] - self.control_bus = controlbus - - sub_node_names = [ii.upper() for ii in sub_node_names] - self.controlbus_index = [sub_node_names.index(ii.upper()) for ii in controlbus] # control bus index in the sub system (number) - # here - PVbus_index = [] - for bus in self.PV_location: - temp = bus.split('.') - if len(temp) == 1: - temp = temp + ['1', '2', '3'] - for ii in range(len(temp) - 1): - PVbus_index.append(sub_node_names.index((temp[0] + '.' + temp[ii + 1]).upper())) - - # =========================================Yiyun's Notes===========================================# - # adding .1 .2 .3 following the number to recognize the three phases. - # =================================================================================================# - self.PVbus_index = PVbus_index - self.control_elem = controlelem - self.controlelem_limit = controlelem_limit - self.controlelem_index = [sub_elem_names.index(ii) for ii in controlelem] # control branches index in the sub system (number) - - def monitor(self, dss, dssObjects, PVSystem_1phase): - PVpowers = [] - for pv in PVSystem_1phase["Name"].tolist(): - nPhases = dssObjects["Generators"][pv].GetValue("phases") - power = dssObjects["Generators"][pv].GetValue("Powers") - PVpowers.append([sum(power[::2])/nPhases, sum(power[1::2])/nPhases]) - PVpowers = np.asarray(PVpowers) - - Vmes = [] - for bus in self.control_bus: - busName = bus.split('.')[0].lower() - Vmag = dssObjects["Buses"][busName].GetValue("puVmagAngle")[::2] - allbusnode = dss.Bus.Nodes() - phase = bus.split('.')[1] - index = allbusnode.index(int(phase)) - Vnode = Vmag[index] - Vmes.append(Vnode) - - Imes = [] - for elem in self.control_elem: - className = elem.split('.')[0] + "s" - I = dssObjects[className][elem].GetValue("CurrentsMagAng")[::2][:3] #TODO: Why is there a hardcoded [:3] ? - Imes.append(I) - - return [self.PV_location,PVpowers,Vmes,Imes] - - - - def control(self, linear_PF_coeff, Options,stepsize,mu0,Vlimit,PVpower,Imes,Vmes,PV_Pmax_forecast): - coeff_p = Options["coeff_p"] - coeff_q = Options["coeff_q"] - - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # linear_PF_coeff is the linear power flow model coefficients for the zone, and linear power flow model - # coefficients are the result vector from function "linear_powerflow_model" - # coeff_p, coeff_q are constant coefficients in PV cost function - # stepsize is a vector of stepsize constants - # mu0 is the dual variable from last time step: mu_Vmag_upper0, mu_Vmag_lower0, mu_I0 - # Vlimit is the allowed voltage limit: Vupper and Vlower - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - PVname = self.PV_name - NPV = len(PVname) - x0 = np.zeros(2 * NPV) - for ii in range(NPV): - x0[ii] = -PVpower[ii][0] # in kW - x0[ii + NPV] = -PVpower[ii][1] # in kVar - - #coeff_V_P = linear_PF_coeff[0] - #coeff_V_Q = linear_PF_coeff[1] - #coeff_Vm = linear_PF_coeff[2] - coeff_Vmag_P = linear_PF_coeff[3] - coeff_Vmag_Q = linear_PF_coeff[4] - #coeff_Vmag_k = linear_PF_coeff[5] - coeff_I_P = linear_PF_coeff[6] - coeff_I_Q = linear_PF_coeff[7] - #coeff_I_const = linear_PF_coeff[8] - stepsize_xp = stepsize[0] - stepsize_xq = stepsize[1] - stepsize_mu = stepsize[2] - Vupper = Vlimit[0] - Vlower = Vlimit[1] - - controlbus_index = self.controlbus_index - PVbus_index = self.PVbus_index - controlelem_index = self.controlelem_index - PV_inverter_size = self.inverter_size - Imes_limit = self.controlelem_limit - - mu_Vmag_upper0 = mu0[0] - mu_Vmag_lower0 = mu0[1] - mu_I0 = mu0[2] - - PVcost_fun_gradient = PV_costFun_gradient(x0, coeff_p, coeff_q, PV_Pmax_forecast) - - Vmag_upper_gradient = np.concatenate((np.dot(coeff_Vmag_P[np.ix_([ii for ii in controlbus_index],[ii for ii in PVbus_index])].transpose(), mu_Vmag_upper0), - np.dot(coeff_Vmag_Q[np.ix_([ii for ii in controlbus_index], [ii for ii in PVbus_index])].transpose(), mu_Vmag_upper0)),axis=0) - Vmag_lower_gradient = np.concatenate((np.dot(coeff_Vmag_P[np.ix_([ii for ii in controlbus_index],[ii for ii in PVbus_index])].transpose(), mu_Vmag_lower0), - np.dot(coeff_Vmag_Q[np.ix_([ii for ii in controlbus_index],[ii for ii in PVbus_index])].transpose(), mu_Vmag_lower0)),axis=0) - - Vmag_gradient = Vmag_upper_gradient - Vmag_lower_gradient - if len(mu_I0)>0 : - temp_real = mu_I0 * np.array(Imes.real) - temp_imag = mu_I0 * np.array(Imes.imag) - - I_gradient_real = np.concatenate((np.dot( - coeff_I_P[np.ix_([ii for ii in controlelem_index], [ii for ii in PVbus_index])].real.transpose(), - temp_real), np.dot( - coeff_I_Q[np.ix_([ii for ii in controlelem_index], [ii for ii in PVbus_index])].real.transpose(), - temp_real)), axis=0) - I_gradient_imag = np.concatenate((np.dot( - coeff_I_P[np.ix_([ii for ii in controlelem_index], [ii for ii in PVbus_index])].imag.transpose(), - temp_imag), np.dot( - coeff_I_Q[np.ix_([ii for ii in controlelem_index], [ii for ii in PVbus_index])].imag.transpose(), - temp_imag)), axis=0) - I_gradient = 2 * I_gradient_real + 2 * I_gradient_imag - else: - I_gradient = 0 - - gradient = PVcost_fun_gradient + Vmag_gradient + I_gradient / 1000 - - # compute x1, mu1 - x1 = np.concatenate([x0[:NPV] - stepsize_xp * gradient[:NPV], x0[NPV:] - stepsize_xq * gradient[NPV:]]) - [x1, Pmax_allPV, Qmax_allPV] = project_PV(x1, PV_Pmax_forecast, PV_inverter_size) - x1 = np.array([round(ii, 5) for ii in x1]) - - mu_Vmag_lower1 = mu_Vmag_lower0 + stepsize_mu * (Vlower - np.array(Vmes)) - mu_Vmag_upper1 = mu_Vmag_upper0 + stepsize_mu * (np.array(Vmes) - Vupper) - mu_Vmag_lower1 = project_dualvariable(mu_Vmag_lower1) - mu_Vmag_upper1 = project_dualvariable(mu_Vmag_upper1) - if mu_I0: - mu_I1 = mu_I0 + stepsize_mu / 300 * np.array(list(map(lambda x: x[0] * x[0] - x[1] * x[1], zip(Imes, Imes_limit)))) - mu_I1 = project_dualvariable(mu_I1) - else: - mu_I1 = mu_I0 - mu1 = [mu_Vmag_upper1,mu_Vmag_lower1,mu_I1] - # =========================================Yiyun's Notes===========================================# - # Each time of calling DERMS.control, it is a one step update of PV real and reactive power outputs - # =================================================================================================# - - return [x1,mu1] diff --git a/src/pydss/pyPostprocessor/PostprocessScripts/__init__.py b/src/pydss/pyPostprocessor/PostprocessScripts/__init__.py deleted file mode 100644 index 4ef8cbcd..00000000 --- a/src/pydss/pyPostprocessor/PostprocessScripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pydss.pyPostprocessor import pyPostprocess diff --git a/src/pydss/pyPostprocessor/__init__.py b/src/pydss/pyPostprocessor/__init__.py deleted file mode 100644 index 5adf07d0..00000000 --- a/src/pydss/pyPostprocessor/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pydss.pyPostprocessor import pyPostprocess \ No newline at end of file diff --git a/src/pydss/pyPostprocessor/pyPostprocess.py b/src/pydss/pyPostprocessor/pyPostprocess.py deleted file mode 100644 index 36904d36..00000000 --- a/src/pydss/pyPostprocessor/pyPostprocess.py +++ /dev/null @@ -1,35 +0,0 @@ -from os.path import dirname, basename, isfile -import glob - -from pydss.pyPostprocessor import PostprocessScripts - - - - -modules = glob.glob(PostprocessScripts.__path__[0]+"/*.py") -pythonFiles = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] - -POST_PROCESSES = {} -for file in pythonFiles: - exec('from pydss.pyPostprocessor.PostprocessScripts import {}'.format(file)) - exec('POST_PROCESSES["{}"] = {}.{}'.format(file, file, file)) - -def Create(project, scenario, ppInfo, dssInstance, dssSolver, dssObjects, dssObjectsByClass, simulationSettings, Logger): - test = None - PostProcessorClass = None - ScriptName = ppInfo.script - assert (ScriptName in pythonFiles), \ - f"Definition for '{ScriptName}' post process script not found. \n" \ - "Please define the controller in PyDSS/pyPostprocessor/PostprocessScripts" - PostProcessor = POST_PROCESSES[ScriptName]( - project, - scenario, - ppInfo, - dssInstance, - dssSolver, - dssObjects, - dssObjectsByClass, - simulationSettings, - Logger, - ) - return PostProcessor diff --git a/src/pydss/pyResults.py b/src/pydss/pyResults.py deleted file mode 100644 index a519eb89..00000000 --- a/src/pydss/pyResults.py +++ /dev/null @@ -1,86 +0,0 @@ -import pandas as pd -import numpy as np -import os - -class pyContrReader: - def __init__(self, Path): - self.pyControllers = {} - filenames = os.listdir(Path) - for filename in filenames: - if filename.endswith('.xlsx') and not filename.startswith('~$'): - pyControllerType = filename.split('.')[0] - filepath = os.path.join(Path, filename) - assert (os.path.exists(filepath)), 'path: "{}" does not exist!'.format(filepath) - ControllerDataset = pd.read_excel(filepath, skiprows=[0,], index_col=[0]) - pyControllerNames = ControllerDataset.index.tolist() - pyController = {} - for pyControllerName in pyControllerNames: - pyControllerData = ControllerDataset.loc[pyControllerName] - assert len(pyControllerData == 1), 'Multiple pydss controller definitions for a single OpenDSS ' +\ - 'element not allowed' - pyControllerDict = pyControllerData.to_dict() - pyController[pyControllerName] = pyControllerDict - self.pyControllers[pyControllerType] = pyController - - -class pySubscriptionReader: - def __init__(self, filePath): - self.SubscriptionDict = {} - assert (os.path.exists(filePath)), 'path: "{}" does not exist!'.format(filePath) - SubscriptionData = pd.read_excel(filePath, skiprows=[0,], index_col=[0]) - requiredColumns = {'Property', 'Subscription ID', 'Unit', 'Subscribe', 'Data type'} - fileColumns = set(SubscriptionData.columns) - diff = requiredColumns.difference(fileColumns) - - assert (len(diff) == 0), 'Missing column in the subscriptions file.\nRequired columns: {}'.format( - requiredColumns - ) - Subscribe = SubscriptionData['Subscribe'] - assert (Subscribe.dtype == bool), 'The subscribe column can only have boolean values.' - self.SubscriptionDict = SubscriptionData.T.to_dict() - - - - -class pyExportReader: - def __init__(self, filePath): - self.pyControllers = {} - self.publicationList = [] - assert (os.path.exists(filePath)), 'path: "{}" does not exist!'.format(filePath) - ControllerDataset = pd.read_excel(filePath, skiprows=[0,], index_col=[0]) - assert (ControllerDataset.columns[0] == 'Publish'), 'First column after class declarations in the ' +\ - 'export defination files should have column ' +\ - 'name "Publish"' - - Publish = ControllerDataset['Publish'] - assert (Publish.dtype == bool), 'The publish column can only have boolean values.' - ControllerDatasetFiltered = ControllerDataset[ControllerDataset.columns[1:]] - - pyControllerNames = ControllerDatasetFiltered.index.tolist() - pyController = {} - for pyControllerName, doPublish in zip(pyControllerNames, Publish.values): - pyControllerData = ControllerDatasetFiltered.loc[pyControllerName] - pulishdata = ControllerDataset['Publish'].loc[pyControllerName] - if isinstance(pulishdata, np.bool_): - pulishdata = [pulishdata] - else: - pulishdata = pulishdata.dropna().values - Data = pyControllerData.copy() - Data.index = range(len(Data)) - for i, publish in enumerate(pulishdata): - if publish: - if isinstance(Data, pd.core.frame.DataFrame): - properties = Data.loc[i].dropna() - else: - properties = Data.dropna().values - for property in properties: - self.publicationList.append("{} {}".format(pyControllerName, property)) - - if len(pyControllerData) > 1: - pyControllerData = pd.Series(pyControllerData.values.flatten()) - pyControllerDict = pyControllerData.dropna().to_dict() - else: - pyControllerDict = pyControllerData.dropna().to_dict() - - self.pyControllers[pyControllerName] = pyControllerDict - self.publicationList = list(set(self.publicationList)) \ No newline at end of file diff --git a/src/pydss/pyContrReader.py b/src/pydss/py_contr_reader.py similarity index 66% rename from src/pydss/pyContrReader.py rename to src/pydss/py_contr_reader.py index 34879eeb..ad68654e 100644 --- a/src/pydss/pyContrReader.py +++ b/src/pydss/py_contr_reader.py @@ -1,40 +1,36 @@ from collections import defaultdict -import pandas as pd -import numpy as np import os -import toml from pydss.config_data import convert_config_data_to_toml from pydss.registry import Registry from pydss.utils.utils import load_data from pydss.exceptions import InvalidConfiguration, InvalidParameter -class pyContrReader: - def __init__(self, Path): + +class PyContrReader: + def __init__(self, path): self.pyControllers = {} - filenames = os.listdir(Path) - found_config_file = False - found_excel_file = False + filenames = os.listdir(path) for filename in filenames: - pyControllerType, ext = os.path.splitext(filename) - if filename.startswith('~$'): + py_controller_type, ext = os.path.splitext(filename) + if filename.startswith("~$"): continue - elif ext == '.xlsx': + elif ext == ".xlsx": filename = convert_config_data_to_toml(filename) elif ext != ".toml": continue - if pyControllerType not in self.pyControllers: - self.pyControllers[pyControllerType] = {} - filepath = os.path.join(Path, filename) - assert (os.path.exists(filepath)), 'path: "{}" does not exist!'.format(filepath) + if py_controller_type not in self.pyControllers: + self.pyControllers[py_controller_type] = {} + filepath = os.path.join(path, filename) + assert os.path.exists(filepath), 'path: "{}" does not exist!'.format(filepath) for name, controller in load_data(filepath).items(): - if name in self.pyControllers[pyControllerType]: + if name in self.pyControllers[py_controller_type]: raise InvalidParameter( f"Multiple pydss controller definitions for a single OpenDSS element not allowed: {name}" ) - self.pyControllers[pyControllerType][name] = controller + self.pyControllers[py_controller_type][name] = controller def read_controller_settings_from_registry(path): @@ -48,7 +44,7 @@ def read_controller_settings_from_registry(path): # The controller settings must be stored in the pydss registry. controller_to_name = load_data(os.path.join(path, filename)) - + for controller, names in controller_to_name.items(): settings = controller_settings[controller_type].get(controller) @@ -65,30 +61,35 @@ def read_controller_settings_from_registry(path): return controllers -class pySubscriptionReader: - def __init__(self, filePath): +class PySubscriptionReader: + def __init__(self, file_path): self.SubscriptionList = {} - if not os.path.exists(filePath): - raise FileNotFoundError('path: "{}" does not exist!'.format(filePath)) + if not os.path.exists(file_path): + raise FileNotFoundError('path: "{}" does not exist!'.format(file_path)) - for elem, elem_data in load_data(filePath).items(): + for elem, elem_data in load_data(file_path).items(): if elem_data["Subscribe"]: self.SubscriptionList[elem] = elem_data -class pyExportReader: - def __init__(self, filePath): +class PyExportReader: + def __init__(self, file_path): self.pyControllers = {} self.publicationList = [] - xlsx_filename = os.path.splitext(filePath)[0] + '.xlsx' - if not os.path.exists(filePath) and os.path.exists(xlsx_filename): + xlsx_filename = os.path.splitext(file_path)[0] + ".xlsx" + if not os.path.exists(file_path) and os.path.exists(xlsx_filename): convert_config_data_to_toml(xlsx_filename) - if not os.path.exists(filePath): - raise FileNotFoundError('path: "{}" does not exist!'.format(filePath)) + if not os.path.exists(file_path): + raise FileNotFoundError('path: "{}" does not exist!'.format(file_path)) - for elem, elem_data in load_data(filePath).items(): + for elem, elem_data in load_data(file_path).items(): self.pyControllers[elem] = elem_data["Publish"][:] self.pyControllers[elem] += elem_data["NoPublish"] for item in elem_data["Publish"]: self.publicationList.append(f"{elem} {item}") + + +globals()["pyContrReader"] = PyContrReader +globals()["pySubscriptionReader"] = PySubscriptionReader +globals()["pyExportReader"] = PyExportReader diff --git a/src/pydss/pyControllers/Settings/PvControllers.toml b/src/pydss/py_controllers/Settings/PvControllers.toml similarity index 100% rename from src/pydss/pyControllers/Settings/PvControllers.toml rename to src/pydss/py_controllers/Settings/PvControllers.toml diff --git a/src/pydss/py_controllers/__init__.py b/src/pydss/py_controllers/__init__.py new file mode 100644 index 00000000..3cb81b0e --- /dev/null +++ b/src/pydss/py_controllers/__init__.py @@ -0,0 +1,3 @@ +from pydss.py_controllers import py_controller + +__all__ = ["py_controller"] diff --git a/src/pydss/pyControllers/Controllers/__init__.py b/src/pydss/py_controllers/controllers/__init__.py similarity index 100% rename from src/pydss/pyControllers/Controllers/__init__.py rename to src/pydss/py_controllers/controllers/__init__.py diff --git a/src/pydss/py_controllers/controllers/_component.py b/src/pydss/py_controllers/controllers/_component.py new file mode 100644 index 00000000..d9f8f991 --- /dev/null +++ b/src/pydss/py_controllers/controllers/_component.py @@ -0,0 +1,55 @@ +"""Shared declarations for migrated controller components.""" + +from collections.abc import Mapping +from typing import Any + +import numpy as np + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + +MEASUREMENTS = ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), +) +COMMANDS = ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("command_enabled", val=1.0), + VariableSpec("diagnostic_residual"), +) + + +class MigratedController(ControllerExplicitComponent): + """Pure controller shell with an explicit state commit boundary.""" + + def setup(self) -> None: + self._step_count = 0 + self.options["input_specs"] = self.input_specs() + self.options["output_specs"] = self.output_specs() + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return MEASUREMENTS + + def output_specs(self) -> tuple[VariableSpec, ...]: + return COMMANDS + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + active_power = float(np.asarray(inputs["measurement_active_power"])) + reactive_power = float(np.asarray(inputs["measurement_reactive_power"])) + return { + "command_active_power": settings.get("active_power", active_power), + "command_reactive_power": settings.get("reactive_power", reactive_power), + "command_enabled": float(settings.get("enabled", 1.0)), + "diagnostic_residual": 0.0, + } + + def commit_step(self) -> None: + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/dynamic_voltage_support.py b/src/pydss/py_controllers/controllers/dynamic_voltage_support.py new file mode 100644 index 00000000..1c9175fd --- /dev/null +++ b/src/pydss/py_controllers/controllers/dynamic_voltage_support.py @@ -0,0 +1,98 @@ +from collections.abc import Mapping +from typing import Any +import math + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class DynamicVoltageSupport(ControllerExplicitComponent): + """OpenMDAO component for dynamic voltage support command calculation.""" + + def setup(self) -> None: + self._state = { + "voltage": 1.0, + "active": None, + "reactive": None, + "initial_reactive": None, + "fault_present": False, + } + self._pending_state = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("command_enabled", val=1.0), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + voltage = self.input_value(inputs, "measurement_voltage_pu") + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + initial_reactive = self._state["initial_reactive"] + if initial_reactive is None: + initial_reactive = reactive + trv = max(float(settings.get("Trv", 0.001)), 1e-9) + tinv = max(float(settings.get("Tinv", 0.001)), 1e-9) + dt = float(settings.get("step_resolution_sec", 1.0)) + alpha_v = dt / (trv + dt) + alpha_i = dt / (tinv + dt) + filtered_voltage = self._state["voltage"] + alpha_v * (voltage - self._state["voltage"]) + error = filtered_voltage - 1.0 + lower_deadband = float(settings.get("dbd1", -0.1)) + upper_deadband = float(settings.get("dbd2", 0.1)) + in_deadband = lower_deadband < error < upper_deadband + fault_present = self._state["fault_present"] or not in_deadband + kvar = initial_reactive if in_deadband else reactive + if not in_deadband and bool(settings.get("capacitive_support", True)): + kvar += -error * float(settings.get("Kqv", 100.0)) + kva_rated = float(settings.get("kva", max(math.hypot(active, reactive), 1.0))) + kvar = min(float(settings.get("kvar_max", 0.44)) * kva_rated, kvar) + kvar = max(float(settings.get("kvar_min", -0.44)) * kva_rated, kvar) + previous_active = active if self._state["active"] is None else self._state["active"] + previous_reactive = reactive if self._state["reactive"] is None else self._state["reactive"] + active_command = previous_active + alpha_i * (active - previous_active) + reactive_command = previous_reactive + alpha_i * (kvar - previous_reactive) + kva_limit = kva_rated + if math.hypot(active_command, reactive_command) > kva_limit: + active_command = math.copysign( + math.sqrt(max(kva_limit**2 - reactive_command**2, 0.0)), active_command + ) + if in_deadband and fault_present and abs(reactive_command - initial_reactive) < 1e-6: + reactive_command = initial_reactive + fault_present = False + self._pending_state = { + "voltage": filtered_voltage, + "active": active_command, + "reactive": reactive_command, + "initial_reactive": initial_reactive, + "fault_present": fault_present, + } + return { + "command_active_power": active_command, + "command_reactive_power": reactive_command, + "command_enabled": 1.0, + "diagnostic_residual": error, + } + + def commit_step(self) -> None: + if self._pending_state is not None: + self._state = self._pending_state + self._pending_state = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/fault_controller.py b/src/pydss/py_controllers/controllers/fault_controller.py new file mode 100644 index 00000000..5bf44f02 --- /dev/null +++ b/src/pydss/py_controllers/controllers/fault_controller.py @@ -0,0 +1,46 @@ +from collections.abc import Mapping +from typing import Any + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class FaultController(ControllerExplicitComponent): + """OpenMDAO component that emits a time-windowed fault command.""" + + def setup(self) -> None: + self._fault_enabled = False + self._pending_fault_enabled = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return (VariableSpec("time_seconds"),) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return (VariableSpec("command_enabled", val=0.0), VariableSpec("diagnostic_residual")) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + time = self.input_value(inputs, "time_seconds") + start = float( + settings.get("start_time_seconds", settings.get("Fault start time (sec)", 0.0)) + ) + duration = float( + settings.get("duration_seconds", settings.get("Fault duration (sec)", 0.0)) + ) + enabled = start <= time < start + duration + self._pending_fault_enabled = enabled + return { + "command_enabled": float(enabled), + "diagnostic_residual": float(enabled != self._fault_enabled), + } + + def commit_step(self) -> None: + if self._pending_fault_enabled is not None: + self._fault_enabled = self._pending_fault_enabled + self._pending_fault_enabled = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/gen_controller.py b/src/pydss/py_controllers/controllers/gen_controller.py new file mode 100644 index 00000000..b9cb1fb0 --- /dev/null +++ b/src/pydss/py_controllers/controllers/gen_controller.py @@ -0,0 +1,67 @@ +from collections.abc import Mapping +from typing import Any + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class GenController(ControllerExplicitComponent): + """OpenMDAO generator controller using declared circuit measurements.""" + + def setup(self) -> None: + self._pending_reactive = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + voltage = self.input_value(inputs, "measurement_voltage_pu") + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + q_limit = abs(float(settings.get("q_limit", settings.get("QlimPU", 1.0)))) + low = float(settings.get("u_min", settings.get("uMin", 0.94))) + high = float(settings.get("u_max", settings.get("uMax", 1.06))) + db_low = float(settings.get("u_db_min", settings.get("uDbMin", 0.97))) + db_high = float(settings.get("u_db_max", settings.get("uDbMax", 1.03))) + if voltage <= low: + target = q_limit + elif voltage < db_low: + target = q_limit * (db_low - voltage) / (db_low - low) + elif voltage <= db_high: + target = 0.0 + elif voltage < high: + target = -q_limit * (voltage - db_high) / (high - db_high) + else: + target = -q_limit + damping = max( + 0.0, min(1.0, float(settings.get("damp_coef", settings.get("DampCoef", 1.0)))) + ) + command_reactive = reactive + (target - reactive) * damping + self._pending_reactive = command_reactive + return { + "command_active_power": active, + "command_reactive_power": command_reactive, + "diagnostic_residual": command_reactive - reactive, + } + + def commit_step(self) -> None: + self._pending_reactive = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/motor_stall.py b/src/pydss/py_controllers/controllers/motor_stall.py new file mode 100644 index 00000000..b8889d55 --- /dev/null +++ b/src/pydss/py_controllers/controllers/motor_stall.py @@ -0,0 +1,74 @@ +from collections.abc import Mapping +from typing import Any +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class MotorStall(ControllerExplicitComponent): + """OpenMDAO motor-stall command component.""" + + def setup(self) -> None: + self._state = "running" + self._stall_start = None + self._trip_time = None + self._pending_state = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("command_enabled", val=1.0), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + voltage = self.input_value(inputs, "measurement_voltage_pu") + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + time = self.input_value(inputs, "time_seconds") + stall_voltage = float(settings.get("v_stall", 0.55)) + protection = float(settings.get("t_protection", settings.get("t_stall", 0.95))) + reconnect = float(settings.get("t_reconnect", settings.get("t_restart", 6.0))) + state, stall_start, trip_time = self._state, self._stall_start, self._trip_time + if state == "running" and voltage < stall_voltage: + state, stall_start = "stalled", time + if state == "stalled" and stall_start is not None and time - stall_start >= protection: + state, trip_time = "tripped", time + if ( + state == "tripped" + and voltage >= float(settings.get("v_rstrt", 0.95)) + and trip_time is not None + and time - trip_time >= reconnect + ): + state, stall_start, trip_time = "running", None, None + if state == "stalled": + active *= float(settings.get("p_fault", 3.5)) + reactive *= float(settings.get("q_fault", 5.0)) + enabled = state != "tripped" + self._pending_state = (state, stall_start, trip_time) + return { + "command_active_power": active if enabled else 0.0, + "command_reactive_power": reactive if enabled else 0.0, + "command_enabled": float(enabled), + "diagnostic_residual": float(state != self._state), + } + + def commit_step(self) -> None: + if self._pending_state is not None: + self._state, self._stall_start, self._trip_time = self._pending_state + self._pending_state = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/motor_stall_backup.py b/src/pydss/py_controllers/controllers/motor_stall_backup.py new file mode 100644 index 00000000..e4282f55 --- /dev/null +++ b/src/pydss/py_controllers/controllers/motor_stall_backup.py @@ -0,0 +1,58 @@ +from collections.abc import Mapping +from typing import Any +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class MotorStallBackup(ControllerExplicitComponent): + """OpenMDAO backup motor-stall command component.""" + + def setup(self) -> None: + self._pending = None + self._state = "running" + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("command_enabled", val=1.0), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + enabled = self.input_value(inputs, "measurement_voltage_pu") >= float( + settings.get("v_stall", 0.55) + ) + state = "running" if enabled else "backup" + self._pending = state + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + if not enabled: + active *= float(settings.get("p_fault", 3.5)) + reactive *= float(settings.get("q_fault", 5.0)) + return { + "command_active_power": active if enabled else 0.0, + "command_reactive_power": reactive if enabled else 0.0, + "command_enabled": float(enabled), + "diagnostic_residual": float(state != self._state), + } + + def commit_step(self) -> None: + if self._pending is not None: + self._state = self._pending + self._pending = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/motor_stall_simple.py b/src/pydss/py_controllers/controllers/motor_stall_simple.py new file mode 100644 index 00000000..3a4a371c --- /dev/null +++ b/src/pydss/py_controllers/controllers/motor_stall_simple.py @@ -0,0 +1,97 @@ +from collections.abc import Mapping +from typing import Any + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class MotorStallSimple(ControllerExplicitComponent): + """OpenMDAO simplified motor-stall command component.""" + + def setup(self) -> None: + self._state = { + "stalled": False, + "disconnected": False, + "stall_start": None, + "disconnect_start": None, + "base_active": None, + "base_reactive": None, + } + self._pending_state = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("command_enabled", val=1.0), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + voltage = self.input_value(inputs, "measurement_voltage_pu") + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + time = self.input_value(inputs, "time_seconds") + state = self._state + base_active = active if state["base_active"] is None else state["base_active"] + base_reactive = reactive if state["base_reactive"] is None else state["base_reactive"] + stall_voltage = float(settings.get("v_stall", 0.55)) + protection_time = float(settings.get("t_protection", 0.95)) + reconnect_time = float(settings.get("t_reconnect", 6.0)) + p_fault = float(settings.get("p_fault", 3.5)) + q_fault = float(settings.get("q_fault", 5.0)) + + stalled = bool(state["stalled"]) + disconnected = bool(state["disconnected"]) + stall_start = state["stall_start"] + disconnect_start = state["disconnect_start"] + if not stalled and not disconnected and voltage < stall_voltage: + stalled, stall_start = True, time + if stalled and stall_start is not None and time - stall_start > protection_time: + stalled, disconnected, disconnect_start = False, True, time + if disconnected and disconnect_start is not None: + if time - disconnect_start > reconnect_time and voltage >= stall_voltage: + disconnected, disconnect_start = False, None + + if disconnected: + active_command = reactive_command = 0.0 + elif stalled: + active_command = base_active * p_fault + reactive_command = base_reactive * q_fault + else: + active_command, reactive_command = base_active, base_reactive + + self._pending_state = { + "stalled": stalled, + "disconnected": disconnected, + "stall_start": stall_start, + "disconnect_start": disconnect_start, + "base_active": base_active, + "base_reactive": base_reactive, + } + return { + "command_active_power": active_command, + "command_reactive_power": reactive_command, + "command_enabled": float(not disconnected), + "diagnostic_residual": 0.0, + } + + def commit_step(self) -> None: + if self._pending_state is not None: + self._state = self._pending_state + self._pending_state = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/pv_controller.py b/src/pydss/py_controllers/controllers/pv_controller.py new file mode 100644 index 00000000..6969f112 --- /dev/null +++ b/src/pydss/py_controllers/controllers/pv_controller.py @@ -0,0 +1,78 @@ +from collections.abc import Mapping +from typing import Any +import math + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class PvController(ControllerExplicitComponent): + """OpenMDAO PV controller for declared voltage and power measurements.""" + + def setup(self) -> None: + self._pending_reactive = None + self._committed_reactive = 0.0 + self._iteration_reactive = 0.0 + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return (VariableSpec("command_reactive_power"), VariableSpec("diagnostic_residual")) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + voltage = self.input_value(inputs, "measurement_voltage_pu") + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + rated_kva = float(self.options["rated_kva"]) + reactive_pu = reactive / rated_kva if rated_kva else 0.0 + u_min = float(settings.get("u_min", settings.get("uMin", 0.94))) + u_db_min = float(settings.get("u_db_min", settings.get("uDbMin", 0.97))) + u_db_max = float(settings.get("u_db_max", settings.get("uDbMax", 1.03))) + u_max = float(settings.get("u_max", settings.get("uMax", 1.06))) + q_limit = abs(float(settings.get("q_lim_pu", settings.get("QlimPU", 0.44)))) + if voltage <= u_min: + target = q_limit + elif voltage < u_db_min: + target = q_limit * (u_db_min - voltage) / (u_db_min - u_min) + elif voltage <= u_db_max: + target = 0.0 + elif voltage < u_max: + target = -q_limit * (voltage - u_db_max) / (u_max - u_db_max) + else: + target = -q_limit + damping = float(settings.get("damp_coef", settings.get("DampCoef", 0.8))) + target = reactive_pu + (target - reactive_pu) * 0.5 / damping + target += (reactive_pu - self._iteration_reactive) * 0.1 / damping + self._iteration_reactive = reactive_pu + if bool(settings.get("enable_pf_limit", settings.get("Enable PF limit", False))): + pf_limit = float(settings.get("pf_lim", settings.get("PFlim", 0.9))) + target = max( + -abs(active * math.tan(math.acos(pf_limit))), + min(abs(active * math.tan(math.acos(pf_limit))), target), + ) + command_reactive = target * rated_kva + self._pending_reactive = command_reactive + return { + "command_reactive_power": command_reactive, + "diagnostic_residual": command_reactive - reactive, + } + + def commit_step(self) -> None: + self._committed_reactive = self._iteration_reactive + self._pending_reactive = None + self._step_count += 1 + + def reset_step(self) -> None: + self._iteration_reactive = self._committed_reactive + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/pv_dynamic.py b/src/pydss/py_controllers/controllers/pv_dynamic.py new file mode 100644 index 00000000..e945e8d5 --- /dev/null +++ b/src/pydss/py_controllers/controllers/pv_dynamic.py @@ -0,0 +1,58 @@ +from collections.abc import Mapping +from typing import Any +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class PvDynamic(ControllerExplicitComponent): + """OpenMDAO boundary for an external dynamic PV model.""" + + def setup(self) -> None: + self._state = {"active": None, "reactive": None} + self._pending_state = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + if not hasattr(self, "_state"): + self._state = {"active": None, "reactive": None} + self._pending_state = None + self._step_count = 0 + settings = self.options["settings"] or {} + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + response = max(0.0, min(1.0, float(settings.get("response_fraction", 1.0)))) + previous_active = active if self._state["active"] is None else self._state["active"] + previous_reactive = reactive if self._state["reactive"] is None else self._state["reactive"] + command_active = previous_active + response * (active - previous_active) + command_reactive = previous_reactive + response * (reactive - previous_reactive) + self._pending_state = {"active": command_active, "reactive": command_reactive} + return { + "command_active_power": command_active, + "command_reactive_power": command_reactive, + "diagnostic_residual": command_active - active, + } + + def commit_step(self) -> None: + if self._pending_state is not None: + self._state = self._pending_state + self._pending_state = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/pv_frequency_ride_thru.py b/src/pydss/py_controllers/controllers/pv_frequency_ride_thru.py new file mode 100644 index 00000000..69e5f1df --- /dev/null +++ b/src/pydss/py_controllers/controllers/pv_frequency_ride_thru.py @@ -0,0 +1,72 @@ +from collections.abc import Mapping +from typing import Any +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class PvFrequencyRideThru(ControllerExplicitComponent): + """OpenMDAO PV frequency ride-through component.""" + + def setup(self) -> None: + self._connected = True + self._violation_start = None + self._pending_state = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_frequency_hz"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("command_enabled", val=1.0), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + if not hasattr(self, "_violation_start"): + self._connected = True + self._violation_start = None + self._pending_state = None + self._step_count = 0 + settings = self.options["settings"] or {} + frequency = self.input_value(inputs, "measurement_frequency_hz") + time = self.input_value(inputs, "time_seconds") + lower = float(settings.get("continuous_f_lower_hz", settings.get("f_lower", 59.3))) + upper = float(settings.get("continuous_f_upper_hz", settings.get("f_upper", 60.5))) + delay = float(settings.get("trip_delay_sec", settings.get("trip_deadtime_sec", 0.0))) + violation_start = self._violation_start + if lower <= frequency <= upper: + violation_start = None + elif violation_start is None: + violation_start = time + connected = self._connected and not ( + violation_start is not None and time - violation_start >= delay + ) + self._pending_state = (connected, violation_start) + return { + "command_active_power": self.input_value(inputs, "measurement_active_power") + if connected + else 0.0, + "command_reactive_power": self.input_value(inputs, "measurement_reactive_power") + if connected + else 0.0, + "command_enabled": float(connected), + "diagnostic_residual": float(connected != self._connected), + } + + def commit_step(self) -> None: + if self._pending_state is not None: + self._connected, self._violation_start = self._pending_state + self._pending_state = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/pv_voltage_ride_thru.py b/src/pydss/py_controllers/controllers/pv_voltage_ride_thru.py new file mode 100644 index 00000000..a6f80f46 --- /dev/null +++ b/src/pydss/py_controllers/controllers/pv_voltage_ride_thru.py @@ -0,0 +1,116 @@ +from collections.abc import Mapping +from typing import Any + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class PvVoltageRideThru(ControllerExplicitComponent): + """OpenMDAO PV voltage ride-through component.""" + + def setup(self) -> None: + self._state = { + "connected": True, + "violation_start": None, + "reconnect_start": None, + "last_time": None, + } + self._pending_state = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_voltage_pu"), + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("command_enabled", val=1.0), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + voltage = self.input_value(inputs, "measurement_voltage_pu") + active = self.input_value(inputs, "measurement_active_power") + reactive = self.input_value(inputs, "measurement_reactive_power") + time = self.input_value(inputs, "time_seconds") + state = self._state + + uv2 = float(settings.get("uv_2_pu", 0.45)) + uv1 = float(settings.get("uv_1_pu", 0.70)) + ov1 = float(settings.get("ov_1_pu", 1.10)) + ov2 = float(settings.get("ov_2_pu", 1.20)) + uv2_time = float(settings.get("uv_2_ct_sec", 0.16)) + uv1_time = float(settings.get("uv_1_ct_sec", 2.0)) + ov1_time = float(settings.get("ov_1_ct_sec", 2.0)) + ov2_time = float(settings.get("ov_2_ct_sec", 0.16)) + deadtime = float(settings.get("reconnect_deadtime_sec", 300.0)) + float(settings.get("reconnect_pmax_time_sec", 300.0)) + + if voltage < uv2 or voltage > ov2: + trip_delay = uv2_time if voltage < uv2 else ov2_time + region = "trip" + elif voltage < uv1 or voltage > ov1: + trip_delay = uv1_time if voltage < uv1 else ov1_time + region = "ride_through" + else: + trip_delay = None + region = "normal" + + violation_start = state["violation_start"] + if region != "normal" and violation_start is None: + violation_start = time + if region == "normal": + violation_start = None + + connected = bool(state["connected"]) + reconnect_start = state["reconnect_start"] + if connected and region == "trip" and violation_start is not None: + connected = time - violation_start < trip_delay + if not connected: + reconnect_start = time + elif not connected and region == "normal": + if reconnect_start is None: + reconnect_start = time + connected = time - reconnect_start >= deadtime + elif connected: + reconnect_start = None + + if not connected: + active_command = reactive_command = 0.0 + elif region == "ride_through": + active_command = 0.0 if voltage < uv1 or voltage > ov1 else active + reactive_command = reactive + else: + active_command, reactive_command = active, reactive + + pending = dict(state) + pending.update( + connected=connected, + violation_start=violation_start, + reconnect_start=reconnect_start, + last_time=time, + ) + self._pending_state = pending + return { + "command_active_power": active_command, + "command_reactive_power": reactive_command, + "command_enabled": float(connected), + "diagnostic_residual": 0.0, + } + + def commit_step(self) -> None: + if self._pending_state is not None: + self._state = self._pending_state + self._pending_state = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/pyControllers/Controllers/Settings/DynamicVoltageSupport.toml b/src/pydss/py_controllers/controllers/settings/DynamicVoltageSupport.toml similarity index 100% rename from src/pydss/pyControllers/Controllers/Settings/DynamicVoltageSupport.toml rename to src/pydss/py_controllers/controllers/settings/DynamicVoltageSupport.toml diff --git a/src/pydss/pyControllers/Controllers/Settings/PvControllers.toml b/src/pydss/py_controllers/controllers/settings/PvControllers.toml similarity index 100% rename from src/pydss/pyControllers/Controllers/Settings/PvControllers.toml rename to src/pydss/py_controllers/controllers/settings/PvControllers.toml diff --git a/src/pydss/pyControllers/Controllers/Settings/VoltageRideThru.toml b/src/pydss/py_controllers/controllers/settings/VoltageRideThru.toml similarity index 100% rename from src/pydss/pyControllers/Controllers/Settings/VoltageRideThru.toml rename to src/pydss/py_controllers/controllers/settings/VoltageRideThru.toml diff --git a/src/pydss/defaults/ExportLists/__init__.py b/src/pydss/py_controllers/controllers/settings/__init__.py similarity index 100% rename from src/pydss/defaults/ExportLists/__init__.py rename to src/pydss/py_controllers/controllers/settings/__init__.py diff --git a/src/pydss/py_controllers/controllers/socket_controller.py b/src/pydss/py_controllers/controllers/socket_controller.py new file mode 100644 index 00000000..1e889ea6 --- /dev/null +++ b/src/pydss/py_controllers/controllers/socket_controller.py @@ -0,0 +1,49 @@ +from collections.abc import Mapping +from typing import Any +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class SocketController(ControllerExplicitComponent): + """Explicit socket command boundary; I/O belongs outside nonlinear trials.""" + + def setup(self) -> None: + self._pending = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + active = float( + settings.get("active_power", self.input_value(inputs, "measurement_active_power")) + ) + reactive = float( + settings.get("reactive_power", self.input_value(inputs, "measurement_reactive_power")) + ) + self._pending = (active, reactive) + return { + "command_active_power": active, + "command_reactive_power": reactive, + "diagnostic_residual": 0.0, + } + + def commit_step(self) -> None: + self._pending = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/storage_controller.py b/src/pydss/py_controllers/controllers/storage_controller.py new file mode 100644 index 00000000..51d70679 --- /dev/null +++ b/src/pydss/py_controllers/controllers/storage_controller.py @@ -0,0 +1,75 @@ +from collections.abc import Mapping +from typing import Any + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class StorageController(ControllerExplicitComponent): + """OpenMDAO storage dispatch component with committed step state.""" + + def setup(self) -> None: + self._previous_active = 0.0 + self._pending_active = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("measurement_active_power"), + VariableSpec("measurement_reactive_power"), + VariableSpec("time_seconds"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("command_active_power"), + VariableSpec("command_reactive_power"), + VariableSpec("diagnostic_residual"), + ) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + measured = self.input_value(inputs, "measurement_active_power") + control = str(settings.get("control", settings.get("mode", "None"))).lower() + if control in {"rt", "realtime", "real_time"}: + target = float(settings.get("kw_out", settings.get("%kWOut", measured))) + elif control in {"sh", "scheduled", "schedule"}: + schedule = settings.get("schedule", settings.get("Schedule", ())) + index = ( + min( + len(schedule) - 1, + max( + 0, + int( + self.input_value(inputs, "time_seconds") + / max(float(settings.get("schedule_period_sec", 3600.0)), 1.0) + ), + ), + ) + if schedule + else 0 + ) + target = float(schedule[index]) if schedule else measured + elif control in {"ps", "peak_shaving", "peakshaving"}: + upper = float(settings.get("ps_ub", settings.get("PS_ub", measured))) + target = measured - max(0.0, measured - upper) * float( + settings.get("damp_coef", settings.get("DampCoef", 1.0)) + ) + else: + target = float(settings.get("active_power", measured)) + self._pending_active = target + return { + "command_active_power": target, + "command_reactive_power": self.input_value(inputs, "measurement_reactive_power"), + "diagnostic_residual": target - self._previous_active, + } + + def commit_step(self) -> None: + if self._pending_active is not None: + self._previous_active = self._pending_active + self._pending_active = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/thermostatic_load.py b/src/pydss/py_controllers/controllers/thermostatic_load.py new file mode 100644 index 00000000..17ab9a4b --- /dev/null +++ b/src/pydss/py_controllers/controllers/thermostatic_load.py @@ -0,0 +1,66 @@ +from collections.abc import Mapping +from typing import Any +import math + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class ThermostaticLoad(ControllerExplicitComponent): + """OpenMDAO thermostatic load command component.""" + + def setup(self) -> None: + settings = self.options["settings"] or {} + self._temperature = float( + settings.get( + "initial_temperature", + (float(settings.get("Tmin", 18.0)) + float(settings.get("Tmax", 30.0))) / 2.0, + ) + ) + self._on = bool(settings.get("initial_on", True)) + self._pending_state = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return ( + VariableSpec("time_seconds"), + VariableSpec("measurement_active_power"), + ) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return (VariableSpec("command_active_power"), VariableSpec("diagnostic_residual")) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + t_min = float(settings.get("t_min", settings.get("Tmin", 18.0))) + t_max = float(settings.get("t_max", settings.get("Tmax", 30.0))) + resistance = max(float(settings.get("r", settings.get("R", 1.0))), 1e-9) + capacitance = max(float(settings.get("c", settings.get("C", 1.0))), 1e-9) + ambient = 30.0 + 10.0 * math.sin( + self.input_value(inputs, "time_seconds") * 2.0 * math.pi / 86400.0 + ) + rated = float(settings.get("kw", self.input_value(inputs, "measurement_active_power"))) + delta = -(self._temperature - ambient) / (resistance * capacitance) + if self._on: + delta -= float(settings.get("mu", 0.0)) * rated / capacitance + temperature = self._temperature + delta + on = self._on + if temperature > t_max: + on = True + elif temperature < t_min: + on = False + self._pending_state = (temperature, on) + return { + "command_active_power": rated if on else 0.0, + "diagnostic_residual": temperature - self._temperature, + } + + def commit_step(self) -> None: + if self._pending_state is not None: + self._temperature, self._on = self._pending_state + self._pending_state = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count diff --git a/src/pydss/py_controllers/controllers/xfmr_controller.py b/src/pydss/py_controllers/controllers/xfmr_controller.py new file mode 100644 index 00000000..8a3f5b16 --- /dev/null +++ b/src/pydss/py_controllers/controllers/xfmr_controller.py @@ -0,0 +1,49 @@ +from collections.abc import Mapping +from typing import Any + +from pydss.openmdao_components import ControllerExplicitComponent, VariableSpec + + +class XfmrController(ControllerExplicitComponent): + """OpenMDAO transformer regulator command component.""" + + def setup(self) -> None: + self._locked = False + self._pending_locked = None + self._step_count = 0 + super().setup() + + def input_specs(self) -> tuple[VariableSpec, ...]: + return (VariableSpec("measurement_active_power"), VariableSpec("time_seconds")) + + def output_specs(self) -> tuple[VariableSpec, ...]: + return (VariableSpec("command_enabled", val=1.0), VariableSpec("diagnostic_residual")) + + def compute_commands(self, inputs) -> Mapping[str, Any]: + settings = self.options["settings"] or {} + reverse_power_locking = bool( + settings.get("rpf_locking", settings.get("RPF locking", False)) + ) + power = self.input_value(inputs, "measurement_active_power") + locked = reverse_power_locking and power < 0.0 + self._pending_locked = locked + return { + "command_enabled": float(not locked), + "diagnostic_residual": float(locked != self._locked), + } + + def commit_step(self) -> None: + if self._pending_locked is not None: + self._locked = self._pending_locked + self._pending_locked = None + self._step_count += 1 + + @property + def step_count(self) -> int: + return self._step_count + + +def __getattr__(name: str): + if name == "xfmrController": + return XfmrController + raise AttributeError(name) diff --git a/src/pydss/pyControllers/enumerations.py b/src/pydss/py_controllers/enumerations.py similarity index 76% rename from src/pydss/pyControllers/enumerations.py rename to src/pydss/py_controllers/enumerations.py index 09712133..abc703dd 100644 --- a/src/pydss/pyControllers/enumerations.py +++ b/src/pydss/py_controllers/enumerations.py @@ -1,21 +1,23 @@ from enum import Enum + class VoltageCalcModes(str, Enum): """ Voltage calculation modes for the controller - + **MAX** - *The the maximum voltage from the available phases as the contoller input* - + **AVG** - *The the average voltage from the available phases as the contoller input* - + **MIN** - *The the minimum voltage from the available phases as the contoller input* - + **A** - *The voltage from phase A as the contoller input* - + **B** - *The voltage from phase B as the contoller input* - + **C** - *The voltage from phase C as the contoller input* """ + MAX = "Max" AVG = "Avg" MIN = "Min" @@ -23,85 +25,97 @@ class VoltageCalcModes(str, Enum): B = "2" C = "3" + class PvStandard(str, Enum): """ PV standards for the controller - + **IEEE_1547_2003** - *For legacy PV systems, use the IEEE 1574-2003 standard* - + **IEEE_1547_2018** - *For legacy PV systems with smart controls, use the IEEE 1574-2018 standard* """ - IEEE_1547_2003 = "1547-2003" + + IEEE_1547_2003 = "1547-2003" IEEE_1547_2018 = "1547-2018" + class RideThroughCategory(str, Enum): """ PV ride-through catregories - + **CATEGORY_I** - *Intended to meet minimum Bulk EPS reliability needs and to be achievable by all DER technologies, including rotating machines* - + **CATEGORY_II** - *Designed to align with the requirements in NERC PRC-024-2* - + **CATEGORY_III** - *Designed to meet the needs of low-inertia or highly-penetrated grids* """ + CATEGORY_I = "Category I" - CATEGORY_II = "Category II" - CATEGORY_III = "Category III" + CATEGORY_II = "Category II" + CATEGORY_III = "Category III" + class PermissiveOperation(str, Enum): """ Possible behaviors for permissive operation - + **CURRENT_LIMITED** - *Current injection into the grid is limited by the inverter during low or high voltage conditions outside the continuous operating range* - + **MOMENTARY_SUCESSION** - *No current is injected into the grid by the inverter during low or high voltage conditions outside the continuous operating range* """ + CURRENT_LIMITED = "Current limited" MOMENTARY_SUCESSION = "Momentary sucession" + class MayTripOperation(str, Enum): """ Possible behaviors for my trip region - + **TRIP** - *PV system disconnects from the grid. Can not reconnect for atleast 300 seconds.* - + **PERMISSIVE_OPERATION** - *Option for the DER to either continue to exchange current with or to cease toenergize an EPS* """ - TRIP = "Trip" - PERMISSIVE_OPERATION ="Permissive operation" - + + TRIP = "Trip" + PERMISSIVE_OPERATION = "Permissive operation" + + class MultipleDisturbances(str, Enum): """ Possible behaviors for multiple disturbamces - + **TRIP** - *PV system disconnects from the grid. Can not reconnect for atleast 300 seconds.* - + **PERMISSIVE_OPERATION** - *Option for the DER to either continue to exchange current with or to cease toenergize an EPS* """ - TRIP = "Trip" - PERMISSIVE_OPERATION = "Permissive operation" - + + TRIP = "Trip" + PERMISSIVE_OPERATION = "Permissive operation" + + class CategoryI(float, Enum): """ Variable defination for Category I - + **OV2_PU** - *Upper bound for the over-voltage region* - + **OV2_CT_SEC** - *Trip time if voltage exceeds OV2_PU (seconds)* - - **OV1_PU** - *Lower bound for the over-voltage region* - + + **OV1_PU** - *Lower bound for the over-voltage region* + **OV1_CT_SEC** - *Trip time if voltage exceeds OV1_CT_SEC (seconds)* - - **UV1_PU** - *Upper bound for the under-voltage region* - + + **UV1_PU** - *Upper bound for the under-voltage region* + **UV1_CT_SEC** - *Trip time if voltage is less than UV1_PU (seconds)* - + **UV2_PU** - *Lower bound for the under-voltage region* - + **UV2_CT_SEC** - *Trip time if voltage is less than UV1_PU (seconds)* """ - OV2_PU = 1.2 + + OV2_PU = 1.2 OV2_CT_SEC = 0.16 OV1_PU = 1.1 OV1_CT_SEC = 2.0 @@ -110,26 +124,28 @@ class CategoryI(float, Enum): UV2_PU = 0.45 UV2_CT_SEC = 0.16 + class CategoryII(float, Enum): """ Variable defination for Category II - + **OV2_PU** - *Upper bound for the over-voltage region* - + **OV2_CT_SEC** - *Trip time if voltage exceeds OV2_PU (seconds)* - - **OV1_PU** - *Lower bound for the over-voltage region* - + + **OV1_PU** - *Lower bound for the over-voltage region* + **OV1_CT_SEC** - *Trip time if voltage exceeds OV1_CT_SEC (seconds)* - - **UV1_PU** - *Upper bound for the under-voltage region* - + + **UV1_PU** - *Upper bound for the under-voltage region* + **UV1_CT_SEC** - *Trip time if voltage is less than UV1_PU (seconds)* - + **UV2_PU** - *Lower bound for the under-voltage region* - + **UV2_CT_SEC** - *Trip time if voltage is less than UV1_PU (seconds)* """ + OV2_PU = 1.2 OV2_CT_SEC = 0.16 OV1_PU = 1.1 @@ -139,26 +155,28 @@ class CategoryII(float, Enum): UV2_PU = 0.45 UV2_CT_SEC = 0.16 + class CategoryIII(float, Enum): """ Variable defination for Category III - + **OV2_PU** - *Upper bound for the over-voltage region* - + **OV2_CT_SEC** - *Trip time if voltage exceeds OV2_PU (seconds)* - - **OV1_PU** - *Lower bound for the over-voltage region* - + + **OV1_PU** - *Lower bound for the over-voltage region* + **OV1_CT_SEC** - *Trip time if voltage exceeds OV1_CT_SEC (seconds)* - - **UV1_PU** - *Upper bound for the under-voltage region* - + + **UV1_PU** - *Upper bound for the under-voltage region* + **UV1_CT_SEC** - *Trip time if voltage is less than UV1_PU (seconds)* - + **UV2_PU** - *Lower bound for the under-voltage region* - + **UV2_CT_SEC** - *Trip time if voltage is less than UV1_PU (seconds)* """ + OV2_PU = 1.2 OV2_CT_SEC = 0.16 OV1_PU = 1.1 @@ -168,57 +186,42 @@ class CategoryIII(float, Enum): UV2_PU = 0.5 UV2_CT_SEC = 2.0 + class SmartControls(str, Enum): """ Supported smart control algorithms - + **NONE** - *No contol algorithm* - + **CONSTANT_POWER_FACTOR** - *Constant power factor implmentation* - - **VARIABLE_POWER_FACTOR** - *Variable power factor implmentation* - + + **VARIABLE_POWER_FACTOR** - *Variable power factor implmentation* + **VOLT_VAR** - *Volt / Var algorithm implementation* - - **VOLT_WATT** - *Volt / Watt algorithm implementation* - + + **VOLT_WATT** - *Volt / Watt algorithm implementation* + **TRIP** - *Over voltage trip implementation* - - """ - - NONE = 'None' - CONSTANT_POWER_FACTOR ='cpf' - VARIABLE_POWER_FACTOR ='vpf' - VOLT_VAR ='VVar' - VOLT_WATT ='vwatt' - TRIP = 'trip' - -class ControlPriority(str, Enum): - """ - Variable to prooritize at inverter capability limit - - **VAR** - *Var priority* - - **WATT** - *Watt priority* - - **PF** - *Powerfactor priority* - - """ - - VAR = 'Var' - WATT ='Watt' - PF = "PF" + + """ + + NONE = "None" + CONSTANT_POWER_FACTOR = "cpf" + VARIABLE_POWER_FACTOR = "vpf" + VOLT_VAR = "VVar" + VOLT_WATT = "vwatt" + TRIP = "trip" class VoltWattCurtailmentStrategy(str, Enum): """ Curtailment strategy for volt / watt algorithm - + **AVAILABLE_POWER** - *Curtailment is based on available power of the inverter* - + **RAETED_POWER** - *Curtailment is based on rated power of the inverter* - + """ - - AVAILABLE_POWER = 'Available Power' - RAETED_POWER ='Rated Power' + + AVAILABLE_POWER = "Available Power" + RAETED_POWER = "Rated Power" diff --git a/src/pydss/pyControllers/models.py b/src/pydss/py_controllers/models.py similarity index 55% rename from src/pydss/pyControllers/models.py rename to src/pydss/py_controllers/models.py index cae7cb20..e4cf3629 100644 --- a/src/pydss/pyControllers/models.py +++ b/src/pydss/py_controllers/models.py @@ -1,296 +1,399 @@ -from typing import Union, Annotated +from typing import Annotated from pydantic import BaseModel, Field, model_validator -from pydss.pyControllers.enumerations import ( - CategoryI, - CategoryII, - CategoryIII, - PvStandard, - VoltageCalcModes, - RideThroughCategory, - PermissiveOperation, - MayTripOperation, +from pydss.py_controllers.enumerations import ( + CategoryI, + CategoryII, + CategoryIII, + PvStandard, + VoltageCalcModes, + RideThroughCategory, + PermissiveOperation, + MayTripOperation, MultipleDisturbances, - SmartControls, - ControlPriority, - VoltWattCurtailmentStrategy, + VoltWattCurtailmentStrategy, ) - class BaseControllerModel(BaseModel): ... - - + + @model_validator(mode="before") + @classmethod + def reject_legacy_controller_settings(cls, values): + if isinstance(values, dict): + legacy = sorted( + key for key in values if key in {"Control1", "Control2", "Control3", "Priority"} + ) + if legacy: + raise ValueError( + "Legacy controller settings are unsupported; use component settings and OpenMDAO variables: " + + ", ".join(legacy) + ) + return values + + class PvVoltageRideThruModel(BaseControllerModel): """Data model for the PV voltage ride through controller""" - - kva : Annotated[ + + kva: Annotated[ float, Field(4.0, ge=0.0, description="kVA capacity of the inverter (AC-side)."), - ] + ] max_kw: Annotated[ float, Field(4.0, ge=0.0, description="kW capacity of the PV system (DC-side)."), - ] + ] voltage_calc_mode: Annotated[ VoltageCalcModes, Field( - VoltageCalcModes.MAX, - description="Voltage values used to calculate Var support from the inverter (Maximum or Average)."), - ] + VoltageCalcModes.MAX, + description="Voltage values used to calculate Var support from the inverter (Maximum or Average).", + ), + ] follow_standard: Annotated[ PvStandard, Field(PvStandard.IEEE_1547_2018, description="IEEE standard the inverter is following."), - ] - ride_through_category : Annotated[ + ] + ride_through_category: Annotated[ RideThroughCategory, - Field(RideThroughCategory.CATEGORY_I, description="PV ride-through category fot the inverter (see IEEE 1547-2018 std for more information)."), - ] + Field( + RideThroughCategory.CATEGORY_I, + description="PV ride-through category fot the inverter (see IEEE 1547-2018 std for more information).", + ), + ] ov_2_pu: Annotated[ float, Field(CategoryI.OV2_PU.value, description="Upper bound for the over-voltage region."), - ] + ] ov_2_ct_sec: Annotated[ float, - Field(CategoryI.OV2_CT_SEC.value, description="Trip duration setting if the upper bound of the over-voltage region is violated."), - ] + Field( + CategoryI.OV2_CT_SEC.value, + description="Trip duration setting if the upper bound of the over-voltage region is violated.", + ), + ] ov_1_pu: Annotated[ float, Field(CategoryI.OV1_PU.value, description="Lower bound for the over-voltage region."), - ] + ] ov_1_ct_sec: Annotated[ float, - Field(CategoryI.OV1_CT_SEC.value, description="Trip duration setting if the lower bound of the over-voltage region is violated."), - ] + Field( + CategoryI.OV1_CT_SEC.value, + description="Trip duration setting if the lower bound of the over-voltage region is violated.", + ), + ] uv_1_pu: Annotated[ float, Field(CategoryI.UV1_PU.value, description="Upper bound for the under-voltage region."), - ] + ] uv_1_ct_sec: Annotated[ float, - Field(CategoryI.UV1_CT_SEC.value, description="Trip duration setting if the upper bound of the under-voltage region is violated."), - ] - uv_2_pu: Annotated[ + Field( + CategoryI.UV1_CT_SEC.value, + description="Trip duration setting if the upper bound of the under-voltage region is violated.", + ), + ] + uv_2_pu: Annotated[ float, Field(CategoryI.UV2_PU.value, description="Lower bound for the under-voltage region."), - ] + ] uv_2_ct_sec: Annotated[ float, - Field(CategoryI.UV2_CT_SEC.value, description="Trip duration setting if the upper bound of the under-voltage region is violated."), - ] - reconnect_deadtime_sec : Annotated[ + Field( + CategoryI.UV2_CT_SEC.value, + description="Trip duration setting if the upper bound of the under-voltage region is violated.", + ), + ] + reconnect_deadtime_sec: Annotated[ float, Field(3000.0, ge=0.0, description=""), - ] - reconnect_pmax_time_sec : Annotated[ + ] + reconnect_pmax_time_sec: Annotated[ float, - Field(300.0, ge=0.0, description="Reconnect after a trip event. PV system will connect back once this time has elapsed and the system voltage is within bounds."), - ] + Field( + 300.0, + ge=0.0, + description="Reconnect after a trip event. PV system will connect back once this time has elapsed and the system voltage is within bounds.", + ), + ] permissive_operation: Annotated[ PermissiveOperation, - Field(PermissiveOperation.CURRENT_LIMITED, description="Defines behavior of the system within the 'permissive operation' region. (see IEEE 1547-2018 std for more information)."), - ] + Field( + PermissiveOperation.CURRENT_LIMITED, + description="Defines behavior of the system within the 'permissive operation' region. (see IEEE 1547-2018 std for more information).", + ), + ] may_trip_operation: Annotated[ MayTripOperation, - Field(MayTripOperation.TRIP, description="Defines behavior of the system within the 'may trip' region. (see IEEE 1547-2018 std for more information)."), - ] + Field( + MayTripOperation.TRIP, + description="Defines behavior of the system within the 'may trip' region. (see IEEE 1547-2018 std for more information).", + ), + ] multiple_disturdances: Annotated[ MultipleDisturbances, - Field(MultipleDisturbances.TRIP, description="Defines behavior of the system after multiple disturbances. (see IEEE 1547-2018 std for more information)."), - ] + Field( + MultipleDisturbances.TRIP, + description="Defines behavior of the system after multiple disturbances. (see IEEE 1547-2018 std for more information).", + ), + ] - @model_validator(mode='after') - def update_settings(self) -> 'PvVoltageRideThruModel': + @model_validator(mode="after") + def update_settings(self) -> "PvVoltageRideThruModel": cat1 = self.ride_through_category == RideThroughCategory.CATEGORY_I cat2 = self.ride_through_category == RideThroughCategory.CATEGORY_II - - self.ov_2_pu = CategoryI.OV2_PU.value if cat1 else CategoryII.OV2_PU.value if cat2 else CategoryIII.OV2_PU.value - self.ov_1_pu = CategoryI.OV1_PU.value if cat1 else CategoryII.OV1_PU.value if cat2 else CategoryIII.OV1_PU.value - self.uv_2_pu = CategoryI.UV2_PU.value if cat1 else CategoryII.UV2_PU.value if cat2 else CategoryIII.UV2_PU.value - self.uv_1_pu = CategoryI.UV1_PU.value if cat1 else CategoryII.UV1_PU.value if cat2 else CategoryIII.UV1_PU.value - self.ov_2_ct_sec = CategoryI.OV2_CT_SEC.value if cat1 else CategoryII.OV2_CT_SEC.value if cat2 else CategoryIII.OV2_CT_SEC.value - self.ov_1_ct_sec = CategoryI.OV1_CT_SEC.value if cat1 else CategoryII.OV1_CT_SEC.value if cat2 else CategoryIII.OV1_CT_SEC.value - self.uv_2_ct_sec = CategoryI.UV2_CT_SEC.value if cat1 else CategoryII.UV2_CT_SEC.value if cat2 else CategoryIII.UV2_CT_SEC.value - self.uv_1_ct_sec = CategoryI.UV1_CT_SEC.value if cat1 else CategoryII.UV1_CT_SEC.value if cat2 else CategoryIII.UV1_CT_SEC.value - - return self + self.ov_2_pu = ( + CategoryI.OV2_PU.value + if cat1 + else CategoryII.OV2_PU.value + if cat2 + else CategoryIII.OV2_PU.value + ) + self.ov_1_pu = ( + CategoryI.OV1_PU.value + if cat1 + else CategoryII.OV1_PU.value + if cat2 + else CategoryIII.OV1_PU.value + ) + self.uv_2_pu = ( + CategoryI.UV2_PU.value + if cat1 + else CategoryII.UV2_PU.value + if cat2 + else CategoryIII.UV2_PU.value + ) + self.uv_1_pu = ( + CategoryI.UV1_PU.value + if cat1 + else CategoryII.UV1_PU.value + if cat2 + else CategoryIII.UV1_PU.value + ) + self.ov_2_ct_sec = ( + CategoryI.OV2_CT_SEC.value + if cat1 + else CategoryII.OV2_CT_SEC.value + if cat2 + else CategoryIII.OV2_CT_SEC.value + ) + self.ov_1_ct_sec = ( + CategoryI.OV1_CT_SEC.value + if cat1 + else CategoryII.OV1_CT_SEC.value + if cat2 + else CategoryIII.OV1_CT_SEC.value + ) + self.uv_2_ct_sec = ( + CategoryI.UV2_CT_SEC.value + if cat1 + else CategoryII.UV2_CT_SEC.value + if cat2 + else CategoryIII.UV2_CT_SEC.value + ) + self.uv_1_ct_sec = ( + CategoryI.UV1_CT_SEC.value + if cat1 + else CategoryII.UV1_CT_SEC.value + if cat2 + else CategoryIII.UV1_CT_SEC.value + ) + + return self class PvSmartController(BaseControllerModel): kvar_limit: Annotated[ float, Field(1.76, ge=0.0, description="kVar capacity of the PV system."), - ] + ] pct_p_cutin: Annotated[ float, - Field(10.0, ge=0.0, le=100.0, description="Percentage of kVA rating of inverter. When the inverter is OFF, the power from the system must be greater than this for the inverter to turn on"), - ] + Field( + 10.0, + ge=0.0, + le=100.0, + description="Percentage of kVA rating of inverter. When the inverter is OFF, the power from the system must be greater than this for the inverter to turn on", + ), + ] pct_p_cutout: Annotated[ float, - Field(10.0, ge=0.0, le=100.0, description="Percentage of kVA rating of inverter. When the inverter is ON, the inverter turns OFF when the power from the array drops below this value."), - ] + Field( + 10.0, + ge=0.0, + le=100.0, + description="Percentage of kVA rating of inverter. When the inverter is ON, the inverter turns OFF when the power from the array drops below this value.", + ), + ] enable_pf_limit: Annotated[ bool, Field(False, description="Enable flag to apply power factor limits on the inverter output"), - ] + ] pf_min: Annotated[ float, - Field(0.95, ge=0.0, le=1.0, description="Minimum allowable powerfactor for the system. 'enable_pf_limit' should be enable for the constraint to be implemented."), - ] - + Field( + 0.95, + ge=0.0, + le=1.0, + description="Minimum allowable powerfactor for the system. 'enable_pf_limit' should be enable for the constraint to be implemented.", + ), + ] + + class MotorStallSimpleSettings(BaseControllerModel): p_fault: Annotated[ float, Field(3.5, ge=3.0, le=5.0, description="Active power multiplier post fault."), - ] + ] q_fault: Annotated[ float, Field(5.0, ge=3.0, le=7.0, description="Reactive power multiplier post fault."), - ] + ] v_stall: Annotated[ float, - Field(0.55, ge=0.53, le=0.58, description="Per unit voltage below which the motor will stall."), - ] + Field( + 0.55, ge=0.53, le=0.58, description="Per unit voltage below which the motor will stall." + ), + ] t_protection: Annotated[ float, - Field(0.95, ge=0.0, le=15.0, description="Time [sec] after stall the motor will disconnect."), - ] + Field( + 0.95, ge=0.0, le=15.0, description="Time [sec] after stall the motor will disconnect." + ), + ] t_reconnect: Annotated[ float, - Field(6.0, ge=5.0, le=7.0, description="Time duration [sec] after which the motor will reconnect."), - ] - + Field( + 6.0, + ge=5.0, + le=7.0, + description="Time duration [sec] after which the motor will reconnect.", + ), + ] + class MotorStallSettings(BaseControllerModel): - k_p1: Annotated[ + k_p1: Annotated[ float, - Field(0 , description="Real power constant for running state 111"), - ] - n_p1: Annotated[ + Field(0, description="Real power constant for running state 111"), + ] + n_p1: Annotated[ float, Field(1.0, description="Real power exponent for running state 1"), - ] - k_p2: Annotated[ + ] + k_p2: Annotated[ float, Field(12.0, description="Real power constant for running state 2"), - ] - n_p2: Annotated[ + ] + n_p2: Annotated[ float, Field(3.2, description="Real power exponent for running state 2"), - ] - k_q1: Annotated[ + ] + k_q1: Annotated[ float, Field(6.0, description="Reactive power constant for running state 1"), - ] - n_q1: Annotated[ + ] + n_q1: Annotated[ float, Field(2.0, description="Reactive power exponent for running state 1"), - ] - k_q2: Annotated[ + ] + k_q2: Annotated[ float, Field(11.0, description="Reactive power constant for running state 2"), - ] - n_q2: Annotated[ + ] + n_q2: Annotated[ float, Field(2.5, description="Reactive power exponent for running state 2."), - ] - t_th: Annotated[ + ] + t_th: Annotated[ float, - Field(4.0, description="Varies based on manufacturer and external factors - sensitivity analysis required"), - ] - f_rst: Annotated[ + Field( + 4.0, + description="Varies based on manufacturer and external factors - sensitivity analysis required", + ), + ] + f_rst: Annotated[ float, - Field(0.2, description="Captures diversity in load; also based on testing (fraction of motors capable of restart)."), - ] - lf_adj: Annotated[ + Field( + 0.2, + description="Captures diversity in load; also based on testing (fraction of motors capable of restart).", + ), + ] + lf_adj: Annotated[ float, Field(0.0, description="Load factor adjustment to the stall voltage10"), - ] - t_th1t: Annotated[ + ] + t_th1t: Annotated[ float, Field(0.7, description="Assumed tripping starting at 70% temperature"), - ] - t_th2t: Annotated[ + ] + t_th2t: Annotated[ float, Field(1.9, description="Assumed all tripped at 190% temperature"), - ] + ] p_fault: Annotated[ float, Field(3.5, ge=3.0, le=5.0, description="Active power multiplier post fault."), - ] + ] q_fault: Annotated[ float, Field(5.0, ge=3.0, le=7.0, description="Reactive power multiplier post fault."), - ] - v_stall: Annotated[ + ] + v_stall: Annotated[ float, - Field(0.55, ge=0.45, le=0.60, description="Stall voltage (range) based on laboratory testing"), - ] - v_break: Annotated[ + Field( + 0.55, ge=0.45, le=0.60, description="Stall voltage (range) based on laboratory testing" + ), + ] + v_break: Annotated[ float, Field(0.86, description="Compressor motor 'breakdown' voltage (pu)"), - ] + ] v_rstrt: Annotated[ float, Field(0.95, description="Reconnect when acceptable voltage met"), - ] - t_stall: Annotated[ + ] + t_stall: Annotated[ float, Field(0.032, description="Stall time (range) based on laboratory testing"), - ] - t_restart: Annotated[ + ] + t_restart: Annotated[ float, Field(0.300, description="Induction motor restart time is relatively short"), - ] - rated_pf: Annotated[ + ] + rated_pf: Annotated[ float, Field(0.939, description="Assumed slightly inductive motors load"), - ] - r_stall_pu: Annotated[ + ] + r_stall_pu: Annotated[ float, - Field(0.100, description="Based on laboratory testing results of residential air-conditioners."), + Field( + 0.100, + description="Based on laboratory testing results of residential air-conditioners.", + ), ] - x_stall_pu: Annotated[ + x_stall_pu: Annotated[ float, - Field(0.100, description="Based on laboratory testing results of residential air-conditioners."), - ] - + Field( + 0.100, + description="Based on laboratory testing results of residential air-conditioners.", + ), + ] + class PvControllerModel(BaseControllerModel): - control1: Annotated[ - SmartControls, - Field( - SmartControls.VOLT_VAR, - title="Control1", - description="Algorithm to run in the first control loop", - alias="Control1", - )] - control2: Annotated[ - SmartControls, - Field( - SmartControls.NONE, - title="Control1", - description="Algorithm to run in the second control loop", - alias="Control2", - )] - control3: Annotated[ - SmartControls, - Field( - SmartControls.NONE, - title="Control3", - description="Algorithm to run in the third control loop", - alias="Control3", - )] pf: Annotated[ float, Field( 1.0, title="pf", description="Power factor for the PV system", - )] + ), + ] pf_min: Annotated[ float, Field( @@ -298,7 +401,8 @@ class PvControllerModel(BaseControllerModel): title="pfMin", description="Minimum allowable power factor for the PV system. Applied only if enable_pf_limit is set.", alias="pfMin", - )] + ), + ] pf_max: Annotated[ float, Field( @@ -306,7 +410,8 @@ class PvControllerModel(BaseControllerModel): title="pfMax", description="Maximum allowable power factor for the PV system. Applied only if enable_pf_limit is set.", alias="pfMax", - )] + ), + ] p_min: Annotated[ float, Field( @@ -314,15 +419,17 @@ class PvControllerModel(BaseControllerModel): title="Pmin", description="TODO", alias="Pmin", - )] - p_max:Annotated[ + ), + ] + p_max: Annotated[ float, Field( 1.0, title="Pmax", description="TODO", alias="Pmax", - )] + ), + ] u_min: Annotated[ float, Field( @@ -330,7 +437,8 @@ class PvControllerModel(BaseControllerModel): title="uMin", description="Per unit voltage value at which inverter produces maximum vars. (volt / var algorithm).", alias="uMin", - )] + ), + ] u_db_min: Annotated[ float, Field( @@ -338,7 +446,8 @@ class PvControllerModel(BaseControllerModel): title="uDbMin", description="Lower bound for the voltage deadband [per unit]. Inverter will not produce or consume vars within these bands (volt / var algorithm).", alias="uDbMin", - )] + ), + ] u_db_max: Annotated[ float, Field( @@ -346,7 +455,8 @@ class PvControllerModel(BaseControllerModel): title="uDbMax", description="Upper bound for the voltage deadband [per unit]. Inverter will not produce or consume vars within these bands (volt / var algorithm).", alias="uDbMax", - )] + ), + ] u_max: Annotated[ float, Field( @@ -354,7 +464,8 @@ class PvControllerModel(BaseControllerModel): title="uMax", description="Per unit voltage value at which inverter consumes maximum vars (volt / var algorithm).", alias="uMax", - )] + ), + ] q_lim_pu: Annotated[ float, Field( @@ -362,7 +473,8 @@ class PvControllerModel(BaseControllerModel): title="QlimPU", description="Inverter reactive power limit [per unit] for the volt / var algorithm.", alias="QlimPU", - )] + ), + ] pf_lim: Annotated[ float, Field( @@ -370,7 +482,8 @@ class PvControllerModel(BaseControllerModel): title="PFlim", description="Inverter power factor limit. Applied only if enable_pf_limit is set.", alias="PFlim", - )] + ), + ] enable_pf_limit: Annotated[ bool, Field( @@ -378,7 +491,8 @@ class PvControllerModel(BaseControllerModel): title="EnablePFLimit", description="Flag to enable / disable power factor limits on the inverter", alias="Enable PF limit", - )] + ), + ] u_min_c: Annotated[ float, Field( @@ -386,7 +500,8 @@ class PvControllerModel(BaseControllerModel): title="uMinC", description="Lower voltage bound [per unit] for the volt / watt algorithm", alias="uMinC", - )] + ), + ] u_max_c: Annotated[ float, Field( @@ -394,7 +509,8 @@ class PvControllerModel(BaseControllerModel): title="uMaxC", description="Upper voltage bound [per unit] for the volt / watt algorithm", alias="uMaxC", - )] + ), + ] p_min_vw: Annotated[ float, Field( @@ -402,7 +518,8 @@ class PvControllerModel(BaseControllerModel): title="PminVW", description="Lower bound for the inveter active power output for the volt / watt algorithm", alias="PminVW", - )] + ), + ] vw_type: Annotated[ VoltWattCurtailmentStrategy, Field( @@ -410,7 +527,8 @@ class PvControllerModel(BaseControllerModel): title="VWtype", description="volt / watt algorithm to be implemented on rated or available power", alias="VWtype", - )] + ), + ] percent_p_cutin: Annotated[ float, Field( @@ -418,7 +536,8 @@ class PvControllerModel(BaseControllerModel): title="PCutin", description="Percentage cut-in power -- Percentage of kVA rating of inverter. When the inverter is OFF, the power from the array must be greater than this for the inverter to turn on.", alias="%PCutin", - )] + ), + ] percent_p_cutout: Annotated[ float, Field( @@ -426,7 +545,8 @@ class PvControllerModel(BaseControllerModel): title="%PCutout", description="Percentage cut-out power -- Percentage of kVA rating of inverter. When the inverter is ON, the inverter turns OFF when the power from the array drops below this value.", alias="%PCutout", - )] + ), + ] efficiency: Annotated[ float, Field( @@ -434,15 +554,8 @@ class PvControllerModel(BaseControllerModel): title="Efficiency", description="Efficieny of the inverter system", alias="Efficiency", - )] - priority: Annotated[ - ControlPriority, - Field( - ControlPriority.VAR, - title="Priority", - description="Set export priority for active power or reactive power", - alias="Priority", - )] + ), + ] damp_coef: Annotated[ float, Field( @@ -450,10 +563,9 @@ class PvControllerModel(BaseControllerModel): title="DampCoef", description="Damping cooefficient for the convergence algorithm", alias="DampCoef", - )] + ), + ] voltage_calc_mode: Annotated[ VoltageCalcModes, - Field( - VoltageCalcModes.MAX, - description="Voltage values used for calculations."), - ] \ No newline at end of file + Field(VoltageCalcModes.MAX, description="Voltage values used for calculations."), + ] diff --git a/src/pydss/py_controllers/py_controller.py b/src/pydss/py_controllers/py_controller.py new file mode 100644 index 00000000..6b2d3fcc --- /dev/null +++ b/src/pydss/py_controllers/py_controller.py @@ -0,0 +1,49 @@ +"""Deterministic registry for OpenMDAO controller components.""" + +from pydss.py_controllers.controllers.dynamic_voltage_support import DynamicVoltageSupport +from pydss.py_controllers.controllers.fault_controller import FaultController +from pydss.py_controllers.controllers.gen_controller import GenController +from pydss.py_controllers.controllers.motor_stall import MotorStall +from pydss.py_controllers.controllers.motor_stall_backup import MotorStallBackup +from pydss.py_controllers.controllers.motor_stall_simple import MotorStallSimple +from pydss.py_controllers.controllers.pv_controller import PvController +from pydss.py_controllers.controllers.pv_dynamic import PvDynamic +from pydss.py_controllers.controllers.pv_frequency_ride_thru import PvFrequencyRideThru +from pydss.py_controllers.controllers.pv_voltage_ride_thru import PvVoltageRideThru +from pydss.py_controllers.controllers.socket_controller import SocketController +from pydss.py_controllers.controllers.storage_controller import StorageController +from pydss.py_controllers.controllers.thermostatic_load import ThermostaticLoad +from pydss.py_controllers.controllers.xfmr_controller import XfmrController + +controller_types = { + cls.__name__: cls + for cls in ( + DynamicVoltageSupport, + FaultController, + GenController, + MotorStall, + MotorStallBackup, + MotorStallSimple, + PvController, + PvDynamic, + PvFrequencyRideThru, + PvVoltageRideThru, + SocketController, + StorageController, + ThermostaticLoad, + XfmrController, + ) +} + + +def create(controller_type, settings=None, *, element_name=""): + """Create a controller component without an OpenDSS or solver dependency.""" + try: + controller_class = controller_types[controller_type] + except KeyError as exc: + raise ValueError(f"Unknown OpenMDAO controller component: {controller_type}") from exc + return controller_class(settings=settings or {}, element_name=element_name) + + +ControllerTypes = controller_types +Create = create diff --git a/src/pydss/pyDSS.py b/src/pydss/py_dss.py similarity index 78% rename from src/pydss/pyDSS.py rename to src/pydss/py_dss.py index 9108de24..2bce1ef5 100644 --- a/src/pydss/pyDSS.py +++ b/src/pydss/py_dss.py @@ -5,19 +5,18 @@ from pydss.common import RUN_SIMULATION_FILENAME from pydss.exceptions import InvalidConfiguration -from pydss.dssInstance import OpenDSS +from pydss.dss_instance import OpenDSS from pydss.simulation_input_models import SimulationSettingsModel, dump_settings -from pydss.utils.utils import dump_data, load_data PYDSS_BASE_DIR = os.path.join(os.path.dirname(getattr(pydss, "__path__")[0]), "pydss") -class instance(object): +class Instance: def __init__(self): self._estimated_space = None def run(self, settings: SimulationSettingsModel, project, scenario, dry_run=False): - + self.run_scenario( project, scenario, @@ -33,21 +32,21 @@ def run_scenario(self, project, scenario, settings: SimulationSettingsModel, dry if dry_run: dss = OpenDSS(settings) self._dump_scenario_simulation_settings(settings) - #dss.init(dss_args) - logger.info('Dry run scenario: %s', settings.project.active_scenario) + # dss.init(dss_args) + logger.info("Dry run scenario: %s", settings.project.active_scenario) if settings.monte_carlo.num_scenarios > 0: raise InvalidConfiguration("Dry run does not support MonteCarlo simulation.") else: - self._estimated_space = dss.DryRunSimulation(project, scenario) + self._estimated_space = dss.dry_run_simulation(project, scenario) return None, None opendss = OpenDSS(settings) self._dump_scenario_simulation_settings(settings) - logger.info('Running scenario: %s', settings.project.active_scenario) + logger.info("Running scenario: %s", settings.project.active_scenario) if settings.monte_carlo.num_scenarios > 0: - opendss.RunMCsimulation(project, scenario, samples=settings.monte_carlo.num_scenarios) + opendss.run_mc_simulation(project, scenario, samples=settings.monte_carlo.num_scenarios) else: - for is_complete, _, _, _ in opendss.RunSimulation(project, scenario): + for is_complete, _, _, _ in opendss.run_simulation(project, scenario): if is_complete: break @@ -64,3 +63,6 @@ def _dump_scenario_simulation_settings(self, settings: SimulationSettingsModel): RUN_SIMULATION_FILENAME, ) dump_settings(settings, scenario_simulation_filename) + + +globals()["instance"] = Instance diff --git a/src/pydss/py_postprocessor/__init__.py b/src/pydss/py_postprocessor/__init__.py new file mode 100644 index 00000000..786753a1 --- /dev/null +++ b/src/pydss/py_postprocessor/__init__.py @@ -0,0 +1 @@ +__all__ = ["py_post_process"] diff --git a/src/pydss/py_postprocessor/postprocess_scripts/__init__.py b/src/pydss/py_postprocessor/postprocess_scripts/__init__.py new file mode 100644 index 00000000..a9a2c5b3 --- /dev/null +++ b/src/pydss/py_postprocessor/postprocess_scripts/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer.py b/src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer.py new file mode 100644 index 00000000..e4fe622a --- /dev/null +++ b/src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer.py @@ -0,0 +1,501 @@ +from pydss.py_postprocessor.py_postprocess_abstract import AbstractPostprocess +from pydss.py_postprocessor.postprocess_scripts.derms_optimizer_helper_modules.opt_funcs import ( + Derms, + linear_powerflow_model, +) +from scipy.sparse import lil_matrix +import scipy.sparse.linalg as sp +import scipy.sparse as sparse +from scipy import stats +import pandas as pd +from math import sqrt +import numpy as np +import os + + +class DermsOptimizer(AbstractPostprocess): + REQUIRED_INPUT_FIELDS_AND_DEFAULTS = { + "control_flag": True, + "control_all_flag": True, + "num_DERMS": 1, + "Vlower": 0.955, + "Vupper": 1.038, + "coeff_p": 0.0005, + "coeff_q": 0.000001, + "stepsize_xp": 1, + "stepsize_xq": 5, + "stepsize_mu": 10, + "opf_iteration": 60, + "max_iterations": 50, + "time_trigger_sec": 900, + "measurements_noises_flag": True, + "implementation_mode": "Voltage triggered", + "Distributions": { + "Vmes": ["norm", [0.0, 0.01]], + "Imes": ["norm", [0.0, 0.01]], + }, + } + + IMPLEMENTATION_MODES = ["Continuous", "Voltage triggered", "Time triggered", "Off"] + + def __init__( + self, + project, + scenario, + inputs, + dss_instance, + dss_solver, + dss_objects, + dss_objects_by_class, + simulation_settings, + logger, + ): + """Constructor method""" + super().__init__( + project, + scenario, + inputs, + dss_instance, + dss_solver, + dss_objects, + dss_objects_by_class, + simulation_settings, + logger, + ) + self.options = {**self.REQUIRED_INPUT_FIELDS_AND_DEFAULTS, **inputs} + self.Settings = simulation_settings + self.Objects = dss_objects_by_class + + self.dss_solver = dss_solver + self.dss = dss_instance + self.logger = logger + self.logger.info("Creating DERMS Optimizer module") + + self.rootPath = simulation_settings["Project"]["Project Path"] + self.dssPath = os.path.join( + self.rootPath, simulation_settings["Project"]["Active Project"], "DSSfiles" + ) + + self.Buses = [] + self.BusDistance = [] + self.Vbase_allnode = [] + + self.initialize_optimizer() + self.ExportCSVfiles = True + self.sim_start = False + self.DERMS_trigger_fail_count = 0 + self.DERMS_trigger_count = 0 + self.DERMS_trigger_success_count = 0 + + p_vdata = self.get_de_rdata() + + assert self.options["implementation_mode"] in self.IMPLEMENTATION_MODES, ( + "valid implementation modes are ()".fomrmat(self.IMPLEMENTATION_MODES) + ) + self.derms_controller = Derms( + p_vdata, + self.controlbus, + self.controlelem, + self.BEcapacity, + self.nodes[self.nSlack :], + self.BEname, + ) + + def initialize_optimizer(self): + self.dss.utils.run_command("calcV") + self.nodes = self.dss.Circuit.YNodeOrder() + self.nNodes = len(self.nodes) + + self.Slack = self.Objects["Vsources"]["Vsource.source"] + self.nSlack = int(self.Slack.get_value("phases")) + + for node in self.nodes: + node = node.lower() + bus, phase_info = node.split(".", 1) + self.Vbase_allnode.append(self.Objects["Buses"][bus].get_variable("kVBase") * 1000) + self.BusDistance.append(self.Objects["Buses"][bus].get_variable("Distance")) + + for x in self.nodes: + b_name = x.split(".")[0] + if b_name not in self.Buses: + self.Buses.append(b_name) + self.nBus = len(self.Buses) + + y00, y01, y10, y11, y11_sparse, y11_inv, ybus = self.extract_impednce_matrix() + self.get_branch_info() + + self.dss_solver.Solve() + v1, v1_pu = self.get_voltage__yorder() + + cap_names = self.dss.Capacitors.AllNames() + ",".join(cap_names) + + load_data = self.get_elem_data( + "Loads", + [ + "Name", + "kV", + "kW", + "pf", + "phase", + "bus", + "phases", + "VoltagesMagAng", + "Powers", + "conn", + ], + ) + sum([L["kW"] for L in load_data]) + # Do we want profile implementation here or a dedicatied profile manager? + + self.PVSystem = self.get_elem_data( + "Generators", ["Name", "bus", "busFull", "phase", "kVA", "kW", "phases", "conn"] + ) + self.pv_system_1phase = self.convert_3phase_pv_to_1phase_pv(self.PVSystem) + self.PVSystem_1phaseDF = pd.DataFrame(self.pv_system_1phase) + if self.options["control_flag"]: + p_vlocation = [] + len(self.pv_system_1phase) + node_index_with_pv = [] + pv_inverter_size = [] + for pv in self.pv_system_1phase: + allpvbus = pv["bus"].split(".") + p_vlocation.append(allpvbus[0]) + if len(allpvbus) == 1: + allpvbus = allpvbus + ["1", "2", "3"] + for ii in range(len(allpvbus) - 1): + pvbus = allpvbus[0] + "." + allpvbus[ii + 1] + node_index_with_pv.append(self.nodes.index(pvbus.upper())) + pv_inverter_size.append(float(pv["kVA"]) / (len(allpvbus) - 1)) + + self.get_elem_data("Capacitors", ["Name", "bus", "phase", "phases", "kV", "kvar"]) + pq_load, pq_pv, pq_node, qcap = self.calc_node_pq() + + if self.options["control_all_flag"]: + self.controlbus = self.nodes[self.nSlack :] + else: + self.controlbus = self.get_contol_buses(["Generators", "Capacitors"]) + + self.controlelem = [] + self.mu0 = [ + np.zeros(len(self.controlbus)), + np.zeros(len(self.controlbus)), + np.zeros(len(self.controlelem)), + ] + + self.power_flow_data = linear_powerflow_model( + y00, y01, y10, y11_inv, [], v1, self.nSlack + ) + + self.stepsize_control = [ + self.options["stepsize_xp"], + self.options["stepsize_xq"], + self.options["stepsize_mu"], + ] + self.vlimit = [self.options["Vupper"], self.options["Vlower"]] + return + + def calc_node_pq(self): + pq_load = self.get_pq_by_class("Loads") + pq_pv = self.get_pq_by_class("Generators") + pq_cap = self.get_pq_by_class("Capacitors") + qcap = pq_cap.imag + pq_node = -pq_load + pq_pv - 1j * np.array(qcap) # power injection + return pq_load, pq_pv, pq_node, qcap + + def get_pq_by_class(self, class_name): + p = [0] * len(self.nodes) + q = [0] * len(self.nodes) + for name, obj in self.Objects[class_name].items(): + power = obj.get_value("Powers") + bus = obj.get_value("BusNames")[0] + nodes = bus.split(".")[1:] if len(bus.split(".")[1:]) else [1, 2, 3] + + for i, ii in enumerate(nodes): + node_name = "{}.{}".format(bus.split(".")[0], ii) + index = self.nodes.index(node_name.upper()) + p[index] = power[2 * i] + q[index] = power[2 * i + 1] + pq = np.array(p) + 1j * np.array(q) + return pq + + def get_elem_data(self, element_class, properties): + data = [] + for name, obj in self.Objects[element_class].items(): + datum = {} + for ppty in properties: + if ppty == "bus": + datum[ppty] = obj.get_value("BusNames")[0].split(".")[0] + elif ppty == "phase": + phases = obj.get_value("BusNames")[0].split(".")[1:] + datum[ppty] = phases if len(phases) else [1, 2, 3] + elif ppty == "busFull": + datum[ppty] = obj.get_value("BusNames")[0] + else: + datum[ppty] = obj.get_value(ppty) + data.append(datum) + return data + + def convert_3phase_pv_to_1phase_pv(self, pv_systems): + # convert multi-phase PV into multiple 1-phase PVs for control implementation purpose + pv_system_1phase = [] + for pv in pv_systems: + for i, ii in enumerate(pv["phase"]): + pv_perphase = {} + pv_perphase["Name"] = pv["Name"] # +'_node'+bus[ii+1] + pv_perphase["bus"] = pv["bus"] + "." + ii + pv_perphase["kW"] = pv["kW"] / pv["phases"] + pv_perphase["kVA"] = pv["kVA"] / pv["phases"] + pv_system_1phase.append(pv_perphase) + return pv_system_1phase + + def get_incidence_matrix(self): + # self.dss.utils.run_command('solve mode=fault') dont think this is needed. May be wrong + ybranch_prim, branch_node_incidence, n_neutral, record_index = self.construct__yprime() + current_coeff_matrix = np.dot(ybranch_prim, branch_node_incidence) + current_coeff_matrix = current_coeff_matrix[record_index, :-n_neutral] + branch_node_incidence = branch_node_incidence[record_index, :-n_neutral] + return current_coeff_matrix, branch_node_incidence, current_coeff_matrix + + def extract_impednce_matrix(self): + y = self.dss.Circuit.SystemY() + ybus = self.to_complex(y) + y00 = ybus[0 : self.nSlack, 0 : self.nSlack] + y01 = ybus[0 : self.nSlack, self.nSlack :] + y10 = ybus[self.nSlack :, 0 : self.nSlack] + y11 = ybus[self.nSlack :, self.nSlack :] + y11_sparse = lil_matrix(y11) + y11_sparse = y11_sparse.tocsr() + a_sps = sparse.csc_matrix(y11) + lu_obj = sp.splu(a_sps) + y11_inv = lu_obj.solve(np.eye(self.nNodes - self.nSlack)) + return y00, y01, y10, y11, y11_sparse, y11_inv, ybus + + def get_voltage__yorder(self): + temp__vbus = self.dss.Circuit.YNodeVArray() + voltage = [complex(0, 0)] * self.nNodes + for ii in range(self.nNodes): + voltage[ii] = complex(temp__vbus[ii * 2], temp__vbus[ii * 2 + 1]) + voltage_pu = list(map(lambda x: abs(x[0]) / x[1], zip(voltage, self.Vbase_allnode))) + return voltage, voltage_pu + + def to_complex(self, values): + ydim = int(sqrt(len(values) / 2)) + yreal = np.array(values[0::2]).reshape((ydim, ydim)) + yimag = np.array(values[1::2]).reshape((ydim, ydim)) + ycmplx = yreal + 1j * yimag + return ycmplx + + def construct__yprime(self): + ybranch_prim = np.array( + [[complex(0, 0)] * 2 * self.nBarnchElements] * 2 * self.nBarnchElements + ) + branch_node_incidence = np.zeros( + [2 * self.nBarnchElements, self.nNodes + 422] + ) # TODO fix hard coded 422 for Green, 104 for Diamond + + record_index = [] + n_neutral = 0 + start_no = 0 + count = 0 + + temp__all_node_names = self.dss.Circuit.YNodeOrder() + for elem_name, elm_obj in self.branchElements.items(): + values = elm_obj.get_value("YPrim") + yprim = self.to_complex(values) + buses = [ii.split(".")[0] for ii in elm_obj.get_value("BusNames")] + nodes = elm_obj.get_value("NodeOrder") + n_phases = int(len(nodes) / 2) + n_neutral += nodes.count(0) + for ii in range(n_phases): + from_node = buses[0] + "." + str(nodes[ii]) + to_node = buses[1] + "." + str(nodes[ii + n_phases]) + if nodes[ii] == 0: + temp__all_node_names.append(from_node.upper()) + if nodes[ii + n_phases] == 0: + temp__all_node_names.append(to_node.upper()) + from_node_index = temp__all_node_names.index(from_node.upper()) + to_node_index = temp__all_node_names.index(to_node.upper()) + branch_node_incidence[2 * count + ii, from_node_index] = 1 + branch_node_incidence[2 * count + n_phases + ii, to_node_index] = 1 + record_index.append(2 * count + ii) + count = count + n_phases + end_no = start_no + 2 * n_phases + ybranch_prim[start_no:end_no, start_no:end_no] = yprim + start_no = end_no + + return ybranch_prim, branch_node_incidence, n_neutral, record_index + + def get_contol_buses(self, class_names): + controlbus = [] + for class_name in class_names: + for elm in self.get_elem_data(class_name, ["bus", "phases"]): + for phase in elm["phases"]: + controlbus.append("{}.{}".format(elm["bus"], phase)) + return controlbus + + @staticmethod + def _get_required_input_fields(): + return {} + + def get_branch_info(self): + self.branchElements = {**self.Objects["Lines"], **self.Objects["Transformers"]} + self.nBarnchElements = len(self.branchElements) + self.BEindex = [] + self.BEcapacity = [] + self.BEname = [] + + for elm_name, elm_obj in self.branchElements.items(): + n_terms = elm_obj.get_value("NumTerminals") + nodes = elm_obj.get_value("NodeOrder") + n_phases = int(len(nodes) / n_terms) + self.BEindex.append(list(range(n_phases))) + for ii in range(n_phases): + self.BEcapacity.append(elm_obj.get_value("NormalAmps")) + self.BEname.append("{}.{}".format(elm_name, nodes[ii])) + + def get_de_rdata(self): + data = self.get_elem_data( + "Generators", ["Name", "busFull", "phase", "kVA", "kW", "phases", "conn"] + ) + data = pd.DataFrame(data) + data.index = data["Name"] + p_vdata = { + "pvName": [], + "pvLocation": [], + "bus": [], + "pvSize": [], + "inverterSize": [], + } + for pv1_p in self.PVSystem_1phaseDF["Name"].tolist(): + pv = data.loc[pv1_p] + p_vdata["pvName"].append(pv["Name"]) + p_vdata["bus"].append(pv["busFull"]) + p_vdata["pvSize"].append(pv["kW"]) + p_vdata["inverterSize"].append(pv["kVA"]) + for b in self.buses(pv): + if b.upper() not in p_vdata["pvLocation"]: + p_vdata["pvLocation"].append(b.upper()) + break + return p_vdata + + def buses(self, pv): + bus = pv["busFull"].split(".") + if len(bus) == 1: + bus += ["1", "2", "3"] + return [f"{bus[0]}.{i}" for i in bus[1:]] + + def run(self, step, step_max, simulation=None): + """Induces and removes a fault as the simulation runs as per user defined settings.""" + self.logger.info("Running DERMS optimization module") + opt_iter = 0 + + # Really bad codeing form + mu0 = [ + [0 for x in range(len(self.controlbus))], + [0 for x in range(len(self.controlbus))], + [0 for x in range(len(self.controlelem))], + ] + + p_vmax = [pv["kW"] if pv["kW"] < pv["kVA"] else pv["kVA"] for pv in self.pv_system_1phase] + + while opt_iter < self.options["max_iterations"]: + self.dss_solver.reSolve() + p_vlocation, p_vpower, vmes, imes = self.dermsController.monitor( + self._dssInstance, self.Objects, self.PVSystem_1phaseDF + ) + self.logger.info("Voltage measurements: {}".format(len(vmes))) + self.logger.info("Maximum voltage: {}".format(max(vmes))) + + if self.options["measurements_noises_flag"]: + dist_name, dist_params = self.options["Distributions"]["Vmes"] + dist = getattr(stats, dist_name) + vmes = vmes + dist.rvs(*dist_params, size=len(vmes)) + dist_name, dist_params = self.options["Distributions"]["Imes"] + dist = getattr(stats, dist_name) + imes = imes + dist.rvs(*dist_params, size=len(imes)) + + if self.options["implementation_mode"] == "Continuous": + derms_trigger = 1 + elif self.options["implementation_mode"] == "Voltage triggered": + if max(vmes) > self.options["Vupper"] or min(vmes) < self.options["Vlower"]: + derms_trigger = 1 + # TODO: Should DERMS trigger count be in this if statement? + elif ( + max(vmes) <= self.options["Vupper"] + and min(vmes) >= self.options["Vlower"] + and opt_iter >= 1 + ): + derms_trigger = 0 + self.DERMS_trigger_count += 1 + self.DERMS_trigger_success_count += 1 + self.logger.info("DERMS OPF successfully triggered, maxV: {}".fotmat(max(vmes))) + break + else: + derms_trigger = 0 + elif self.options["implementation_mode"] == "Time triggered": + # TODO: Is step * stepsize_sim >= time_mode_resolution required? Is the idea to ensure that the controller does not run on the first iteration? + if ( + step > 0 + and self.dss_solver.GetTotalSeconds() % self.options["time_trigger_sec"] == 0 + ): + derms_trigger = 1 + else: + derms_trigger = 0 + elif self.options["implementation_mode"] == "Off": + derms_trigger = 0 + + if derms_trigger == 1: + [x1, mu1] = self.dermsController.control( + self.power_flow_data, + self.options, + self.stepsize_control, + mu0, + self.vlimit, + p_vpower, + imes, + vmes, + p_vmax, + ) + # ------ apply the setpoint ------ + n_pv_1_phs = len(self.pv_system_1phase) + for pv in self.PVSystem: + idx = [ + i for i, x in enumerate(self.pv_system_1phase) if x["Name"] == pv["Name"] + ] + self.Objects["Generators"][pv["Name"]].SetParameter( + "kW", sum([x1[ii] for ii in idx]) + ) + self.Objects["Generators"][pv["Name"]].SetParameter( + "kvar", sum([x1[ii + n_pv_1_phs] for ii in idx]) + ) + + mu0 = mu1 + # resV_record.append(max(Vmes)) + opt_iter = opt_iter + 1 + if max(vmes) <= self.options["Vupper"] and min(vmes) >= self.options["Vlower"]: + self.DERMS_trigger_count += 1 + self.DERMS_trigger_success_count += 1 + self.logger.info("DERMS OPF successfully triggered") + break + elif derms_trigger == 0: + # TODO: Is this piece of code required? + # for pv in self.PVSystem: + # PVgen = float(pv['kW']) * PVshape[ + # int((present_step - 1) * stepsize_sim / data_resolution + 3600 / data_resolution * startH)] + # if PVgen > float(pv["kVA"]): + # PVgen = float(pv["kVA"]) + # dss.run_command('edit ' + str(pv["name"]) + ' kW=' + str(PVgen) + ' pf=' + str(pf_auto)) + break + if opt_iter == self.options["max_iterations"]: + self.DERMS_trigger_count += 1 + if max(vmes) > self.options["Vupper"] or min(vmes) < self.options["Vlower"]: + self.DERMS_trigger_fail_count += 1 + self.logger.warning("DERMS OPF triggered and failed") + + opt_iter += 1 + # step-=1 # uncomment the line if the post process needs to rerun for the same point in time + return step + + def get_preoptimization_results(self): + return diff --git a/src/pydss/defaults/Monte_Carlo/__init__.py b/src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer_helper_modules/__init__.py similarity index 100% rename from src/pydss/defaults/Monte_Carlo/__init__.py rename to src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer_helper_modules/__init__.py diff --git a/src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer_helper_modules/opt_funcs.py b/src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer_helper_modules/opt_funcs.py new file mode 100644 index 00000000..52812a6b --- /dev/null +++ b/src/pydss/py_postprocessor/postprocess_scripts/derms_optimizer_helper_modules/opt_funcs.py @@ -0,0 +1,622 @@ +import numpy as np +import math +import csv + + +def linear_powerflow_model(y00, y01, y10, y11_inv, i_coeff, v1, slack_no): + # voltage linearlization + v1_conj = np.conj(v1[slack_no:]) + v1_conj_inv = 1 / v1_conj + coeff_v = y11_inv * v1_conj_inv + coeff_v_p = coeff_v + coeff_v_q = -1j * coeff_v + coeff__vm = -np.dot(y11_inv, np.dot(y10, v1[:slack_no])) + + # voltage magnitude linearization + m = coeff__vm + m_inv = 1 / coeff__vm + coeff__vmag_k = abs(m) + a = (np.multiply(coeff_v.transpose(), m_inv)).transpose() + coeff__vmag_p = (np.multiply(a.real.transpose(), coeff__vmag_k)).transpose() + coeff__vmag_q = (np.multiply((-1j * a).real.transpose(), coeff__vmag_k)).transpose() + + # current linearization + if len(i_coeff): + coeff_i_p = np.dot(i_coeff[:, slack_no:], coeff_v_p) + coeff_i_q = np.dot(i_coeff[:, slack_no:], coeff_v_q) + coeff_i_const = np.dot(i_coeff[:, slack_no:], coeff__vm) + np.dot( + i_coeff[:, :slack_no], v1[:slack_no] + ) + else: + coeff_i_p = [] + coeff_i_q = [] + coeff_i_const = [] + + # =========================================Yiyun's Notes===========================================# + # Output relations: Vmag = coeff_Vmag_P * Pnode + coeff_Vmag_Q * Qnode + coeff_Vm + # I = coeff_I_P * Pnode + coeff_I_Q * Qnode + coeff_I_const (complex value) + # ================================================================================================# + + return ( + coeff_v_p, + coeff_v_q, + coeff__vm, + coeff__vmag_p, + coeff__vmag_q, + coeff__vmag_k, + coeff_i_p, + coeff_i_q, + coeff_i_const, + ) + + +def validate_linear_model(coeff__vp, coeff__vq, coeff__vm, pq_node, slack_number): + v_cal = ( + coeff__vm + + np.dot(coeff__vp, np.array([np.real(ii) * 1000 for ii in pq_node[slack_number:]])) + + np.dot(coeff__vq, np.array([np.imag(ii) * 1000 for ii in pq_node[slack_number:]])) + ) + v_cal_1 = coeff__vm + np.dot(coeff__vp, np.conj(pq_node[slack_number:] * 1000)) + # coeff_Vp*Pnode + coeff_Vq*Qnode + coeff_Vm + + # =========================================Yiyun's Notes===========================================# + # 1000 should be the S base + # =================================================================================================# + + return [v_cal, v_cal_1] + + +def check_vi_correct( + v1, + pq_node, + slack_number, + coeff_v, + coeff__vm, + coeff__vmag_p, + coeff__vmag_q, + coeff__vmag_k, + y10, + y11, + coeff_i_p, + coeff_i_q, + coeff_i_const, + i_coeff, +): + v1_linear = np.dot(coeff_v, np.conj(pq_node[slack_number:] * 1000)) + coeff__vm + v1_linear = list(v1_linear) + vdiff = list( + map(lambda x: abs(x[0] - x[1]) / abs(x[0]) * 100, zip(v1[slack_number:], v1_linear)) + ) + with open("voltage_diff.csv", "w") as f: + csvwriter = csv.writer(f) + csvwriter.writerow(vdiff) + f.close() + + v1_mag_linear = ( + np.dot(coeff__vmag_p, (pq_node[slack_number:] * 1000).real) + + np.dot(coeff__vmag_q, (pq_node[slack_number:] * 1000).imag) + + coeff__vmag_k + ) + v1_mag_linear = list(v1_mag_linear) + vdiff = list( + map( + lambda x: abs(abs(x[0]) - x[1]) / abs(x[0]) * 100, zip(v1[slack_number:], v1_mag_linear) + ) + ) + with open("voltageMag_diff.csv", "w") as f: + csvwriter = csv.writer(f) + csvwriter.writerow(vdiff) + f.close() + + # get Ibus + ibus = list( + map( + lambda x: (x[0] * 1000 / x[1]).conjugate(), + zip(list(pq_node)[slack_number:], v1[slack_number:]), + ) + ) + ibus_cal_0 = np.dot(y10, v1[0:slack_number]) + ibus_cal_1 = np.dot(y11, v1[slack_number:]) + ibus_cal = list(map(lambda x: x[0] + x[1], zip(ibus_cal_0, ibus_cal_1))) + idiff = list(map(lambda x: abs(x[0] - x[1]), zip(ibus, ibus_cal))) + with open("currentBus_diff.csv", "w") as f: + csvwriter = csv.writer(f) + csvwriter.writerow(idiff) + f.close() + + # get Ibranch + ibranch = np.dot(i_coeff, v1) + ibranch_cal = np.dot(i_coeff[:, slack_number:], v1_linear) + np.dot( + i_coeff[:, 0:slack_number], v1[:slack_number] + ) + ibranch_diff = list(map(lambda x: abs(x[0] - x[1]), zip(ibranch, ibranch_cal))) + with open("current_diff.csv", "w") as f: + csvwriter = csv.writer(f) + csvwriter.writerow(ibranch_diff) + f.close() + + +def cost_fun( + x, + dual_upper, + dual_lower, + v1_pu, + ppv_max, + coeff_p, + coeff_q, + npv, + control_bus_index, + vupper, + vlower, + dual_current, + thermal_limit, + i1_mag, +): + # cost_function = coeff_p*(Pmax-P)^2+coeff_q*Q^2+dual_upper*(v1-1.05)+dual_lower*(0.95-v1) + f1 = 0 + for ii in range(npv): + f1 = ( + f1 + + coeff_p * (ppv_max[ii] - x[ii]) * (ppv_max[ii] - x[ii]) + + coeff_q * x[ii + npv] * x[ii + npv] + ) + # f = f1 + np.dot(dual_upper,(np.array(v1_pu)[control_bus_index]-Vupper)) + np.dot(dual_lower,(Vlower-np.array(v1_pu)[control_bus_index])) + v_evaluate = [v1_pu[ii] for ii in control_bus_index] + f2 = ( + f1 + + np.dot(dual_upper, np.array([max(ii - vupper, 0) for ii in v_evaluate])) + + np.dot(dual_lower, np.array([max(vlower - ii, 0) for ii in v_evaluate])) + ) + f3 = np.dot( + dual_current, + np.array( + [ + max(ii, 0) + for ii in list(map(lambda x: x[0] * x[0] - x[1] * x[1], zip(i1_mag, thermal_limit))) + ] + ), + ) + f = f2 + f3 + + # =========================================Yiyun's Notes===========================================# + # f1 is the quadratic PV curtailment plus quadratic reactive power injection + # f2 is the Lagrangian term for voltage violations and line current violations + # ===> Note the "control_bus_index" might be the index for measurement sensitivity analysis + # =================================================================================================# + + return [f1, f] + + +def pv_cost_fun_gradient(x, coeff_p, coeff_q, pmax): + grad = np.zeros(len(x)) + for ii in range(int(len(x) / 2)): + grad[ii] = -2 * coeff_p * (pmax[ii] * 1000 - x[ii] * 1000) + grad[ii + int(len(x) / 2)] = 2 * coeff_q * x[ii + int(len(x) / 2)] * 1000 + # grad[ii + int(len(x) / 2)] = 0 + + # =========================================Yiyun's Notes===========================================# + # x is the decision vector [P,Q] + # =================================================================================================# + + return grad + + +def voltage_constraint_gradient( + all_node_names, node_with_pv, dual_upper, dual_lower, coeff__vmag_p, coeff__vmag_q +): + node_noslackbus = all_node_names + node_noslackbus[0:3] = [] + + # =========================================Yiyun's Notes===========================================# + # remove the slack bus + # =================================================================================================# + + grad_upper = np.matrix([0] * len(node_noslackbus) * 2).transpose() + grad_lower = np.matrix([0] * len(node_noslackbus) * 2).transpose() + count = 0 + for node in node_noslackbus: + if node in node_with_pv: + grad_upper[count] = dual_upper.transpose() * coeff__vmag_p[:, count] + grad_upper[count + len(node_noslackbus)] = ( + dual_upper.transpose() * coeff__vmag_q[:, count] + ) + grad_lower[count] = -dual_lower.transpose() * coeff__vmag_p[:, count] + grad_lower[count + len(node_noslackbus)] = ( + -dual_lower.transpose() * coeff__vmag_q[:, count] + ) + count = count + 1 + return [grad_upper, grad_lower] + + +def current_constraint_gradient( + all_node_names, node_with_pv, dual_upper, coeff__imag_p, coeff__imag_q +): + node_noslackbus = all_node_names + node_noslackbus[0:3] = [] + grad_upper = np.matrix([0] * len(node_noslackbus) * 2).transpose() + count = 0 + for node in node_noslackbus: + if node in node_with_pv: + grad_upper[count] = dual_upper.transpose() * coeff__imag_p[:, count] + grad_upper[count + len(node_noslackbus)] = ( + dual_upper.transpose() * coeff__imag_q[:, count] + ) + count = count + 1 + return grad_upper + + # =========================================Yiyun's Notes===========================================# + # PV_costFun_gradient, voltage_constraint_gradient, current_constraint_gradient and project_PV.. + # ... are set up for updating the PV decision variables in eq(10) + # =================================================================================================# + + +def voltage_constraint(v1_mag): + g = v1_mag - 1.05 + g.append(0.95 - v1_mag) + return g + + +def current_constraint(i1_mag, imax): + g = [] + g.append(i1_mag - imax) + + # =========================================Yiyun's Notes===========================================# + # assume single directional power flow + # voltage_constraint, current_constraint, and project_dualvariable are set up for updating the dual... + # ... variables in eq (11) + # =================================================================================================# + + return g + + +def project_dualvariable(mu): + for ii in range(len(mu)): + mu[ii] = max(mu[ii], 0) + + # =========================================Yiyun's Notes===========================================# + # If the corresponding constraints in primal problem is in canonical form, then dual variable is >=0 + # =================================================================================================# + + return mu + + +def project_pv(x, pmax, sinv): + qavailable = 0 + pavailable = 0 + num = len(sinv) + for ii in range(num): + if x[ii] > pmax[ii]: + x[ii] = pmax[ii] + elif x[ii] < 0: + x[ii] = 0 + + if sinv[ii] > x[ii]: + qmax = math.sqrt(sinv[ii] * sinv[ii] - x[ii] * x[ii]) + else: + qmax = 0 + if x[ii + num] > qmax: + x[ii + num] = qmax + # elif x[ii + num] < 0: + # x[ii + num] = 0 + elif x[ii + num] < -qmax: + x[ii + num] = -qmax + + pavailable = pavailable + pmax[ii] + qavailable = qavailable + qmax + return [x, pavailable, qavailable] + + +def dual_update(mu, coeff_mu, constraint): + mu_new = mu + coeff_mu * constraint + mu_new = project_dualvariable(mu_new) + + # =========================================Yiyun's Notes===========================================# + # normal way for update Lagrangian variable is by the sub-gradient of cost function + # Here is the equation (11) in the draft paper + # =================================================================================================# + + return mu_new + + +def matrix_cal_for_sub_power(v0, y00, y01, y11, v1_noload): + diag_v0 = np.matrix([[complex(0, 0)] * 3] * 3) + diag_v0[0, 0] = v0[0] + diag_v0[1, 1] = v0[1] + diag_v0[2, 2] = v0[2] + k = diag_v0 * y01.conj() * np.linalg.inv(y11.conj()) + g = ( + diag_v0 * y00.conj() * np.matrix(v0).transpose().conj() + + diag_v0 * y01.conj() * v1_noload.conj() + ) + return [k, g] + + +def sub_power_pq(v1, pq_node, k, g): + diag_v1 = np.matrix([[complex(0, 0)] * len(v1)] * len(v1)) + for ii in range(len(v1)): + diag_v1[ii, ii] = v1[ii] + m = k * np.linalg.inv(diag_v1) + mr = m.real + mi = m.imag + p0 = g.real + (mr.dot(pq_node.real) * 1000 - mi.dot(pq_node.imag) * 1000) + q0 = g.imag + (mr.dot(pq_node.imag) * 1000 + mi.dot(pq_node.real) * 1000) + + p0 = p0 / 1000 + q0 = q0 / 1000 # convert to kW/kVar + + # =========================================Yiyun's Notes===========================================# + # Power injection at substation/feeder head + # =================================================================================================# + + return [p0, q0, m] + + +def sub_cost_fun_gradient(x, sub_ref, coeff_sub, sub_measure, m, node_with_pv): + grad_a = np.matrix([0] * len(x)).transpose() + grad_b = np.matrix([0] * len(x)).transpose() + grad_c = np.matrix([0] * len(x)).transpose() + + mr = m.real + mi = m.imag + count = 0 + for node in node_with_pv: + grad_a[count] = -mr[0, int(node)] + grad_b[count] = -mr[1, int(node)] + grad_c[count] = -mr[2, int(node)] + + grad_a[count + len(node_with_pv)] = mi[0, int(node)] + grad_b[count + len(node_with_pv)] = mi[1, int(node)] + grad_c[count + len(node_with_pv)] = mi[2, int(node)] + + count = count + 1 + + res = coeff_sub * ( + (sub_measure[0] - sub_ref[0]) * 1000 * grad_a + + (sub_measure[1] - sub_ref[1]) * 1000 * grad_b + + (sub_measure[2] - sub_ref[2]) * 1000 * grad_c + ) + res = res / 1000 + + return res + + +def projection(x, xmax, xmin): + for ii in range(len(x)): + if x.item(ii) > xmax[ii]: + x[ii] = xmax[ii] + if x.item(ii) < xmin[ii]: + x[ii] = xmin[ii] + return x + + +class Derms: + def __init__( + self, pv_data, controlbus, controlelem, controlelem_limit, sub_node_names, sub_elem_names + ): + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # PV_name: names of all PVs in the zone + # PV_size: sizes of all PVs in the zone + # PV_location: busnames of all PVs in the zone + # controlbus: names of all controlled nodes + # sub_node_names: names of all nodes in the zone + # sub_node_names "include" controlbus + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + self.PV_name = pv_data["pvName"] + self.PV_location = pv_data["pvLocation"] + self.PV_size = pv_data["pvSize"] + self.inverter_size = pv_data["inverterSize"] + self.control_bus = controlbus + + sub_node_names = [ii.upper() for ii in sub_node_names] + self.controlbus_index = [ + sub_node_names.index(ii.upper()) for ii in controlbus + ] # control bus index in the sub system (number) + # here + p_vbus_index = [] + for bus in self.PV_location: + temp = bus.split(".") + if len(temp) == 1: + temp = temp + ["1", "2", "3"] + for ii in range(len(temp) - 1): + p_vbus_index.append(sub_node_names.index((temp[0] + "." + temp[ii + 1]).upper())) + + # =========================================Yiyun's Notes===========================================# + # adding .1 .2 .3 following the number to recognize the three phases. + # =================================================================================================# + self.p_vbus_index = p_vbus_index + self.control_elem = controlelem + self.controlelem_limit = controlelem_limit + self.controlelem_index = [ + sub_elem_names.index(ii) for ii in controlelem + ] # control branches index in the sub system (number) + + def monitor(self, dss, dss_objects, pv_system_1phase): + p_vpowers = [] + for pv in pv_system_1phase["Name"].tolist(): + n_phases = dss_objects["Generators"][pv].get_value("phases") + power = dss_objects["Generators"][pv].get_value("Powers") + p_vpowers.append([sum(power[::2]) / n_phases, sum(power[1::2]) / n_phases]) + p_vpowers = np.asarray(p_vpowers) + + vmes = [] + for bus in self.control_bus: + bus_name = bus.split(".")[0].lower() + vmag = dss_objects["Buses"][bus_name].get_value("puVmagAngle")[::2] + allbusnode = dss.Bus.nodes() + phase = bus.split(".")[1] + index = allbusnode.index(int(phase)) + vnode = vmag[index] + vmes.append(vnode) + + imes = [] + for elem in self.control_elem: + class_name = elem.split(".")[0] + "s" + currents = dss_objects[class_name][elem].get_value("CurrentsMagAng")[::2][ + :3 + ] # TODO: Why is there a hardcoded [:3] ? + imes.append(currents) + + return [self.PV_location, p_vpowers, vmes, imes] + + def control( + self, + linear_pf_coeff, + options, + stepsize, + mu0, + vlimit, + p_vpower, + imes, + vmes, + pv__pmax_forecast, + ): + coeff_p = options["coeff_p"] + coeff_q = options["coeff_q"] + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # linear_PF_coeff is the linear power flow model coefficients for the zone, and linear power flow model + # coefficients are the result vector from function "linear_powerflow_model" + # coeff_p, coeff_q are constant coefficients in PV cost function + # stepsize is a vector of stepsize constants + # mu0 is the dual variable from last time step: mu_Vmag_upper0, mu_Vmag_lower0, mu_I0 + # Vlimit is the allowed voltage limit: Vupper and Vlower + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + p_vname = self.PV_name + npv = len(p_vname) + x0 = np.zeros(2 * npv) + for ii in range(npv): + x0[ii] = -p_vpower[ii][0] # in kW + x0[ii + npv] = -p_vpower[ii][1] # in kVar + + # coeff_V_P = linear_PF_coeff[0] + # coeff_V_Q = linear_PF_coeff[1] + # coeff_Vm = linear_PF_coeff[2] + coeff__vmag_p = linear_pf_coeff[3] + coeff__vmag_q = linear_pf_coeff[4] + # coeff_Vmag_k = linear_PF_coeff[5] + coeff_i_p = linear_pf_coeff[6] + coeff_i_q = linear_pf_coeff[7] + # coeff_I_const = linear_PF_coeff[8] + stepsize_xp = stepsize[0] + stepsize_xq = stepsize[1] + stepsize_mu = stepsize[2] + vupper = vlimit[0] + vlower = vlimit[1] + + controlbus_index = self.controlbus_index + p_vbus_index = self.p_vbus_index + controlelem_index = self.controlelem_index + pv_inverter_size = self.inverter_size + imes_limit = self.controlelem_limit + + mu__vmag_upper0 = mu0[0] + mu__vmag_lower0 = mu0[1] + mu_i0 = mu0[2] + + p_vcost_fun_gradient = pv_cost_fun_gradient(x0, coeff_p, coeff_q, pv__pmax_forecast) + + vmag_upper_gradient = np.concatenate( + ( + np.dot( + coeff__vmag_p[ + np.ix_([ii for ii in controlbus_index], [ii for ii in p_vbus_index]) + ].transpose(), + mu__vmag_upper0, + ), + np.dot( + coeff__vmag_q[ + np.ix_([ii for ii in controlbus_index], [ii for ii in p_vbus_index]) + ].transpose(), + mu__vmag_upper0, + ), + ), + axis=0, + ) + vmag_lower_gradient = np.concatenate( + ( + np.dot( + coeff__vmag_p[ + np.ix_([ii for ii in controlbus_index], [ii for ii in p_vbus_index]) + ].transpose(), + mu__vmag_lower0, + ), + np.dot( + coeff__vmag_q[ + np.ix_([ii for ii in controlbus_index], [ii for ii in p_vbus_index]) + ].transpose(), + mu__vmag_lower0, + ), + ), + axis=0, + ) + + vmag_gradient = vmag_upper_gradient - vmag_lower_gradient + if len(mu_i0) > 0: + temp_real = mu_i0 * np.array(imes.real) + temp_imag = mu_i0 * np.array(imes.imag) + + i_gradient_real = np.concatenate( + ( + np.dot( + coeff_i_p[ + np.ix_([ii for ii in controlelem_index], [ii for ii in p_vbus_index]) + ].real.transpose(), + temp_real, + ), + np.dot( + coeff_i_q[ + np.ix_([ii for ii in controlelem_index], [ii for ii in p_vbus_index]) + ].real.transpose(), + temp_real, + ), + ), + axis=0, + ) + i_gradient_imag = np.concatenate( + ( + np.dot( + coeff_i_p[ + np.ix_([ii for ii in controlelem_index], [ii for ii in p_vbus_index]) + ].imag.transpose(), + temp_imag, + ), + np.dot( + coeff_i_q[ + np.ix_([ii for ii in controlelem_index], [ii for ii in p_vbus_index]) + ].imag.transpose(), + temp_imag, + ), + ), + axis=0, + ) + i_gradient = 2 * i_gradient_real + 2 * i_gradient_imag + else: + i_gradient = 0 + + gradient = p_vcost_fun_gradient + vmag_gradient + i_gradient / 1000 + + # compute x1, mu1 + x1 = np.concatenate( + [x0[:npv] - stepsize_xp * gradient[:npv], x0[npv:] - stepsize_xq * gradient[npv:]] + ) + [x1, pmax_all_pv, qmax_all_pv] = project_pv(x1, pv__pmax_forecast, pv_inverter_size) + x1 = np.array([round(ii, 5) for ii in x1]) + + mu__vmag_lower1 = mu__vmag_lower0 + stepsize_mu * (vlower - np.array(vmes)) + mu__vmag_upper1 = mu__vmag_upper0 + stepsize_mu * (np.array(vmes) - vupper) + mu__vmag_lower1 = project_dualvariable(mu__vmag_lower1) + mu__vmag_upper1 = project_dualvariable(mu__vmag_upper1) + if mu_i0: + mu_i1 = mu_i0 + stepsize_mu / 300 * np.array( + list(map(lambda x: x[0] * x[0] - x[1] * x[1], zip(imes, imes_limit))) + ) + mu_i1 = project_dualvariable(mu_i1) + else: + mu_i1 = mu_i0 + mu1 = [mu__vmag_upper1, mu__vmag_lower1, mu_i1] + # =========================================Yiyun's Notes===========================================# + # Each time of calling DERMS.control, it is a one step update of PV real and reactive power outputs + # =================================================================================================# + + return [x1, mu1] diff --git a/src/pydss/pyPostprocessor/PostprocessScripts/EdLiFoControl.py b/src/pydss/py_postprocessor/postprocess_scripts/ed_li_fo_control.py similarity index 80% rename from src/pydss/pyPostprocessor/PostprocessScripts/EdLiFoControl.py rename to src/pydss/py_postprocessor/postprocess_scripts/ed_li_fo_control.py index b6617409..a1280498 100644 --- a/src/pydss/pyPostprocessor/PostprocessScripts/EdLiFoControl.py +++ b/src/pydss/py_postprocessor/postprocess_scripts/ed_li_fo_control.py @@ -8,7 +8,7 @@ import pandas as pd import opendssdirect as dss import numpy as np -from pydss.pyPostprocessor.pyPostprocessAbstract import AbstractPostprocess +from pydss.py_postprocessor.py_postprocess_abstract import AbstractPostprocess # from pydss.exceptions import InvalidParameter, OpenDssConvergenceError from pydss.utils.timing_utils import timed_info @@ -55,35 +55,33 @@ def compute_electric_distance(bus_phases=None): lines["phases"] = pd.to_numeric(lines["phases"]) if bus_phases is not None: - lines = lines.loc[ - lines["phases"] == bus_phases, ["bus1", "bus2", "rmatrix"] - ].copy() + lines = lines.loc[lines["phases"] == bus_phases, ["bus1", "bus2", "rmatrix"]].copy() lines["g"] = lines["rmatrix"].apply(get_g) busall = np.unique((list(lines["bus1"]) + list(lines["bus2"]))) - disGmat_df = pd.DataFrame(0, index=busall, columns=busall) + dis_gmat_df = pd.DataFrame(0, index=busall, columns=busall) for line in lines.index: - disGmat_df.loc[lines.loc[line, "bus1"], lines.loc[line, "bus2"]] = -lines.loc[line, "g"] - disGmat_df.loc[lines.loc[line, "bus2"], lines.loc[line, "bus1"]] = -lines.loc[line, "g"] + dis_gmat_df.loc[lines.loc[line, "bus1"], lines.loc[line, "bus2"]] = -lines.loc[line, "g"] + dis_gmat_df.loc[lines.loc[line, "bus2"], lines.loc[line, "bus1"]] = -lines.loc[line, "g"] for bus in busall: - disGmat_df.loc[bus, bus] = -sum(disGmat_df.loc[bus, :]) + dis_gmat_df.loc[bus, bus] = -sum(dis_gmat_df.loc[bus, :]) - disRmat_df = pd.DataFrame( - np.linalg.pinv(np.array(disGmat_df)), - index=disGmat_df.index, - columns=disGmat_df.columns, + dis_rmat_df = pd.DataFrame( + np.linalg.pinv(np.array(dis_gmat_df)), + index=dis_gmat_df.index, + columns=dis_gmat_df.columns, ) dismat_df = pd.DataFrame(0, index=busall, columns=busall) - for bus_i in disGmat_df.index: - for bus_j in disGmat_df.columns: + for bus_i in dis_gmat_df.index: + for bus_j in dis_gmat_df.columns: dismat_df.loc[bus_i, bus_j] = ( - disRmat_df.loc[bus_i, bus_i] - + disRmat_df.loc[bus_j, bus_j] - - disRmat_df.loc[bus_i, bus_j] - - disRmat_df.loc[bus_j, bus_i] + dis_rmat_df.loc[bus_i, bus_i] + + dis_rmat_df.loc[bus_j, bus_j] + - dis_rmat_df.loc[bus_i, bus_j] + - dis_rmat_df.loc[bus_j, bus_i] ) return dismat_df @@ -96,9 +94,7 @@ def choose_zone_radius(dismat_df): avg_distance = np.mean(np.array(dismat_df)) std_distance = np.std(np.array(dismat_df)) dispersion_factor = std_distance / avg_distance - k = ( - -0.8 - ) # zone_radius calibration factor bounded by the inverse of the dispersion factor + k = -0.8 # zone_radius calibration factor bounded by the inverse of the dispersion factor if abs(k) > 1 / dispersion_factor: k = -0.8 / dispersion_factor # to avoid negative zone radius zone_thr = round(avg_distance + k * std_distance, 4) @@ -121,7 +117,7 @@ def parse_pv_scenario(file_path, min_lifo_pv_size): Sample syntax: PV_dict, PV_dataframe = parse_pv_scenario(deployment_path + deployment_name) """ - PVsys_dict = dict() + p_vsys_dict = dict() attribute_list = [ "phases", "bus1", @@ -137,24 +133,22 @@ def parse_pv_scenario(file_path, min_lifo_pv_size): ] if os.path.exists(file_path): - with open(file_path, "r") as depfile: for line in depfile.readlines(): pvname = line.split("PVSystem.")[1].split()[0].lower() - PVsys_dict[pvname] = dict() - PVsys_dict[pvname]["pvname"] = pvname + p_vsys_dict[pvname] = dict() + p_vsys_dict[pvname]["pvname"] = pvname for att in attribute_list: - PVsys_dict[pvname][att] = string_attribute_parser(att, line) + p_vsys_dict[pvname][att] = string_attribute_parser(att, line) else: flag = dss.PVsystems.First() while flag > 0: pvname = dss.PVsystems.Name().lower() - PVsys_dict[pvname] = dict() - PVsys_dict[pvname]["pvname"] = pvname + p_vsys_dict[pvname] = dict() + p_vsys_dict[pvname]["pvname"] = pvname for att in attribute_list: - if att in [ "kV", "irradiance", @@ -165,13 +159,13 @@ def parse_pv_scenario(file_path, min_lifo_pv_size): "%cutout", "Vmaxpu", ]: - PVsys_dict[pvname][att] = float(dss.Properties.Value(att)) + p_vsys_dict[pvname][att] = float(dss.Properties.Value(att)) else: - PVsys_dict[pvname][att] = dss.Properties.Value(att) + p_vsys_dict[pvname][att] = dss.Properties.Value(att) flag = dss.PVsystems.Next() - return PVsys_dict, pd.DataFrame.from_dict(PVsys_dict, "index") + return p_vsys_dict, pd.DataFrame.from_dict(p_vsys_dict, "index") def get_monitored_line_dataframe(phase_info=3): @@ -186,9 +180,7 @@ def get_monitored_line_dataframe(phase_info=3): if phase_info != 3: monitored_lines = lines else: - monitored_lines = lines.loc[ - lines["phases"] == 3, ["bus1", "bus2", "rmatrix"] - ].copy() + monitored_lines = lines.loc[lines["phases"] == 3, ["bus1", "bus2", "rmatrix"]].copy() return monitored_lines @@ -209,14 +201,10 @@ def check_line_overloads(monitored_lines): line_name = dss.CktElement.Name() line_limit = dss.CktElement.NormalAmps() raw_current = dss.CktElement.Currents() - line_current = [ - math.sqrt(i ** 2 + j ** 2) - for i, j in zip(raw_current[::2], raw_current[1::2]) - ] + line_current = [math.sqrt(i**2 + j**2) for i, j in zip(raw_current[::2], raw_current[1::2])] ldg = max(line_current) / float(line_limit) if ldg > 1.0: - overloaded_line_dict[line_name] = ldg * 100 affected_buses.add(dss.CktElement.BusNames()[0]) @@ -247,7 +235,7 @@ def form_pvzones(affected_buses, dismat_df, zone_thr): return zone -class EdLiFoControl(AbstractPostprocess): +class EdLifoControl(AbstractPostprocess): """ Electric distance last-in First-out curtailment based PV output control """ @@ -268,28 +256,28 @@ def __init__( project, scenario, inputs, - dssInstance, - dssSolver, - dssObjects, - dssObjectsByClass, - simulationSettings, - Logger, + dss_instance, + dss_solver, + dss_objects, + dss_objects_by_class, + simulation_settings, + logger, ): """ Constructor method """ - super(EdLiFoControl, self).__init__( + super().__init__( project, scenario, inputs, - dssInstance, - dssSolver, - dssObjects, - dssObjectsByClass, - simulationSettings, - Logger, + dss_instance, + dss_solver, + dss_objects, + dss_objects_by_class, + simulation_settings, + logger, ) - dss = dssInstance + dss = dss_instance if isinstance(self.config["curtailment_size"], numbers.Number): self.curtailment_size = self.config["curtailment_size"] @@ -306,7 +294,6 @@ def __init__( self.dismat_df.to_csv(self.electrical_distance_file_path) else: - self.dismat_df = pd.read_csv(self.electrical_distance_file_path, index_col=0) ( @@ -332,9 +319,7 @@ def __init__( self.lifo_min_pv_size = self.config["lifo_min_pv_size"] self.user_lifo_pv_list = self.config["user_lifo_pv_list"] - self.pvdict, self.pvdf = parse_pv_scenario( - self.lifo_pv_path, self.lifo_min_pv_size - ) + self.pvdict, self.pvdf = parse_pv_scenario(self.lifo_pv_path, self.lifo_min_pv_size) self.pvdf3 = self.pvdf[self.pvdf["phases"] == "3"].copy() self.export_path = os.path.join(self.config["Outputs"]) self.monitored_lines = get_monitored_line_dataframe("all") @@ -377,9 +362,7 @@ def get_lifo_ordered_list(self): self.poa_lifo_ordered_pv_list = list(ordered_pv_df.index) if list(self.pvdf3.phases): self.poa_lifo_ordered_pv_list = [ - p - for p in self.poa_lifo_ordered_pv_list - if p in list(self.pvdf3.index) + p for p in self.poa_lifo_ordered_pv_list if p in list(self.pvdf3.index) ] else: self.poa_lifo_ordered_pv_list = self.user_lifo_pv_list @@ -395,24 +378,22 @@ def record_curtailment(self, pv_sys): for k, x in self.pvdict.items() if pv_sys == k ][0] - power_PV = dss.CktElement.Powers() - net_power_PV = sum(power_PV[::2]) - net_kvar_PV = sum(power_PV[1::2]) + power_pv = dss.CktElement.Powers() + net_power_pv = sum(power_pv[::2]) + net_kvar_pv = sum(power_pv[1::2]) self.PV_kW_curtailed[pv_sys + "Ref"] = self.init_net_power_PV - self.PV_kW_curtailed[pv_sys + "Actual"] = net_power_PV - self.PV_kW_curtailed[pv_sys + "NetCurt"] = self.init_net_power_PV - net_power_PV + self.PV_kW_curtailed[pv_sys + "Actual"] = net_power_pv + self.PV_kW_curtailed[pv_sys + "NetCurt"] = self.init_net_power_PV - net_power_pv self.PV_kW_curtailed[pv_sys + "PctCurt"] = ( - 100 * abs(self.init_net_power_PV - net_power_PV) / max(1, abs(avail_power)) + 100 * abs(self.init_net_power_PV - net_power_pv) / max(1, abs(avail_power)) ) - self.PV_kW_curtailed[pv_sys + "kVarActual"] = net_kvar_PV + self.PV_kW_curtailed[pv_sys + "kVarActual"] = net_kvar_pv self.total_kVA_curtailed += self.PV_curtailed[pv_sys] self.total_kW_curtailed += self.PV_kW_curtailed[pv_sys + "NetCurt"] self.total_init_kW_deployed += self.PV_kW_curtailed[pv_sys + "Ref"] - self.total_kW_output += net_power_PV - self.logger.info( - f"Total curtailment for PVSystem.{pv_sys}: {self.total_kVA_curtailed}" - ) + self.total_kW_output += net_power_pv + self.logger.info(f"Total curtailment for PVSystem.{pv_sys}: {self.total_kVA_curtailed}") which_bus = dss.Properties.Value("bus1") dss.Circuit.SetActiveBus(which_bus) @@ -425,7 +406,6 @@ def solve_overloads(self): Uses the LiFo logic to solve line and transformer overloads """ for pv_sys in self.my_lifo_list: - dss.Circuit.SetActiveClass("PVsystems") if not dss.Circuit.SetActiveElement(f"PVSystem.{pv_sys}") > 0: @@ -448,9 +428,7 @@ def solve_overloads(self): oldpctpmpp = self.init_pctpmpp[pv_sys] while ( - self.affected_buses - and self.pvs_df.loc[pv_sys, "kVARated"] > 0 - and oldpctpmpp > 0 + self.affected_buses and self.pvs_df.loc[pv_sys, "kVARated"] > 0 and oldpctpmpp > 0 ): """ This while loop breaks the above conditions or when all PVs are curtailed, @@ -460,7 +438,6 @@ def solve_overloads(self): dss.Circuit.SetActiveElement(f"PVSystem.{pv_sys}") if self.curtail_option == "%Pmpp": - self.logger.info(f"Old %Pmpp: {oldpctpmpp}") newpctpmpp = max(oldpctpmpp - self.pctpmpp_step, 0) @@ -475,34 +452,27 @@ def solve_overloads(self): self.PV_curtailed[pv_sys] += min( self.curtailment_size, self.pvs_df.loc[pv_sys, "kVARated"] ) - new_kVA = max( - self.pvs_df.loc[pv_sys, "kVARated"] - self.curtailment_size, 0 - ) - dss.run_command(f"Edit PVSystem.{pv_sys} kVA={new_kVA}") + new_k_va = max(self.pvs_df.loc[pv_sys, "kVARated"] - self.curtailment_size, 0) + dss.run_command(f"Edit PVSystem.{pv_sys} kVA={new_k_va}") dss.run_command("_SolvePFlow") - self.overloads_df, self.affected_buses = check_line_overloads( - self.monitored_lines - ) + self.overloads_df, self.affected_buses = check_line_overloads(self.monitored_lines) self.pvs_df = dss.utils.pvsystems_to_dataframe() if self.curtail_option == "%Pmpp": oldpctpmpp = newpctpmpp if self.affected_buses and oldpctpmpp == 0: - self.comment = ( - f"{pv_sys} is fully curtailed but overloads still exist!" - ) + self.comment = f"{pv_sys} is fully curtailed but overloads still exist!" elif self.curtail_option == "kva": if ( self.affected_buses - and max( - self.pvs_df.loc[self.poa_lifo_ordered_pv_list, "kVARated"] - ) - == 0 + and max(self.pvs_df.loc[self.poa_lifo_ordered_pv_list, "kVARated"]) == 0 ): - self.comment = "All PV systems have been curtailed, but overloads still exist!" + self.comment = ( + "All PV systems have been curtailed, but overloads still exist!" + ) if not self.affected_buses: self.solution_status = 1 @@ -518,9 +488,7 @@ def solve_overloads(self): for pv_sys in self.my_lifo_list: inipctpmpp = self.init_pctpmpp[pv_sys] inikva = self.init_kVA[pv_sys] - dss.run_command( - f"Edit PVSystem.{pv_sys} %Pmpp={inipctpmpp} kVA={inikva}" - ) + dss.run_command(f"Edit PVSystem.{pv_sys} %Pmpp={inipctpmpp} kVA={inikva}") def get_curtailment_candidates(self): """ @@ -544,7 +512,7 @@ def get_curtailment_candidates(self): if self.zone_option == "Yes" and lifo_pv_list: self.my_lifo_list = lifo_pv_list + [ - x for x in self.poa_lifo_ordered_pv_list if not x in lifo_pv_list + x for x in self.poa_lifo_ordered_pv_list if x not in lifo_pv_list ] else: @@ -564,13 +532,13 @@ def poa_curtail(self): self.pvbus_voltage = dict() self.solution_status = -1 self.comment = "RAS" - total_kVA_deployed = self.pvs_df["kVARated"].sum() + total_k_va_deployed = self.pvs_df["kVARated"].sum() self.total_init_kW_deployed = 0 # #print("Total PV KVA deployed: ",total_kVA_deployed) self.total_kVA_curtailed = 0 self.total_kW_curtailed = 0 self.total_kW_output = 0 - Pct_kW_curtailment = 0 + pct_k_w_curtailment = 0 for pv_sys in self.poa_lifo_ordered_pv_list: # pv_sys_n = remove_pen(pv_sys) @@ -582,18 +550,13 @@ def poa_curtail(self): self.PV_kW_curtailed[pv_sys + "PctCurt"] = 0 self.PV_kW_curtailed[pv_sys + "%Pmpp"] = 100 - self.overloads_df, self.affected_buses = check_line_overloads( - self.monitored_lines - ) + self.overloads_df, self.affected_buses = check_line_overloads(self.monitored_lines) self.logger.info(f"Number of buses affected: {len(self.affected_buses)}") self.zone = [] if self.affected_buses: - - self.zone = form_pvzones( - self.affected_buses, self.dismat_df, self.zone_threshold - ) + self.zone = form_pvzones(self.affected_buses, self.dismat_df, self.zone_threshold) # #print('zone length:',len(zone)) else: @@ -610,7 +573,6 @@ def poa_curtail(self): self.logger.info(f"There are {number_ol} persisting overloads") elif self.my_lifo_list: - self.init_pctpmpp = {} self.init_kVA = {} self.init_net_power_PV = 0 @@ -627,13 +589,13 @@ def poa_curtail(self): self.solution_status == 0 self.PV_curtailed["Comment"] = self.comment - Pct_curtailment = self.total_kVA_curtailed * 100 / max(1, total_kVA_deployed) - self.PV_curtailed["Pct_curtailment"] = Pct_curtailment + pct_curtailment = self.total_kVA_curtailed * 100 / max(1, total_k_va_deployed) + self.PV_curtailed["Pct_curtailment"] = pct_curtailment - Pct_kW_curtailment = ( + pct_k_w_curtailment = ( abs(self.total_kW_curtailed) * 100 / max(1, abs(self.total_init_kW_deployed)) ) - self.PV_kW_curtailed["Pct_kW_curtailment"] = Pct_kW_curtailment + self.PV_kW_curtailed["Pct_kW_curtailment"] = pct_k_w_curtailment self.PV_kW_curtailed["Total_PV_kW_deployed"] = self.total_init_kW_deployed self.PV_kW_curtailed["Total_PV_kW_curtailed"] = self.total_kW_curtailed self.PV_kW_curtailed["Total_PV_kW_output"] = self.total_kW_output @@ -646,7 +608,7 @@ def poa_curtail(self): self.error = sol.Convergence() # This error is fake for now, find how to get this from Opendssdirect - def run(self, step, stepMax, simulation=None): + def run(self, step, step_max, simulation=None): """ Runs edLiFo """ diff --git a/src/pydss/py_postprocessor/py_postprocess.py b/src/pydss/py_postprocessor/py_postprocess.py new file mode 100644 index 00000000..f6850773 --- /dev/null +++ b/src/pydss/py_postprocessor/py_postprocess.py @@ -0,0 +1,40 @@ +from pydss.py_postprocessor.postprocess_scripts.derms_optimizer import DermsOptimizer +from pydss.py_postprocessor.postprocess_scripts.ed_li_fo_control import EdLifoControl + + +POST_PROCESSES = { + "DERMSOptimizer": DermsOptimizer, + "EdLiFoControl": EdLifoControl, + "derms_optimizer": DermsOptimizer, + "ed_li_fo_control": EdLifoControl, +} + + +def create( + project, + scenario, + pp_info, + dss_instance, + dss_solver, + dss_objects, + dss_objects_by_class, + simulation_settings, + logger, +): + script_name = pp_info.script + assert script_name in POST_PROCESSES, ( + f"Definition for '{script_name}' post process script not found. \n" + "Please define the controller in PyDSS/py_postprocessor/postprocess_scripts" + ) + post_processor = POST_PROCESSES[script_name]( + project, + scenario, + pp_info, + dss_instance, + dss_solver, + dss_objects, + dss_objects_by_class, + simulation_settings, + logger, + ) + return post_processor diff --git a/src/pydss/pyPostprocessor/pyPostprocessAbstract.py b/src/pydss/py_postprocessor/py_postprocess_abstract.py similarity index 66% rename from src/pydss/pyPostprocessor/pyPostprocessAbstract.py rename to src/pydss/py_postprocessor/py_postprocess_abstract.py index 2220573c..82182b13 100644 --- a/src/pydss/pyPostprocessor/pyPostprocessAbstract.py +++ b/src/pydss/py_postprocessor/py_postprocess_abstract.py @@ -1,4 +1,3 @@ - import abc import os @@ -15,22 +14,32 @@ class AbstractPostprocess(abc.ABC): :type scenario: PyDssScenario :param inputs: user inputs :type inputs: dict - :param dssInstance: A :class:`pydss.dssElement.dssElement` object that wraps around an OpenDSS 'Fault' element + :param dssInstance: A :class:`pydss.dss_element.DssElement` object that wraps around an OpenDSS 'Fault' element :type dssInstance: dict - :param dssBuses: Dictionary of all :class:`pydss.dssBus.dssBus` objects in pydss - :type dssBuses: dict of :class:`pydss.dssBus.dssBus` objects - :param dssObjects: Dictionary of all :class:`pydss.dssElement.dssElement` objects in pydss - :type dssObjects: dict of :class:`pydss.dssElement.dssElement` objects - :param dssObjectsByClass: Dictionary of all :class:`pydss.dssElement.dssElement` objects in pydss sorted by class - :type dssObjectsByClass: dict of :class:`pydss.dssElement.dssElement` objects - :param dssSolver: An instance of one of the classes defined in :mod:`pydss.SolveMode`. - :type dssSolver: :mod:`pydss.SolveMode` + :param dssBuses: Dictionary of all :class:`pydss.dss_bus.DssBus` objects in pydss + :type dssBuses: dict of :class:`pydss.dss_bus.DssBus` objects + :param dssObjects: Dictionary of all :class:`pydss.dss_element.DssElement` objects in pydss + :type dssObjects: dict of :class:`pydss.dss_element.DssElement` objects + :param dssObjectsByClass: Dictionary of all :class:`pydss.dss_element.DssElement` objects in pydss sorted by class + :type dssObjectsByClass: dict of :class:`pydss.dss_element.DssElement` objects + :param dssSolver: An instance of one of the classes defined in :mod:`pydss.solve_mode`. + :type dssSolver: :mod:`pydss.solve_mode` """ - def __init__(self, project, scenario, inputs, dssInstance, dssSolver, dssObjects, dssObjectsByClass, simulationSettings, logger): - """This is the constructor class. - """ + def __init__( + self, + project, + scenario, + inputs, + dss_instance, + dss_solver, + dss_objects, + dss_objects_by_class, + simulation_settings, + logger, + ): + """This is the constructor class.""" self.project = project self.scenario = scenario if inputs.config_file == "": @@ -39,14 +48,14 @@ def __init__(self, project, scenario, inputs, dssInstance, dssSolver, dssObjects self.config = load_data(inputs.config_file) self.config["Outputs"] = project.get_post_process_directory(scenario.name) os.makedirs(self.config["Outputs"], exist_ok=True) - self.Settings = simulationSettings + self.Settings = simulation_settings - self._dssInstance = dssInstance + self._dssInstance = dss_instance self.logger = logger self._check_input_fields() @abc.abstractmethod - def run(self, step, stepMax, simulation=None): + def run(self, step, step_max, simulation=None): """Method used to run a post processing script. Parameters @@ -67,9 +76,8 @@ def _get_required_input_fields(self): @abc.abstractmethod def finalize(self): - """Method used to combine post processing results from all steps. - """ - + """Method used to combine post processing results from all steps.""" + def _check_input_fields(self): required_fields = self._get_required_input_fields() fields = set(self.config.keys()) diff --git a/src/pydss/py_results.py b/src/pydss/py_results.py new file mode 100644 index 00000000..2ef61375 --- /dev/null +++ b/src/pydss/py_results.py @@ -0,0 +1,108 @@ +import pandas as pd +import numpy as np +import os + + +class PyContrReader: + def __init__(self, path): + self.pyControllers = {} + filenames = os.listdir(path) + for filename in filenames: + if filename.endswith(".xlsx") and not filename.startswith("~$"): + controller_type = filename.split(".")[0] + filepath = os.path.join(path, filename) + assert os.path.exists(filepath), 'path: "{}" does not exist!'.format(filepath) + controller_dataset = pd.read_excel( + filepath, + skiprows=[ + 0, + ], + index_col=[0], + ) + controller_names = controller_dataset.index.tolist() + controllers = {} + for controller_name in controller_names: + controller_data = controller_dataset.loc[controller_name] + assert len(controller_data == 1), ( + "Multiple pydss controller definitions for a single OpenDSS " + + "element not allowed" + ) + controller_dict = controller_data.to_dict() + controllers[controller_name] = controller_dict + self.pyControllers[controller_type] = controllers + + +class PySubscriptionReader: + def __init__(self, file_path): + self.SubscriptionDict = {} + assert os.path.exists(file_path), 'path: "{}" does not exist!'.format(file_path) + subscription_data = pd.read_excel( + file_path, + skiprows=[ + 0, + ], + index_col=[0], + ) + required_columns = {"Property", "Subscription ID", "Unit", "Subscribe", "Data type"} + file_columns = set(subscription_data.columns) + diff = required_columns.difference(file_columns) + + assert len(diff) == 0, ( + "Missing column in the subscriptions file.\nRequired columns: {}".format( + required_columns + ) + ) + subscribe = subscription_data["Subscribe"] + assert subscribe.dtype == bool, "The subscribe column can only have boolean values." + self.SubscriptionDict = subscription_data.T.to_dict() + + +class PyExportReader: + def __init__(self, file_path): + self.pyControllers = {} + self.publicationList = [] + assert os.path.exists(file_path), 'path: "{}" does not exist!'.format(file_path) + controller_dataset = pd.read_excel( + file_path, + skiprows=[ + 0, + ], + index_col=[0], + ) + assert controller_dataset.columns[0] == "Publish", ( + "First column after class declarations in the " + + "export defination files should have column " + + 'name "Publish"' + ) + + publish = controller_dataset["Publish"] + assert publish.dtype == bool, "The publish column can only have boolean values." + filtered_dataset = controller_dataset[controller_dataset.columns[1:]] + + controller_names = filtered_dataset.index.tolist() + for controller_name, do_publish in zip(controller_names, publish.values): + controller_data = filtered_dataset.loc[controller_name] + publish_data = controller_dataset["Publish"].loc[controller_name] + if isinstance(publish_data, np.bool_): + publish_data = [publish_data] + else: + publish_data = publish_data.dropna().values + data = controller_data.copy() + data.index = range(len(data)) + for i, should_publish in enumerate(publish_data): + if should_publish: + if isinstance(data, pd.core.frame.DataFrame): + properties = data.loc[i].dropna() + else: + properties = data.dropna().values + for property_name in properties: + self.publicationList.append("{} {}".format(controller_name, property_name)) + + if len(controller_data) > 1: + controller_data = pd.Series(controller_data.values.flatten()) + controller_dict = controller_data.dropna().to_dict() + else: + controller_dict = controller_data.dropna().to_dict() + + self.pyControllers[controller_name] = controller_dict + self.publicationList = list(set(self.publicationList)) diff --git a/src/pydss/pydss_fs_interface.py b/src/pydss/pydss_fs_interface.py index 82c76c02..613c2864 100644 --- a/src/pydss/pydss_fs_interface.py +++ b/src/pydss/pydss_fs_interface.py @@ -8,12 +8,17 @@ import tarfile import zipfile -from loguru import logger import pandas as pd import toml -from pydss.common import PLOTS_FILENAME, PROJECT_TAR, PROJECT_ZIP, \ - ControllerType, ExportMode, SIMULATION_SETTINGS_FILENAME +from pydss.common import ( + PLOTS_FILENAME, + PROJECT_TAR, + PROJECT_ZIP, + ControllerType, + ExportMode, + SIMULATION_SETTINGS_FILENAME, +) from pydss.exceptions import InvalidConfiguration from pydss.simulation_input_models import SimulationSettingsModel, load_simulation_settings from pydss.utils.utils import load_data @@ -23,6 +28,7 @@ SCENARIOS = "Scenarios" PROJECT_DIRECTORIES = ("DSSfiles", "Exports", "Logs", "Scenarios") + class PyDssFileSystemInterface(abc.ABC): """Interface to read pydss files on differing filesystem structures.""" @@ -140,7 +146,6 @@ def read_scenario_pv_profiles(self, scenario_name): """ - @property def scenario_names(self): """Return the scenario names in the project. @@ -191,14 +196,13 @@ def _list_scenario_names(self): class PyDssFileSystemInterface(PyDssFileSystemInterface): """Reads pydss files when the project is expanded into directories.""" + def __init__(self, project_dir, simulation_file): self._project_dir = project_dir self._scenarios_dir = os.path.join(self._project_dir, SCENARIOS) self._dss_dir = os.path.join(self._project_dir, "DSSfiles") - self._settings = load_simulation_settings( - os.path.join(self._project_dir, simulation_file) - ) + self._settings = load_simulation_settings(os.path.join(self._project_dir, simulation_file)) self._check_scenarios() @@ -217,7 +221,8 @@ def read_csv(self, path): def _list_scenario_names(self): scenarios = [ - x for x in os.listdir(self._scenarios_dir) + x + for x in os.listdir(self._scenarios_dir) if os.path.isdir(os.path.join(self._scenarios_dir, x)) ] scenarios.sort() @@ -280,6 +285,7 @@ def simulation_config(self): class PyDssArchiveFileInterfaceBase(PyDssFileSystemInterface): """Base class for archive types.""" + def __init__(self, project_dir): self._project_dir = project_dir data = self._load_data(SIMULATION_SETTINGS_FILENAME) @@ -308,7 +314,7 @@ def _list_scenario_names(self): scenarios = None with pd.HDFStore(store_filename, "r") as store: - for (path, subgroups, _) in store.walk(): + for path, subgroups, _ in store.walk(): if path == "/Exports": scenarios = subgroups break @@ -369,6 +375,7 @@ def read_scenario_pv_profiles(self, scenario_name): class PyDssTarFileInterface(PyDssArchiveFileInterfaceBase): """Reads pydss files when the project is archived in tar file.""" + def __init__(self, project_dir): tar_path = os.path.join(project_dir, PROJECT_TAR) self._tar = tarfile.open(tar_path) @@ -390,10 +397,9 @@ def read_csv(self, path): return pd.read_csv(self._tar.extractfile(os.path.normpath(path).replace("\\", "/"))) - - class PyDssZipFileInterface(PyDssArchiveFileInterfaceBase): """Reads pydss files when the project is archived in zip file.""" + def __init__(self, project_dir): self._zip = zipfile.ZipFile(os.path.join(project_dir, PROJECT_ZIP)) super(PyDssZipFileInterface, self).__init__(project_dir) diff --git a/src/pydss/pydss_project.py b/src/pydss/pydss_project.py index 07f4b825..699742d2 100644 --- a/src/pydss/pydss_project.py +++ b/src/pydss/pydss_project.py @@ -1,4 +1,5 @@ """Contains functionality to configure pydss simulations.""" + import os import shutil import sys @@ -12,18 +13,33 @@ from loguru import logger import pydss -from pydss.common import PROJECT_TAR, PROJECT_ZIP, CONTROLLER_TYPES, \ - SIMULATION_SETTINGS_FILENAME, DEFAULT_SIMULATION_SETTINGS_FILE, \ - ControllerType, ExportMode, SnapshotTimePointSelectionMode, MONTE_CARLO_SETTINGS_FILENAME,\ - filename_from_enum, DEFAULT_MONTE_CARLO_SETTINGS_FILE,\ - SUBSCRIPTIONS_FILENAME, DEFAULT_SUBSCRIPTIONS_FILE, OPENDSS_MASTER_FILENAME, \ - RUN_SIMULATION_FILENAME +from pydss.common import ( + PROJECT_TAR, + PROJECT_ZIP, + SIMULATION_SETTINGS_FILENAME, + DEFAULT_SIMULATION_SETTINGS_FILE, + ControllerType, + ExportMode, + SnapshotTimePointSelectionMode, + MONTE_CARLO_SETTINGS_FILENAME, + filename_from_enum, + DEFAULT_MONTE_CARLO_SETTINGS_FILE, + SUBSCRIPTIONS_FILENAME, + DEFAULT_SUBSCRIPTIONS_FILE, + OPENDSS_MASTER_FILENAME, + RUN_SIMULATION_FILENAME, +) from pydss.exceptions import InvalidParameter, InvalidConfiguration -from pydss.pyDSS import instance -from pydss.pydss_fs_interface import PyDssFileSystemInterface, \ - PyDssArchiveFileInterfaceBase, PyDssTarFileInterface, \ - PyDssZipFileInterface, PROJECT_DIRECTORIES, \ - SCENARIOS, STORE_FILENAME +from pydss.py_dss import instance +from pydss.pydss_fs_interface import ( + PyDssFileSystemInterface, + PyDssArchiveFileInterfaceBase, + PyDssTarFileInterface, + PyDssZipFileInterface, + PROJECT_DIRECTORIES, + SCENARIOS, + STORE_FILENAME, +) from pydss.reports.reports import REPORTS_DIR from pydss.registry import Registry from pydss.simulation_input_models import ( @@ -53,8 +69,15 @@ class PyDssProject: _SKIP_ARCHIVE = (PROJECT_ZIP, PROJECT_TAR, STORE_FILENAME, REPORTS_DIR) - def __init__(self, path, name, scenarios, settings: SimulationSettingsModel, fs_intf=None, - simulation_file=SIMULATION_SETTINGS_FILENAME): + def __init__( + self, + path, + name, + scenarios, + settings: SimulationSettingsModel, + fs_intf=None, + simulation_file=SIMULATION_SETTINGS_FILENAME, + ): self._name = name self._scenarios = scenarios self._settings = settings @@ -65,7 +88,7 @@ def __init__(self, path, name, scenarios, settings: SimulationSettingsModel, fs_ self._simulation_file = simulation_file self._scenarios_dir = os.path.join(self._project_dir, SCENARIOS) self._fs_intf = fs_intf # Only needed for reading a project that was - # already executed. + # already executed. self._hdf_store = None self._estimated_space = {} @@ -154,9 +177,7 @@ def get_post_process_directory(self, scenario_name): """ # Make sure the scenario exists. This will throw if not. self.get_scenario(scenario_name) - return os.path.join( - self._project_dir, "Scenarios", scenario_name, "PostProcess" - ) + return os.path.join(self._project_dir, "Scenarios", scenario_name, "PostProcess") def get_scenario(self, name): """Return the scenario with name. @@ -224,7 +245,7 @@ def simulation_config(self): @property def estimated_space(self): """Return the estimated space in bytes. - + Returns ------- int @@ -247,10 +268,18 @@ def serialize(self, opendss_project_folder): logger.info("Initialized directories in %s", self._project_dir) @classmethod - - def create_project(cls, path, name, scenarios, simulation_config=None, options=None, - simulation_file=SIMULATION_SETTINGS_FILENAME, opendss_project_folder=None, - master_dss_file=OPENDSS_MASTER_FILENAME, force=False): + def create_project( + cls, + path, + name, + scenarios, + simulation_config=None, + options=None, + simulation_file=SIMULATION_SETTINGS_FILENAME, + opendss_project_folder=None, + master_dss_file=OPENDSS_MASTER_FILENAME, + force=False, + ): """Create a new PyDssProject on the filesystem. Parameters @@ -286,8 +315,7 @@ def create_project(cls, path, name, scenarios, simulation_config=None, options=N ) project.serialize(opendss_project_folder=opendss_project_folder) sc_names = project.list_scenario_names() - logger.info("Created project=%s with scenarios=%s at %s", name, - sc_names, path) + logger.info("Created project=%s with scenarios=%s at %s", name, sc_names, path) return project def read_scenario_export_metadata(self, scenario_name): @@ -332,11 +360,10 @@ def run(self, logging_configured=True, tar_project=False, zip_project=False, dry filename = os.path.join(self._project_dir, "Logs", "pydss.log") else: filename = None - file_level = "INFO" logger.level(console_level) if filename: logger.add(filename) - + if dry_run: store_filename = os.path.join(tempfile.gettempdir(), STORE_FILENAME) else: @@ -366,6 +393,7 @@ def run(self, logging_configured=True, tar_project=False, zip_project=False, dry if not dry_run and (export_tables or generate_reports): # Hack. Have to import here. Need to re-organize to fix. from pydss.pydss_results import PyDssResults + results = PyDssResults(self._project_dir) if export_tables: for scenario in results.scenarios: @@ -390,13 +418,16 @@ def run(self, logging_configured=True, tar_project=False, zip_project=False, dry def _dump_simulation_settings(self): # Various settings may have been updated. Write the actual settings to a file. - filename = os.path.join( self._project_dir, RUN_SIMULATION_FILENAME) + filename = os.path.join(self._project_dir, RUN_SIMULATION_FILENAME) dump_settings(self._settings, filename) def _serialize_scenarios(self): scenarios = [] for scenario in self._scenarios: - cfg = scenario.snapshot_time_point_selection_config or SnapshotTimePointSelectionConfigModel() + cfg = ( + scenario.snapshot_time_point_selection_config + or SnapshotTimePointSelectionConfigModel() + ) model = ScenarioModel( name=scenario.name, post_process_infos=[], @@ -404,16 +435,13 @@ def _serialize_scenarios(self): ) model.post_process_infos = scenario.post_process_infos scenarios.append(model) - scenario.serialize( - os.path.join(self._scenarios_dir, scenario.name) - ) + scenario.serialize(os.path.join(self._scenarios_dir, scenario.name)) self._settings.project.scenarios = scenarios def _tar_project_files(self, delete=True): orig = os.getcwd() os.chdir(self._project_dir) - skip_names = (PROJECT_ZIP, STORE_FILENAME, REPORTS_DIR) try: filename = PROJECT_TAR to_delete = [] @@ -431,7 +459,7 @@ def _tar_project_files(self, delete=True): else: shutil.rmtree(name) - path = os.path.join(self._project_dir, filename) + os.path.join(self._project_dir, filename) finally: os.chdir(orig) @@ -505,8 +533,8 @@ def load_project( """ name = os.path.basename(path) - #if simulation_file is None: - #simulation_file = SIMULATION_SETTINGS_FILENAME + # if simulation_file is None: + # simulation_file = SIMULATION_SETTINGS_FILENAME if os.path.exists(os.path.join(path, PROJECT_TAR)): fs_intf = PyDssTarFileInterface(path) @@ -542,8 +570,15 @@ def load_project( ) @classmethod - def run_project(cls, path, options=None, tar_project=False, zip_project=False, simulation_file=None, dry_run=False): - + def run_project( + cls, + path, + options=None, + tar_project=False, + zip_project=False, + simulation_file=None, + dry_run=False, + ): """Load a PyDssProject from directory and run all scenarios. Parameters @@ -582,7 +617,9 @@ def read_scenario_settings(self, scenario): settings_file = scenario_path / RUN_SIMULATION_FILENAME if not settings_file.exists(): - raise InvalidConfiguration(f"{RUN_SIMULATION_FILENAME} does not exist. Was the scenario run?") + raise InvalidConfiguration( + f"{RUN_SIMULATION_FILENAME} does not exist. Was the scenario run?" + ) return load_simulation_settings(settings_file) @@ -621,24 +658,29 @@ class PyDssScenario: "pyControllerList", "pyPlotList", "PostProcess", - 'Monte_Carlo' + "Monte_Carlo", ) REQUIRED_POST_PROCESS_FIELDS = ("script", "config_file") - def __init__(self, name, controller_types=None, controllers=None, - export_modes=None, exports=None, - post_process_infos=None, - snapshot_time_point_selection_config=None): + def __init__( + self, + name, + controller_types=None, + controllers=None, + export_modes=None, + exports=None, + post_process_infos=None, + snapshot_time_point_selection_config=None, + ): self.name = name self.post_process_infos = [] self.snapshot_time_point_selection_config = None - if (controller_types is None and controllers is None): + if controller_types is None and controllers is None: self.controllers = {} elif controller_types is not None: self.controllers = { - x: self.load_controller_config_from_type(x) - for x in controller_types + x: self.load_controller_config_from_type(x) for x in controller_types } elif isinstance(controllers, str): basename = os.path.splitext(os.path.basename(controllers))[0] @@ -649,16 +691,12 @@ def __init__(self, name, controller_types=None, controllers=None, self.controllers = controllers if export_modes is not None and exports is not None: - raise InvalidParameter( - "export_modes and exports cannot both be set" - ) - if (export_modes is None and exports is None): + raise InvalidParameter("export_modes and exports cannot both be set") + if export_modes is None and exports is None: mode = PyDssScenario.DEFAULT_EXPORT_MODE self.exports = {mode: self.load_export_config_from_mode(mode)} elif export_modes is not None: - self.exports = { - x: self.load_export_config_from_mode(x) for x in export_modes - } + self.exports = {x: self.load_export_config_from_mode(x) for x in export_modes} elif isinstance(exports, str): mode = ExportMode(os.path.splitext(os.path.basename(exports))[0]) self.exports = {mode: load_data(exports)} @@ -719,25 +757,20 @@ def serialize(self, path): os.makedirs(os.path.join(path, name), exist_ok=True) for controller_type, controllers in self.controllers.items(): - filename = os.path.join( - path, "pyControllerList", filename_from_enum(controller_type) - ) + filename = os.path.join(path, "pyControllerList", filename_from_enum(controller_type)) dump_data(controllers, filename) for mode, exports in self.exports.items(): - dump_data( - exports, - os.path.join(path, "ExportLists", filename_from_enum(mode)) - ) + dump_data(exports, os.path.join(path, "ExportLists", filename_from_enum(mode))) dump_data( load_data(DEFAULT_MONTE_CARLO_SETTINGS_FILE), - os.path.join(path, "Monte_Carlo", MONTE_CARLO_SETTINGS_FILENAME) + os.path.join(path, "Monte_Carlo", MONTE_CARLO_SETTINGS_FILENAME), ) dump_data( load_data(DEFAULT_SUBSCRIPTIONS_FILE), - os.path.join(path, "ExportLists", SUBSCRIPTIONS_FILENAME) + os.path.join(path, "ExportLists", SUBSCRIPTIONS_FILENAME), ) @staticmethod @@ -815,14 +848,12 @@ def load_config(path): dict """ - files = [os.path.join(path, x) for x in os.listdir(path) \ - if os.path.splitext(x)[1] == ".toml"] + files = [os.path.join(path, x) for x in os.listdir(path) if os.path.splitext(x)[1] == ".toml"] assert len(files) == 1, "only 1 .toml file is currently supported" return load_data(files[0]) -def update_pydss_controllers(project_path, scenario, controller_type, - controller, dss_file): +def update_pydss_controllers(project_path, scenario, controller_type, controller, dss_file): """Update a scenario's controllers from an OpenDSS file. Parameters diff --git a/src/pydss/pydss_results.py b/src/pydss/pydss_results.py index 554c727f..c95ab4c0 100644 --- a/src/pydss/pydss_results.py +++ b/src/pydss/pydss_results.py @@ -1,4 +1,5 @@ """Provides access to pydss result data.""" + from collections import defaultdict import json import os @@ -9,24 +10,23 @@ import pandas as pd from loguru import logger -from pydss.common import DatasetPropertyType +from pydss.common import DatasetPropertyType from pydss.dataset_buffer import DatasetBuffer from pydss.element_options import ElementOptions from pydss.exceptions import InvalidParameter from pydss.pydss_project import PyDssProject, RUN_SIMULATION_FILENAME from pydss.reports.reports import Reports, REPORTS_DIR from pydss.utils.dataframe_utils import read_dataframe, write_dataframe -from pydss.utils.utils import dump_data, load_data, make_json_serializable, \ - make_timestamps -from pydss.value_storage import ValueStorageBase, get_dataset_property_type, \ - get_time_step_path +from pydss.utils.utils import dump_data, load_data, make_json_serializable, make_timestamps +from pydss.value_storage import ValueStorageBase, get_dataset_property_type, get_time_step_path + class PyDssResults: """Interface to perform analysis on pydss output data.""" + def __init__( - self, project_path=None, project=None, in_memory=False, - frequency=False, mode=False - ): + self, project_path=None, project=None, in_memory=False, frequency=False, mode=False + ): """Constructs PyDssResults object. Parameters @@ -80,7 +80,7 @@ def __del__(self): self._hdf_store.flush() self._hdf_store.close() logger.info("store closed sucessfully") - + def generate_reports(self): """Generate all reports specified in the configuration. @@ -221,10 +221,10 @@ def simulation_config(self): class PyDssScenarioResults: """Contains results for one scenario.""" + def __init__( - self, name, project_path, store, fs_intf, metadata, options, - frequency=False, mode=False - ): + self, name, project_path, store, fs_intf, metadata, options, frequency=False, mode=False + ): self._name = name self._project_path = project_path self._hdf_store = store @@ -249,9 +249,7 @@ def __init__( return self._group = self._hdf_store[f"Exports/{name}"] - self._elem_classes = [ - x for x in self._group if isinstance(self._group[x], h5py.Group) - ] + self._elem_classes = [x for x in self._group if isinstance(self._group[x], h5py.Group)] self._parse_datasets() @@ -268,8 +266,8 @@ def _parse_datasets(self): self._elem_values_by_prop[elem_class][prop] = [] prop_names = self._elem_values_by_prop elif dataset_property_type in ( - DatasetPropertyType.PER_TIME_POINT, - DatasetPropertyType.FILTERED, + DatasetPropertyType.PER_TIME_POINT, + DatasetPropertyType.FILTERED, ): self._elem_data_by_prop[elem_class][prop] = [] prop_names = self._elem_data_by_prop @@ -279,8 +277,9 @@ def _parse_datasets(self): self._props_by_class[elem_class].append(prop) self._elem_indices_by_prop[elem_class][prop] = {} names = DatasetBuffer.get_names(dataset) - self._column_ranges_per_elem[elem_class][prop] = \ + self._column_ranges_per_elem[elem_class][prop] = ( DatasetBuffer.get_column_ranges(dataset) + ) for i, name in enumerate(names): self._elems_by_class[elem_class].add(name) prop_names[elem_class][prop].append(name) @@ -377,7 +376,7 @@ def _export_element_values(self, path, fmt, compress): if length == 1: val = dataset[:][0][start] else: - val = dataset[:][0][start: start + length] + val = dataset[:][0][start : start + length] if prop not in elem_prop_nums[elem_class]: elem_prop_nums[elem_class][prop] = {} elem_prop_nums[elem_class][prop][name] = val @@ -418,7 +417,9 @@ def _export_summed_element_values(self, path, fmt, compress): filename = os.path.join(path, "summed_element_property_values.json") dump_data(self._summed_elem_props, filename, default=make_json_serializable) - def get_dataframe(self, element_class, prop, element_name, real_only=False, abs_val=False, **kwargs): + def get_dataframe( + self, element_class, prop, element_name, real_only=False, abs_val=False, **kwargs + ): """Return the dataframe for an element. Parameters @@ -450,13 +451,23 @@ def get_dataframe(self, element_class, prop, element_name, real_only=False, abs_ prop_type = get_dataset_property_type(dataset) if prop_type == DatasetPropertyType.PER_TIME_POINT: return self._get_elem_prop_dataframe( - element_class, prop, element_name, dataset, real_only=real_only, - abs_val=abs_val, **kwargs + element_class, + prop, + element_name, + dataset, + real_only=real_only, + abs_val=abs_val, + **kwargs, ) elif prop_type == DatasetPropertyType.FILTERED: return self._get_filtered_dataframe( - element_class, prop, element_name, dataset, real_only=real_only, - abs_val=abs_val, **kwargs + element_class, + prop, + element_name, + dataset, + real_only=real_only, + abs_val=abs_val, + **kwargs, ) assert False, str(prop_type) @@ -604,7 +615,7 @@ def get_element_property_value(self, element_class, prop, element_name): length = col_range[1] if length == 1: return dataset[:][0][start] - return dataset[:][0][start: start + length] + return dataset[:][0][start : start + length] def get_option_values(self, element_class, prop, element_name): """Return the option values for the element property. @@ -621,7 +632,9 @@ def get_option_values(self, element_class, prop, element_name): df = self.get_dataframe(element_class, prop, element_name) return ValueStorageBase.get_option_values(df, element_name) - def get_summed_element_dataframe(self, element_class, prop, real_only=False, abs_val=False, group=None): + def get_summed_element_dataframe( + self, element_class, prop, real_only=False, abs_val=False, group=None + ): """Return the dataframe for a summed element property. Parameters @@ -867,9 +880,7 @@ def read_element_info_file(self, filename): if basename.replace("Info", "") == filename: actual = _file if actual is None: - raise InvalidParameter( - f"element info file for {filename} is not stored" - ) + raise InvalidParameter(f"element info file for {filename} is not stored") filename = actual return self._fs_intf.read_csv(filename) @@ -983,7 +994,9 @@ def _fix_columns(name, columns): cols.append(ValueStorageBase.DELIMITER.join(fields)) return cols - def _get_elem_prop_dataframe(self, elem_class, prop, name, dataset, real_only=False, abs_val=False, **kwargs): + def _get_elem_prop_dataframe( + self, elem_class, prop, name, dataset, real_only=False, abs_val=False, **kwargs + ): col_range = self._get_element_column_range(elem_class, prop, name) df = DatasetBuffer.to_dataframe(dataset, column_range=col_range) @@ -1000,8 +1013,9 @@ def _get_element_column_range(self, elem_class, prop, name): col_range = self._column_ranges_per_elem[elem_class][prop][elem_index] return col_range - def _get_filtered_dataframe(self, elem_class, prop, name, dataset, - real_only=False, abs_val=False, **kwargs): + def _get_filtered_dataframe( + self, elem_class, prop, name, dataset, real_only=False, abs_val=False, **kwargs + ): indices_df = self._get_indices_df() elem_index = self._elem_indices_by_prop[elem_class][prop][name] length = dataset.attrs["length"] @@ -1040,9 +1054,7 @@ def _get_indices_df(self): return self._indices_df def _make_indices_df(self): - data = { - "Timestamp": make_timestamps(self._group["Timestamp"][:, 0]) - } + data = {"Timestamp": make_timestamps(self._group["Timestamp"][:, 0])} if self._add_frequency: data["Frequency"] = self._group["Frequency"][:, 0] if self._add_mode: diff --git a/src/pydss/registry.py b/src/pydss/registry.py index 091e295c..d4efc9dd 100644 --- a/src/pydss/registry.py +++ b/src/pydss/registry.py @@ -21,21 +21,21 @@ "name": "NO_VRT", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/PvControllers.toml", + "pydss/py_controllers/controllers/settings/PvControllers.toml", ), }, { "name": "cpf", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/PvControllers.toml", + "pydss/py_controllers/controllers/settings/PvControllers.toml", ), }, { "name": "volt-var", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/PvControllers.toml", + "pydss/py_controllers/controllers/settings/PvControllers.toml", ), }, ], @@ -44,21 +44,21 @@ "name": "NO_VRT_DVS_test", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/VoltageRideThru.toml" + "pydss/py_controllers/controllers/settings/VoltageRideThru.toml", ), }, { "name": "1547_CAT_III_test", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/VoltageRideThru.toml" + "pydss/py_controllers/controllers/settings/VoltageRideThru.toml", ), }, { "name": "1547_CAT_III_DVS_test", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/VoltageRideThru.toml" + "pydss/py_controllers/controllers/settings/VoltageRideThru.toml", ), }, ], @@ -68,18 +68,19 @@ ControllerType.MOTOR_STALL.value: [], ControllerType.MOTOR_STALL_SIMPLE.value: [], ControllerType.FAULT_CONTROLLER.value: [], - ControllerType.DYNAMIC_VOLTAGE_SUPPORT.value:[{ + ControllerType.DYNAMIC_VOLTAGE_SUPPORT.value: [ + { "name": "DVS_test", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/DynamicVoltageSupport.toml" + "pydss/py_controllers/controllers/settings/DynamicVoltageSupport.toml", ), }, { "name": "DVS_VRT_test", "filename": os.path.join( os.path.dirname(getattr(pydss, "__path__")[0]), - "pydss/pyControllers/Controllers/Settings/DynamicVoltageSupport.toml" + "pydss/py_controllers/controllers/settings/DynamicVoltageSupport.toml", ), }, ], @@ -276,7 +277,7 @@ def unregister_controller(self, controller_type, name): if not self.is_controller_registered(controller_type, name): raise InvalidParameter(f"{controller_type} / {name} isn't registered") if self._is_default_controller(controller_type, name): - raise InvalidParameter(f"Cannot unregister a default controller") + raise InvalidParameter("Cannot unregister a default controller") self._controllers[controller_type].pop(name) self._serialize_registry() diff --git a/src/pydss/reports/__init__.py b/src/pydss/reports/__init__.py index 76deebf0..593ff8ec 100644 --- a/src/pydss/reports/__init__.py +++ b/src/pydss/reports/__init__.py @@ -3,5 +3,5 @@ __path__ = pkgutil.extend_path(__path__, __name__) -for _, modname, _ in pkgutil.walk_packages(path=__path__, prefix=__name__ + '.'): +for _, modname, _ in pkgutil.walk_packages(path=__path__, prefix=__name__ + "."): __import__(modname) diff --git a/src/pydss/reports/capacitor_change_count.py b/src/pydss/reports/capacitor_change_count.py index c52cce48..01dc2364 100644 --- a/src/pydss/reports/capacitor_change_count.py +++ b/src/pydss/reports/capacitor_change_count.py @@ -1,4 +1,3 @@ - import os from loguru import logger @@ -6,6 +5,7 @@ from pydss.reports.reports import ReportBase from pydss.utils.utils import dump_data + class CapacitorStateChangeReport(ReportBase): """Reports the state changes per Capacitor. @@ -23,9 +23,11 @@ def generate(self, output_dir): for scenario in self._results.scenarios: scenario_data = {"name": scenario.name, "capacitors": []} for capacitor in scenario.list_element_names("Capacitors"): - change_count = int(scenario.get_element_property_value( - "Capacitors", "TrackStateChanges", capacitor - )) + change_count = int( + scenario.get_element_property_value( + "Capacitors", "TrackStateChanges", capacitor + ) + ) changes = {"name": capacitor, "change_count": change_count} scenario_data["capacitors"].append(changes) data["scenarios"].append(scenario_data) diff --git a/src/pydss/reports/feeder_losses.py b/src/pydss/reports/feeder_losses.py index d426cfae..09e499b8 100644 --- a/src/pydss/reports/feeder_losses.py +++ b/src/pydss/reports/feeder_losses.py @@ -1,4 +1,3 @@ - from typing import Annotated, Dict from datetime import timedelta import math @@ -10,38 +9,51 @@ from pydss.reports.reports import ReportBase + class FeederLossesMetricsModel(BaseModel): """Data model for metrics describing feeder losses""" - model_config = ConfigDict(title="FeederLossesMetricsModel", str_strip_whitespace=True, validate_assignment=True, validate_default=True, extra="forbid", use_enum_values=False) + + model_config = ConfigDict( + title="FeederLossesMetricsModel", + str_strip_whitespace=True, + validate_assignment=True, + validate_default=True, + extra="forbid", + use_enum_values=False, + ) total_losses_kwh: Annotated[ float, Field( - None, + None, title="total_losses_kwh", description="Total losses in the circuit", - )] - line_losses_kwh: Annotated[ + ), + ] + line_losses_kwh: Annotated[ float, Field( - None, + None, title="line_losses_kwh", description="Total line losses", - )] - transformer_losses_kwh: Annotated[ + ), + ] + transformer_losses_kwh: Annotated[ float, Field( - None, + None, title="transformer_losses_kwh", description="Total transformer losses", - )] - total_load_demand_kwh: Annotated[ + ), + ] + total_load_demand_kwh: Annotated[ float, Field( - None, + None, title="total_load_demand_kwh", description="Total power output of loads", - )] + ), + ] class SimulationFeederLossesMetricsModel(BaseModel): @@ -52,9 +64,9 @@ class SimulationFeederLossesMetricsModel(BaseModel): def compare_feeder_losses( - metrics1: SimulationFeederLossesMetricsModel, - metrics2: SimulationFeederLossesMetricsModel, - rel_tol=0.000001, + metrics1: SimulationFeederLossesMetricsModel, + metrics2: SimulationFeederLossesMetricsModel, + rel_tol=0.000001, ): """Compares the values of two instances of FeederLossesMetricsModel. @@ -111,10 +123,12 @@ def generate(self, output_dir): def _generate_from_in_memory_metrics(self, scenario, to_kwh): total_losses_dict = scenario.get_summed_element_total("Circuits", "LossesSum") - total_losses = abs(next(iter(total_losses_dict.values()))) / 1000 # OpenDSS reports total losses in Watts + total_losses = ( + abs(next(iter(total_losses_dict.values()))) / 1000 + ) # OpenDSS reports total losses in Watts line_losses_dict = scenario.get_summed_element_total("Circuits", "LineLossesSum") line_losses = abs(next(iter(line_losses_dict.values()))) - transformer_losses = (total_losses - line_losses) + transformer_losses = total_losses - line_losses total_load_power_dict = scenario.get_summed_element_total("Loads", "PowersSum") total_load_power = 0 for val in total_load_power_dict.values(): @@ -135,7 +149,7 @@ def _generate_from_all_time_points(self, scenario, to_kwh): df_line_losses = scenario.get_full_dataframe("Circuits", "LineLosses") assert len(df_line_losses.columns) == 1 df_loads_powers = scenario.get_full_dataframe("Loads", "Powers") - total_losses = abs(df_losses.sum().sum()) / 1000 # OpenDSS reports total losses in Watts + total_losses = abs(df_losses.sum().sum()) / 1000 # OpenDSS reports total losses in Watts line_losses = abs(df_line_losses.sum().sum()) transformer_losses = total_losses - line_losses return FeederLossesMetricsModel( @@ -160,7 +174,7 @@ def get_required_exports(simulation_config): { "property": "LineLosses", "store_values_type": "all", - } + }, ], "Loads": [ { @@ -168,7 +182,7 @@ def get_required_exports(simulation_config): "store_values_type": "all", "data_conversion": "abs_sum", }, - ] + ], } return { @@ -182,7 +196,7 @@ def get_required_exports(simulation_config): "property": "LineLosses", "store_values_type": "sum", "sum_elements": True, - } + }, ], "Loads": [ { @@ -191,7 +205,7 @@ def get_required_exports(simulation_config): "sum_elements": True, "data_conversion": "abs_sum", }, - ] + ], } @staticmethod diff --git a/src/pydss/reports/pv_reports.py b/src/pydss/reports/pv_reports.py index 0bc65e03..3b8132eb 100644 --- a/src/pydss/reports/pv_reports.py +++ b/src/pydss/reports/pv_reports.py @@ -1,4 +1,3 @@ - import math import abc import os @@ -9,14 +8,15 @@ from pydss.common import PV_LOAD_SHAPE_FILENAME from pydss.reports.reports import ReportBase, ReportGranularity -from pydss.utils.dataframe_utils import read_dataframe, write_dataframe -from pydss.utils.utils import dump_data +from pydss.utils.dataframe_utils import read_dataframe PF1_SCENARIO = "pf1" CONTROL_MODE_SCENARIO = "control_mode" + class PvReportBase(ReportBase, abc.ABC): """Base class for PV reports""" + def __init__(self, name, results, simulation_config): super().__init__(name, results, simulation_config) assert len(results.scenarios) == 2 @@ -33,9 +33,7 @@ def __init__(self, name, results, simulation_config): self._pf1_pv_systems = { x["name"]: x for x in self._pf1_scenario.read_pv_profiles()["pv_systems"] } - self._control_mode_pv_systems = { - x["name"]: x for x in cm_profiles["pv_systems"] - } + self._control_mode_pv_systems = {x["name"]: x for x in cm_profiles["pv_systems"]} def _get_pv_system_info(self, pv_system, scenario): if scenario == PF1_SCENARIO: @@ -95,14 +93,16 @@ def __init__(self, name, results, simulation_config): if not self._has_pv_systems(): return - diff_tolerance = self._report_settings.diff_tolerance_percent_pmpp * .01 - denominator_tolerance = self._report_settings.denominator_tolerance_percent_pmpp * .01 + diff_tolerance = self._report_settings.diff_tolerance_percent_pmpp * 0.01 + denominator_tolerance = self._report_settings.denominator_tolerance_percent_pmpp * 0.01 logger.debug("tolerances: diff=%s denominator=%s", diff_tolerance, denominator_tolerance) self._diff_tolerances = {} self._denominator_tolerances = {} for pv_system in self._pf1_scenario.read_pv_profiles()["pv_systems"]: self._diff_tolerances[pv_system["name"]] = pv_system["pmpp"] * diff_tolerance - self._denominator_tolerances[pv_system["name"]] = pv_system["pmpp"] * denominator_tolerance + self._denominator_tolerances[pv_system["name"]] = ( + pv_system["pmpp"] * denominator_tolerance + ) @staticmethod def _calculate_clipping(total_dc_power, pf1_real_power): @@ -112,7 +112,7 @@ def _calculate_clipping(total_dc_power, pf1_real_power): def _calculate_clipping_array(dc_power, pf1_real_power): dcp = dc_power.values rp = pf1_real_power.values - rp = np.where(rp==0, np.nan, rp) + rp = np.where(rp == 0, np.nan, rp) return (dcp - rp) / rp * 100 def _get_total_dc_power_across_pv_systems(self): @@ -130,9 +130,7 @@ def _get_total_dc_power(self, pv_system): def _generate_per_pv_system_per_time_point(self, output_dir): pv_load_shapes = self._read_pv_load_shapes() - pf1_real_power_full = self._pf1_scenario.get_full_dataframe( - "PVSystems", "Powers" - ) + pf1_real_power_full = self._pf1_scenario.get_full_dataframe("PVSystems", "Powers") name = None # TODO: Apply tolerances to other granularity options. @@ -149,11 +147,12 @@ def calc_clipping(dcp, rp): name = _name cm_info = self._get_pv_system_info(name, CONTROL_MODE_SCENARIO) pf1_real_power = pf1_real_power_full[name + "__Powers"] - dc_power = pv_load_shapes[cm_info["load_shape_profile"]] * \ - cm_info["pmpp"] * \ - cm_info["irradiance"] - assert len(dc_power) == len(pf1_real_power), \ - f"{len(dc_power)} {len(pf1_real_power)}" + dc_power = ( + pv_load_shapes[cm_info["load_shape_profile"]] + * cm_info["pmpp"] + * cm_info["irradiance"] + ) + assert len(dc_power) == len(pf1_real_power), f"{len(dc_power)} {len(pf1_real_power)}" col = name + "__Clipping" data[col] = dc_power.combine(pf1_real_power, calc_clipping).values @@ -177,9 +176,7 @@ def _generate_per_pv_system_total(self, output_dir): self._export_json_report(data, output_dir, self.TOTAL_FILENAME) def _generate_all_pv_systems_per_time_point(self, output_dir): - pf1_real_power = self._pf1_scenario.get_summed_element_dataframe( - "PVSystems", "Powers" - ) + pf1_real_power = self._pf1_scenario.get_summed_element_dataframe("PVSystems", "Powers") pv_load_shapes = self._read_pv_load_shapes() dc_powers = {} for name in self._pv_system_names: @@ -189,7 +186,10 @@ def _generate_all_pv_systems_per_time_point(self, output_dir): assert len(dc_power) == len(pf1_real_power) dc_powers[name] = dc_power.values # TODO: just for validation - assert math.isclose(sum(dc_power.values), cm_info["load_shape_pmult_sum"] * cm_info["pmpp"] * cm_info["irradiance"]) + assert math.isclose( + sum(dc_power.values), + cm_info["load_shape_pmult_sum"] * cm_info["pmpp"] * cm_info["irradiance"], + ) df = pd.DataFrame(dc_powers, index=pf1_real_power.index) total_dc_power = df.sum(axis=1) @@ -203,9 +203,9 @@ def _generate_all_pv_systems_per_time_point(self, output_dir): def _generate_all_pv_systems_total(self, output_dir): total_dc_power = self._get_total_dc_power_across_pv_systems() - pf1_real_power = next(iter( - self._pf1_scenario.get_summed_element_total("PVSystems", "PowersSum").values() - )) + pf1_real_power = next( + iter(self._pf1_scenario.get_summed_element_total("PVSystems", "PowersSum").values()) + ) clipping = self._calculate_clipping(total_dc_power, pf1_real_power) data = {"clipping": clipping} self._export_json_report(data, output_dir, self.TOTAL_FILENAME) @@ -257,21 +257,22 @@ def __init__(self, name, results, simulation_config): if not self._has_pv_systems(): return - diff_tolerance = self._report_settings.diff_tolerance_percent_pmpp * .01 - denominator_tolerance = self._report_settings.denominator_tolerance_percent_pmpp * .01 + diff_tolerance = self._report_settings.diff_tolerance_percent_pmpp * 0.01 + denominator_tolerance = self._report_settings.denominator_tolerance_percent_pmpp * 0.01 logger.debug("tolerances: diff=%s denominator=%s", diff_tolerance, denominator_tolerance) self._diff_tolerances = {} self._denominator_tolerances = {} for pv_system in self._pf1_scenario.read_pv_profiles()["pv_systems"]: self._diff_tolerances[pv_system["name"]] = pv_system["pmpp"] * diff_tolerance - self._denominator_tolerances[pv_system["name"]] = pv_system["pmpp"] * denominator_tolerance + self._denominator_tolerances[pv_system["name"]] = ( + pv_system["pmpp"] * denominator_tolerance + ) def _generate_per_pv_system_per_time_point(self, output_dir): pf1_power = self._pf1_scenario.get_full_dataframe("PVSystems", "Powers") - control_mode_power = self._control_mode_scenario.get_full_dataframe( - "PVSystems", "Powers" - ) + control_mode_power = self._control_mode_scenario.get_full_dataframe("PVSystems", "Powers") name = None + def calc_curtailment(pf1, cm): if pf1 < self._denominator_tolerances[name]: return 0 @@ -318,12 +319,16 @@ def _generate_all_pv_systems_per_time_point(self, output_dir): self._export_dataframe_report(df, output_dir, "pv_curtailment") def _generate_all_pv_systems_total(self, output_dir): - pf1_power = next(iter( - self._pf1_scenario.get_summed_element_total("PVSystems", "PowersSum").values() - )) - control_mode_power = next(iter( - self._control_mode_scenario.get_summed_element_total("PVSystems", "PowersSum").values() - )) + pf1_power = next( + iter(self._pf1_scenario.get_summed_element_total("PVSystems", "PowersSum").values()) + ) + control_mode_power = next( + iter( + self._control_mode_scenario.get_summed_element_total( + "PVSystems", "PowersSum" + ).values() + ) + ) curtailment = (pf1_power - control_mode_power) / pf1_power * 100 data = {"curtailment": curtailment} @@ -353,9 +358,7 @@ def calculate_pv_curtailment(self): pd.DataFrame """ - pf1_power = self._pf1_scenario.get_full_dataframe( - "PVSystems", "Powers", real_only=True - ) + pf1_power = self._pf1_scenario.get_full_dataframe("PVSystems", "Powers", real_only=True) control_mode_power = self._control_mode_scenario.get_full_dataframe( "PVSystems", "Powers", real_only=True ) diff --git a/src/pydss/reports/reg_control_tap_number_change_count.py b/src/pydss/reports/reg_control_tap_number_change_count.py index a727b032..964c4e3d 100644 --- a/src/pydss/reports/reg_control_tap_number_change_count.py +++ b/src/pydss/reports/reg_control_tap_number_change_count.py @@ -21,9 +21,11 @@ def generate(self, output_dir): for scenario in self._results.scenarios: scenario_data = {"name": scenario.name, "reg_controls": []} for reg_control in scenario.list_element_names("RegControls"): - change_count = int(scenario.get_element_property_value( - "RegControls", "TrackTapNumberChanges", reg_control - )) + change_count = int( + scenario.get_element_property_value( + "RegControls", "TrackTapNumberChanges", reg_control + ) + ) changes = {"name": reg_control, "change_count": change_count} scenario_data["reg_controls"].append(changes) data["scenarios"].append(scenario_data) diff --git a/src/pydss/reports/reports.py b/src/pydss/reports/reports.py index f42be673..2d60ae7e 100644 --- a/src/pydss/reports/reports.py +++ b/src/pydss/reports/reports.py @@ -19,8 +19,10 @@ REPORTS_DIR = "Reports" + class Reports: """Generate reports from a pydss project""" + def __init__(self, results): self._results = results self._report_names = [] @@ -63,8 +65,7 @@ def append_required_exports(exports, settings: SimulationSettingsModel): scenarios = report.scenarios active_scenario = settings.project.active_scenario if scenarios and active_scenario not in scenarios: - logger.debug("report %s is not enabled for scenario %s", name, - active_scenario) + logger.debug("report %s is not enabled for scenario %s", name, active_scenario) continue required = all_reports[name].get_required_exports(settings) @@ -73,12 +74,15 @@ def append_required_exports(exports, settings: SimulationSettingsModel): found = False store_type = req_prop.get("store_values_type", "all") for prop in exports.list_element_properties(elem_class): - if prop.name == req_prop["property"] and \ - prop.store_values_type.value == store_type: + if ( + prop.name == req_prop["property"] + and prop.store_values_type.value == store_type + ): if prop.opendss_classes or req_prop.get("opendss_classes"): assert prop.sum_elements == req_prop.get("sum_elements", False) - assert prop.data_conversion == \ - req_prop.get("data_conversion", DataConversion.NONE) + assert prop.data_conversion == req_prop.get( + "data_conversion", DataConversion.NONE + ) prop.append_opendss_classes(req_prop["opendss_classes"]) found = True if not found: @@ -146,8 +150,10 @@ def generate(self): # in order to be automatically imported. Otherwise, add a direct import in # PyDSS/reports/__init__.py. + class ReportBase(abc.ABC): """Base class for reports""" + def __init__(self, name, results, settings): self._results = results self._scenarios = results.scenarios diff --git a/src/pydss/reports/thermal_metrics.py b/src/pydss/reports/thermal_metrics.py index ccc5dba5..84a05b0d 100644 --- a/src/pydss/reports/thermal_metrics.py +++ b/src/pydss/reports/thermal_metrics.py @@ -12,6 +12,7 @@ ) from pydss.utils.utils import load_data + class ThermalMetrics(ReportBase): """Reports thermal metrics. @@ -112,7 +113,9 @@ def _generate_from_all_time_points(self): return scenarios - def _make_thermal_metrics_model(self, df, window_size, window_size_hours, inst_threshold, mavg_threshold): + def _make_thermal_metrics_model( + self, df, window_size, window_size_hours, inst_threshold, mavg_threshold + ): df_mavg = df.rolling(window=window_size).mean() max_instantaneous = self._get_max_values(df) max_mavg = self._get_max_values(df_mavg) @@ -122,8 +125,12 @@ def _make_thermal_metrics_model(self, df, window_size, window_size_hours, inst_t max_moving_average_loadings_pct=max_mavg, max_moving_average_loading_pct=max(max_mavg.values()), window_size_hours=window_size_hours, - num_time_points_with_instantaneous_violations=self._get_num_time_points_with_violations(df, inst_threshold), - num_time_points_with_moving_average_violations=self._get_num_time_points_with_violations(df_mavg, mavg_threshold), + num_time_points_with_instantaneous_violations=self._get_num_time_points_with_violations( + df, inst_threshold + ), + num_time_points_with_moving_average_violations=self._get_num_time_points_with_violations( + df_mavg, mavg_threshold + ), instantaneous_threshold=inst_threshold, moving_average_threshold=mavg_threshold, ) @@ -177,7 +184,7 @@ def get_required_exports(simulation_config): "property": "OverloadsMetricInMemory", "opendss_classes": ["Lines", "Transformers"], } - ] + ] } @staticmethod diff --git a/src/pydss/reports/voltage_metrics.py b/src/pydss/reports/voltage_metrics.py index c222d0ea..074dcfe7 100644 --- a/src/pydss/reports/voltage_metrics.py +++ b/src/pydss/reports/voltage_metrics.py @@ -6,7 +6,7 @@ from pydss.common import MinMax, NODE_NAMES_BY_TYPE_FILENAME from pydss.reports.reports import ReportBase, ReportGranularity -from pydss.utils.utils import serialize_timedelta, deserialize_timedelta, load_data +from pydss.utils.utils import load_data from pydss.node_voltage_metrics import ( NodeVoltageMetricsByType, SimulationVoltageMetricsModel, @@ -20,6 +20,7 @@ VoltageMetric6, ) + class VoltageMetrics(ReportBase): """Reports voltage metrics. @@ -69,9 +70,7 @@ class VoltageMetrics(ReportBase): def __init__(self, name, results, simulation_config): super().__init__(name, results, simulation_config) - self._granularity = ReportGranularity( - self._report_global_settings.granularity - ) + self._granularity = ReportGranularity(self._report_global_settings.granularity) self._range_a_limits = MinMax( min=self._report_settings.range_a_limits[0], max=self._report_settings.range_a_limits[1], @@ -129,7 +128,9 @@ def _generate_from_all_time_points(self): ) node_names_by_type = load_data(filename) assert len(set(node_names_by_type["primaries"])) == len(node_names_by_type["primaries"]) - assert len(set(node_names_by_type["secondaries"])) == len(node_names_by_type["secondaries"]) + assert len(set(node_names_by_type["secondaries"])) == len( + node_names_by_type["secondaries"] + ) df = scenario.get_full_dataframe("Buses", "puVmagAngle", mag_ang="mag") columns = [] for column in df.columns: @@ -173,9 +174,9 @@ def _gen_metrics(self, df): max_val = row.max() min_val = row.min() if ( - not (max_val > self._range_b_limits.max) - and not (min_val < self._range_b_limits.min) - and (max_val > self._range_a_limits.max or min_val < self._range_a_limits.min) + not (max_val > self._range_b_limits.max) + and not (min_val < self._range_b_limits.min) + and (max_val > self._range_a_limits.max or min_val < self._range_a_limits.min) ): metric_1_violation_time_points.append(timestamp) if max_val > self._range_b_limits.max or min_val < self._range_b_limits.min: @@ -228,17 +229,23 @@ def _gen_metrics(self, df): metric_5=vmetric_5, metric_6=vmetric_6, summary=NodeVoltageMetricsByType.create_summary( - vmetric_1, vmetric_2, vmetric_3, vmetric_5, vmetric_6, list(df.columns), - len(df), self._resolution, self._range_a_limits, self._range_b_limits, - self._moving_window_minutes - ) + vmetric_1, + vmetric_2, + vmetric_3, + vmetric_5, + vmetric_6, + list(df.columns), + len(df), + self._resolution, + self._range_a_limits, + self._range_b_limits, + self._moving_window_minutes, + ), ) @staticmethod def get_required_exports(simulation_config): - inputs = VoltageMetrics.get_inputs_from_defaults( - simulation_config, VoltageMetrics.NAME - ) + inputs = VoltageMetrics.get_inputs_from_defaults(simulation_config, VoltageMetrics.NAME) if inputs["store_all_time_points"]: return { # TODO: This should use Circuit.AllBusMagPu for performance reasons. @@ -269,9 +276,7 @@ def get_required_scenario_names(): @staticmethod def set_required_project_settings(settings): - inputs = VoltageMetrics.get_inputs_from_defaults( - settings, VoltageMetrics.NAME - ) + inputs = VoltageMetrics.get_inputs_from_defaults(settings, VoltageMetrics.NAME) exports = settings.exports if inputs["store_all_time_points"] and not exports.export_node_names_by_type: exports.export_node_names_by_type = True diff --git a/src/pydss/ResultData.py b/src/pydss/result_data.py similarity index 85% rename from src/pydss/ResultData.py rename to src/pydss/result_data.py index 7aebe352..43ea4a34 100644 --- a/src/pydss/ResultData.py +++ b/src/pydss/result_data.py @@ -1,5 +1,3 @@ - - from datetime import datetime import pathlib import os @@ -8,17 +6,16 @@ from loguru import logger import pandas as pd -from pydss.unitDefinations import unit_info +from pydss.unit_definitions import unit_info from pydss.common import ( PV_LOAD_SHAPE_FILENAME, PV_PROFILES_FILENAME, NODE_NAMES_BY_TYPE_FILENAME, - DatasetPropertyType, ) from pydss.dataset_buffer import DatasetBuffer from pydss.utils.dss_utils import get_node_names_by_type -from pydss.exceptions import InvalidConfiguration, InvalidParameter -from pydss.export_list_reader import ExportListReader, StoreValuesType +from pydss.exceptions import InvalidParameter +from pydss.export_list_reader import ExportListReader from pydss.reports.reports import Reports, ReportGranularity from pydss.simulation_input_models import SimulationSettingsModel from pydss.utils.dataframe_utils import write_dataframe @@ -28,22 +25,30 @@ create_loadshape_pmult_dataframe_for_simulation, ) from pydss.utils.timing_utils import Timer, timer_stats_collector, track_timing -from pydss.value_storage import ValueContainer, ValueByNumber from pydss.metrics import ( OpenDssPropertyMetric, SummedElementsByGroupOpenDssPropertyMetric, SummedElementsOpenDssPropertyMetric, ) + class ResultData: """Exports data to files.""" METADATA_FILENAME = "metadata.json" INDICES_BASENAME = "indices" - def __init__(self, settings: SimulationSettingsModel, system_paths, dss_objects, - dss_objects_by_class, dss_buses, dss_solver, dss_command, - dss_instance): + def __init__( + self, + settings: SimulationSettingsModel, + system_paths, + dss_objects, + dss_objects_by_class, + dss_buses, + dss_solver, + dss_command, + dss_instance, + ): self._dss_solver = dss_solver self._results = {} self._buses = dss_buses @@ -57,8 +62,8 @@ def __init__(self, settings: SimulationSettingsModel, system_paths, dss_objects, self._current_results = {} self._dss_command = dss_command - self._start_day = dss_solver.StartDay - self._end_day = dss_solver.EndDay + self._start_day = dss_solver.start_day + self._end_day = dss_solver.end_day self._time_dataset = None self._frequency_dataset = None self._mode_dataset = None @@ -123,7 +128,9 @@ def _create_exports(self): if obj.Enabled and prop.should_store_name(obj.FullName): dss_objs.append(obj) else: - dss_objs = [x for x in objs.values() if x.Enabled and prop.should_store_name(x.FullName)] + dss_objs = [ + x for x in objs.values() if x.Enabled and prop.should_store_name(x.FullName) + ] if prop.custom_metric is None: self._add_opendss_metric(prop, dss_objs) else: @@ -131,7 +138,7 @@ def _create_exports(self): def _add_opendss_metric(self, prop, dss_objs): obj = dss_objs[0] - if not obj.IsValidAttribute(prop.name): + if not obj.is_valid_attribute(prop.name): raise InvalidParameter(f"{obj.FullName} / {prop.name} cannot be exported") key = (prop.elem_class, prop.name) if prop.sum_elements: @@ -171,9 +178,9 @@ def _add_custom_metric(self, prop, dss_objs): else: metric.add_property(prop) - def InitializeDataStore(self, hdf_store, num_steps, MC_scenario_number=None): - if MC_scenario_number is not None: - self._scenario = self._base_scenario + f"_MC{MC_scenario_number}" + def initialize_data_store(self, hdf_store, num_steps, mc_scenario_number=None): + if mc_scenario_number is not None: + self._scenario = self._base_scenario + f"_MC{mc_scenario_number}" self._hdf_store = hdf_store self._time_dataset = DatasetBuffer( hdf_store=hdf_store, @@ -181,7 +188,7 @@ def InitializeDataStore(self, hdf_store, num_steps, MC_scenario_number=None): max_size=num_steps, dtype=float, columns=("Timestamp",), - max_chunk_bytes=self._max_chunk_bytes + max_chunk_bytes=self._max_chunk_bytes, ) self._frequency_dataset = DatasetBuffer( hdf_store=hdf_store, @@ -189,7 +196,7 @@ def InitializeDataStore(self, hdf_store, num_steps, MC_scenario_number=None): max_size=num_steps, dtype=float, columns=("Frequency",), - max_chunk_bytes=self._max_chunk_bytes + max_chunk_bytes=self._max_chunk_bytes, ) self._mode_dataset = DatasetBuffer( hdf_store=hdf_store, @@ -197,7 +204,7 @@ def InitializeDataStore(self, hdf_store, num_steps, MC_scenario_number=None): max_size=num_steps, dtype="S10", columns=("Mode",), - max_chunk_bytes=self._max_chunk_bytes + max_chunk_bytes=self._max_chunk_bytes, ) self._cur_step = 0 @@ -214,15 +221,17 @@ def _iter_metrics(self): yield metric @property - def CurrentResults(self): + def current_results(self): return self._current_results @track_timing(timer_stats_collector) - def UpdateResults(self, store_nan=False): + def update_results(self, store_nan=False): self._current_results.clear() # Get the number of seconds since the Epoch without any timezone conversions. - timestamp = (self._dss_solver.GetDateTime() - datetime(1970, 1, 1, 0, 0)).total_seconds() # datetime.utcfromtimestamp(0) is now depriciated + timestamp = ( + self._dss_solver.GetDateTime() - datetime(1970, 1, 1, 0, 0) + ).total_seconds() # datetime.utcfromtimestamp(0) is now depriciated self._time_dataset.write_value([timestamp]) self._frequency_dataset.write_value([self._dss_solver.getFrequency()]) self._mode_dataset.write_value([self._dss_solver.getMode()]) @@ -239,7 +248,7 @@ def UpdateResults(self, store_nan=False): self._cur_step += 1 return self._current_results - def ExportResults(self): + def export_results(self): metadata = { "event_log": None, "element_info_files": [], @@ -257,10 +266,12 @@ def ExportResults(self): filename = os.path.join(self._export_dir, self.METADATA_FILENAME) dump_data(metadata, filename, indent=4) - logger.info(f"Exported metadata to {filename}", ) + logger.info( + f"Exported metadata to {filename}", + ) self._hdf_store = None - def Close(self): + def close(self): for dataset in (self._time_dataset, self._frequency_dataset, self._mode_dataset): dataset.flush_data() for metric in self._iter_metrics(): @@ -279,7 +290,9 @@ def _export_event_log(self, metadata): out = self._dss_command(cmd) if out != event_log: raise Exception(f"Failed to export EventLog: {out}") - logger.info(f"Exported OpenDSS event log to {out}", ) + logger.info( + f"Exported OpenDSS event log to {out}", + ) metadata["event_log"] = self._export_relative_dir + f"/{event_log}" finally: os.chdir(orig) @@ -293,7 +306,7 @@ def _find_feeder_head_line(self): feeder_head_line = None flag = dss.Topology.First() while flag > 0: - if 'line' in dss.Topology.BranchName().lower(): + if "line" in dss.Topology.BranchName().lower(): feeder_head_line = dss.Topology.BranchName() break else: @@ -306,14 +319,14 @@ def _get_feeder_head_loading(self): if head_line is not None: flag = dss.Circuit.SetActiveElement(head_line) - if flag>0: + if flag > 0: n_phases = dss.CktElement.NumPhases() max_amp = dss.CktElement.NormalAmps() - Currents = dss.CktElement.CurrentsMagAng()[:2*n_phases] - Current_magnitude = Currents[::2] + currents = dss.CktElement.CurrentsMagAng()[: 2 * n_phases] + current_magnitude = currents[::2] - max_flow = max(max(Current_magnitude), 1e-10) - loading = max_flow/max_amp + max_flow = max(max(current_magnitude), 1e-10) + loading = max_flow / max_amp return loading else: @@ -322,7 +335,9 @@ def _get_feeder_head_loading(self): return None def _reverse_powerflow(self): - reverse_pf = dss.Circuit.TotalPower()[0] > 0 # total substation power is an injection(-) or a consumption(+) + reverse_pf = ( + dss.Circuit.TotalPower()[0] > 0 + ) # total substation power is an injection(-) or a consumption(+) return reverse_pf def _export_feeder_head_info(self, metadata): @@ -333,23 +348,24 @@ def _export_feeder_head_info(self, metadata): 3- The feeder head load in (kW, kVar). Negative in case of power injection 4- The reverse power flow flag. True if power is flowing back to the feeder head, False otherwise """ - if not "feeder_head_info_files" in metadata.keys(): + if "feeder_head_info_files" not in metadata.keys(): metadata["feeder_head_info_files"] = [] total_power = dss.Circuit.TotalPower() - df_dict = {"FeederHeadLine": self._find_feeder_head_line(), - "FeederHeadLoading": self._get_feeder_head_loading(), - "FeederHeadLoadKW": total_power[0], - "FeederHeadLoadKVar": total_power[1], - "ReversePowerFlow": self._reverse_powerflow() - } + df_dict = { + "FeederHeadLine": self._find_feeder_head_line(), + "FeederHeadLoading": self._get_feeder_head_loading(), + "FeederHeadLoadKW": total_power[0], + "FeederHeadLoadKVar": total_power[1], + "ReversePowerFlow": self._reverse_powerflow(), + } filename = "FeederHeadInfo" fname = filename + ".json" relpath = os.path.join(self._export_relative_dir, fname) filepath = os.path.join(self._export_dir, fname) - #write_dataframe(df, filepath) + # write_dataframe(df, filepath) dump_data(df_dict, filepath) metadata["feeder_head_info_files"].append(relpath) logger.info("Exported %s information to %s.", filename, filepath) @@ -373,7 +389,7 @@ def _export_elements(self, metadata, element_types): ("Vsource", "VsourcesInfo", dss.Vsources.Count), ("XYCurve", "XYCurvesInfo", dss.XYCurves.Count), # TODO This can be very large. Consider making it configurable. - #("LoadShape", "LoadShapeInfo", dss.LoadShape.Count), + # ("LoadShape", "LoadShapeInfo", dss.LoadShape.Count), ] if element_types: types = set() @@ -407,7 +423,9 @@ def _export_transformers(self, metadata): while flag > 0: name = dss.CktElement.Name() df_dict["Transformer"].append(name) - df_dict["HighSideConnection"].append(dss.Properties.Value("conns").split("[")[1].split(",")[0].strip(" ").lower()) + df_dict["HighSideConnection"].append( + dss.Properties.Value("conns").split("[")[1].split(",")[0].strip(" ").lower() + ) df_dict["NumPhases"].append(dss.CktElement.NumPhases()) flag = dss.ActiveClass.Next() @@ -432,12 +450,14 @@ def _export_pv_profiles(self): profile_name = obj.GetParameter("yearly").lower() if profile_name != "": profiles.add(profile_name) - pv_infos.append({ - "irradiance": obj.GetParameter("irradiance"), - "name": full_name, - "pmpp": obj.GetParameter("pmpp"), - "load_shape_profile": profile_name, - }) + pv_infos.append( + { + "irradiance": obj.GetParameter("irradiance"), + "name": full_name, + "pmpp": obj.GetParameter("pmpp"), + "load_shape_profile": profile_name, + } + ) pmult_sums = {} if dss.LoadShape.First() == 0: @@ -518,3 +538,10 @@ def max_num_bytes(self): for metric in self._iter_metrics(): total += metric.max_num_bytes() return total + + +ResultData.InitializeDataStore = ResultData.initialize_data_store +ResultData.CurrentResults = property(lambda self: self.current_results) +ResultData.UpdateResults = ResultData.update_results +ResultData.ExportResults = ResultData.export_results +ResultData.Close = ResultData.close diff --git a/src/pydss/simulation_input_models.py b/src/pydss/simulation_input_models.py index 8b2911ce..9bed6bdc 100644 --- a/src/pydss/simulation_input_models.py +++ b/src/pydss/simulation_input_models.py @@ -2,15 +2,14 @@ import enum import shutil -from datetime import datetime, timedelta +from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Annotated from pydantic import field_validator, model_validator, ConfigDict from pydantic import BaseModel, Field -#from pydantic.json import isoformat, timedelta_isoformat - +# from pydantic.json import isoformat, timedelta_isoformat from pydss.common import ( @@ -21,20 +20,30 @@ SimulationType, SnapshotTimePointSelectionMode, SIMULATION_SETTINGS_FILENAME, - ControllerType + ControllerType, ) from pydss.dataset_buffer import DEFAULT_MAX_CHUNK_BYTES from pydss.utils.utils import dump_data, load_data from pydss.common import DATE_FORMAT + class InputsBaseModel(BaseModel): """Base class for all input models""" - model_config = ConfigDict(title="InputsBaseModel", str_strip_whitespace=True, validate_assignment=True, validate_default=True, extra="forbid", use_enum_values=False, populate_by_name=True) + + model_config = ConfigDict( + title="InputsBaseModel", + str_strip_whitespace=True, + validate_assignment=True, + validate_default=True, + extra="forbid", + use_enum_values=False, + populate_by_name=True, + ) def dict(self, *args, **kwargs): try: data = super().model_dump(*args, **kwargs, mode="json") - except: + except Exception: data = super().model_dump(*args, **kwargs) for key, val in data.items(): if isinstance(val, enum.Enum): @@ -52,31 +61,41 @@ def dict(self, *args, **kwargs): class SnapshotTimePointSelectionConfigModel(InputsBaseModel): """Defines the user inputs for auto-selecting snapshot time points.""" - mode: Annotated[SnapshotTimePointSelectionMode, Field( - title="mode", - description="Mode", - default=SnapshotTimePointSelectionMode.NONE, - )] - start_time: Annotated[datetime, Field( - title="start_time", - description="Start time in the load shape profiles", - default=datetime.strptime("2020-1-1 00:00:00.0", DATE_FORMAT) - )] - search_duration_min: Annotated[float, Field( - title="search_duration_min", - description="Duration in minutes to search in the load shape profiles", - default=1440.0, - )] - - @field_validator('start_time', mode="before") + mode: Annotated[ + SnapshotTimePointSelectionMode, + Field( + title="mode", + description="Mode", + default=SnapshotTimePointSelectionMode.NONE, + ), + ] + start_time: Annotated[ + datetime, + Field( + title="start_time", + description="Start time in the load shape profiles", + default=datetime.strptime("2020-1-1 00:00:00.0", DATE_FORMAT), + ), + ] + search_duration_min: Annotated[ + float, + Field( + title="search_duration_min", + description="Duration in minutes to search in the load shape profiles", + default=1440.0, + ), + ] + + @field_validator("start_time", mode="before") @classmethod def double(cls, v: str) -> str: if isinstance(v, str): if "T" in v: v = datetime.fromisoformat(v) - else: + else: v = datetime.strptime(v, DATE_FORMAT) - return v + return v + class ScenarioPostProcessModel(InputsBaseModel): """Defines user inputs for a scenario post-process script.""" @@ -87,13 +106,15 @@ class ScenarioPostProcessModel(InputsBaseModel): "", title="script", description="Post-process script", - )] + ), + ] config_file: Annotated[ str, Field( title="config_file", description="Post-process config file", - )] + ), + ] class ScenarioModel(InputsBaseModel): @@ -104,21 +125,24 @@ class ScenarioModel(InputsBaseModel): Field( title="name", description="Name of scenario", - )] + ), + ] post_process_infos: Annotated[ List[ScenarioPostProcessModel], Field( title="post_process_infos", description="Post-process script descriptors", default=[], - )] + ), + ] snapshot_time_point_selection_config: Annotated[ - SnapshotTimePointSelectionConfigModel, + SnapshotTimePointSelectionConfigModel, Field( title="snapshot_time_point_selection_config", description="Descriptor for auto-selecting snapshot time points", default=SnapshotTimePointSelectionConfigModel(), - )] + ), + ] class SimulationRangeModel(InputsBaseModel): @@ -129,18 +153,20 @@ class SimulationRangeModel(InputsBaseModel): start: Annotated[ str, - Field( + Field( title="start", description="Time to start running control algorithms each day.", default="00:00:00", - )] - end: Annotated[ + ), + ] + end: Annotated[ str, - Field( + Field( title="end", description="Time to stop running control algorithms each day.", default="23:59:59", - )] + ), + ] class ProjectModel(InputsBaseModel): @@ -153,7 +179,8 @@ class ProjectModel(InputsBaseModel): title="project_path", description="Base path of project. Join with 'active_project' to get full path", alias="Project Path", - )] + ), + ] active_project: Annotated[ Optional[str], Field( @@ -161,15 +188,17 @@ class ProjectModel(InputsBaseModel): title="active_project", description="Active project name. Join with 'project_path' to get full path", alias="Active Project", - )] + ), + ] active_project_path: Annotated[ - Optional[Path], + Optional[Path], Field( None, title="active_project_path", description="Path to project. Auto-generated.", - #internal=True, - )] + # internal=True, + ), + ] scenarios: Annotated[ Optional[List[ScenarioModel]], Field( @@ -177,7 +206,8 @@ class ProjectModel(InputsBaseModel): description="List of scenarios", alias="Scenarios", default=[], - )] + ), + ] active_scenario: Annotated[ Optional[str], Field( @@ -185,22 +215,25 @@ class ProjectModel(InputsBaseModel): description="Name of active scenario", alias="Active Scenario", default="", - )] + ), + ] start_time: Annotated[ Optional[datetime], - Field( + Field( title="start_time", description="Start time of simulation", default=datetime.strptime("2020-1-1 00:00:00.0", DATE_FORMAT), alias="Start time", - )] + ), + ] simulation_duration_min: Annotated[ Optional[float], Field( title="simulation_duration_min", description="Simulation duration in minutes.", default=1440.0, - )] + ), + ] step_resolution_sec: Annotated[ Optional[float], Field( @@ -208,7 +241,8 @@ class ProjectModel(InputsBaseModel): description="Time step resolution in seconds", alias="Step resolution (sec)", default=900.0, - )] + ), + ] loadshape_start_time: Annotated[ Optional[datetime], Field( @@ -216,7 +250,8 @@ class ProjectModel(InputsBaseModel): description="Start time of loadshape profiles", alias="Loadshape start time", default="2020-01-01 00:00:00.0", - )] + ), + ] simulation_range: Annotated[ Optional[SimulationRangeModel], Field( @@ -225,7 +260,8 @@ class ProjectModel(InputsBaseModel): "Useful for skipping night when simulating PV Systems.", alias="Simulation range", default=SimulationRangeModel(), - )] + ), + ] simulation_type: Annotated[ SimulationType, Field( @@ -233,15 +269,17 @@ class ProjectModel(InputsBaseModel): description="Type of simulation to run.", alias="Simulation Type", default=SimulationType.QSTS, - )] + ), + ] control_mode: Annotated[ - ControlMode, + ControlMode, Field( title="control_mode", description="Simulation control mode", alias="Control mode", default=ControlMode.STATIC, - )] + ), + ] max_control_iterations: Annotated[ int, Field( @@ -249,7 +287,9 @@ class ProjectModel(InputsBaseModel): description="Maximum outer loop control iterations", alias="Max Control Iterations", default=50, - )] + ge=1, + ), + ] convergence_error_percent_threshold: Annotated[ float, Field( @@ -257,7 +297,8 @@ class ProjectModel(InputsBaseModel): description="Convergence error threshold as a percent", alias="Convergence error percent threshold", default=0.0, - )] + ), + ] error_tolerance: Annotated[ float, Field( @@ -265,7 +306,8 @@ class ProjectModel(InputsBaseModel): description="Error tolerance in per unit", alias="Error tolerance", default=0.001, - )] + ), + ] max_error_tolerance: Annotated[ float, Field( @@ -273,7 +315,8 @@ class ProjectModel(InputsBaseModel): description="Abort simulation if a convergence error exceeds this value.", alias="Max error tolerance", default=0.0, - )] + ), + ] skip_export_on_convergence_error: Annotated[ bool, Field( @@ -281,7 +324,8 @@ class ProjectModel(InputsBaseModel): description="Do not export data at a time point if there is a convergence error.", alias="Skip export on convergence error", default=True, - )] + ), + ] dss_file: Annotated[ str, Field( @@ -289,7 +333,8 @@ class ProjectModel(InputsBaseModel): description="OpenDSS master filename", alias="DSS File", default="Master.dss", - )] + ), + ] dss_file_absolute_path: Annotated[ bool, Field( @@ -297,15 +342,17 @@ class ProjectModel(InputsBaseModel): description="Set to true if 'dss_file' is an absolute path.", alias="DSS File Absolute Path", default=False, - )] - disable_pydss_controllers: Annotated[ + ), + ] + openmdao_reports: Annotated[ bool, Field( - title="disable_pydss_controllers", - description="Allows disabling of the control algorithms", - alias="Disable pydss controllers", + title="openmdao_reports", + description="Generate OpenMDAO reports and nonlinear solver recordings.", + alias="OpenMDAO Reports", default=False, - )] + ), + ] use_controller_registry: Annotated[ bool, Field( @@ -313,24 +360,25 @@ class ProjectModel(InputsBaseModel): description="Use local controller registry.", alias="Use Controller Registry", default=False, - )] + ), + ] - @field_validator('start_time', 'loadshape_start_time', mode="before") + @field_validator("start_time", "loadshape_start_time", mode="before") @classmethod def double(cls, v: str) -> str: if isinstance(v, str): if "T" in v: v = datetime.fromisoformat(v) - else: + else: v = datetime.strptime(v, DATE_FORMAT) - return v - + return v + @model_validator(mode="before") @classmethod def pre_process(cls, values): # Correct legacy files. values.pop("Return Results", None) - + for val in ("Simulation Type", "simulation_type"): if val in values: if not isinstance(values[val], SimulationType): @@ -343,7 +391,7 @@ def pre_process(cls, values): ) values["simulation_duration_min"] = old_duration return values - + @field_validator("project_path") @classmethod def check_project_path(cls, val): @@ -356,32 +404,35 @@ def check_project_path(cls, val): raise ValueError(f"project_path={val} does not exist") return val - - @model_validator(mode='after') - def check_active_project(self)-> 'ProjectModel': + @model_validator(mode="after") + def check_active_project(self) -> "ProjectModel": if self.project_path is None or self.active_project is None: return None - + active_project_path = self.project_path / self.active_project if not active_project_path.exists(): raise ValueError(f"project_path={active_project_path} does not exist") return self - - @model_validator(mode='before') - def assign_active_project_path(self)-> 'ProjectModel': - if "project_path" in self and "active_project" in self and self["active_project"] is not None: - self['active_project_path'] = Path(self["project_path"]) / self["active_project"] + + @model_validator(mode="before") + def assign_active_project_path(self) -> "ProjectModel": + if ( + "project_path" in self + and "active_project" in self + and self["active_project"] is not None + ): + self["active_project_path"] = Path(self["project_path"]) / self["active_project"] return self - @model_validator(mode='after') - def check_scenarios(self)-> 'ProjectModel': + @model_validator(mode="after") + def check_scenarios(self) -> "ProjectModel": if hasattr(self, "project_path") and self.project_path is None: return self - + if hasattr(self, "scenarios") and not self.scenarios: raise ValueError("project['scenarios'] cannot be empty") return self - + def dict(self, *args, **kwargs): data = super().dict(*args, **kwargs) data.pop("active_project_path") @@ -391,22 +442,24 @@ def dict(self, *args, **kwargs): class ExportsModel(InputsBaseModel): """Defines the user inputs for defining data exports.""" - export_results: Annotated[ - bool, + export_results: Annotated[ + bool, Field( title="export_results", description="Set to true to export circuit element values at each time point.", default=False, alias="Log Results", - )] + ), + ] export_elements: Annotated[ - bool, + bool, Field( title="export_elements", description="Set to true to export static information for all circuit elements.", default=True, alias="Export Elements", - )] + ), + ] export_element_types: Annotated[ list, Field( @@ -414,50 +467,56 @@ class ExportsModel(InputsBaseModel): description="Restrict 'export_elements' to these element types. Default is all types", default=[], alias="Export Element Types", - )] - export_data_tables:Annotated[ - bool, + ), + ] + export_data_tables: Annotated[ + bool, Field( title="export_data_tables", description="Set to true to export circuit element data in tabular files. While it does " - "duplicate data, it provides a way to preserve a human-readable " - "dataset that does not require pydss to interpret.", + "duplicate data, it provides a way to preserve a human-readable " + "dataset that does not require pydss to interpret.", default=True, alias="Export Data Tables", - )] + ), + ] export_pv_profiles: Annotated[ - bool, + bool, Field( title="export_pv_profiles", description="Set to true to export PV profiles to tabular files.", default=False, alias="Export PV Profiles", - )] + ), + ] export_data_in_memory: Annotated[ - bool, + bool, Field( title="export_data_in_memory", description="Set to true to keep circuit element data in memory rather than periodically " "flushing to an HDF5 file. This can be faster but will consume more memory.", default=False, alias="Export Data In Memory", - )] + ), + ] export_node_names_by_type: Annotated[ - bool, + bool, Field( title="export_node_names_by_type", description="Set to true to export node names by primary/secondary type to a file.", default=False, alias="Export Node Names By Type", - )] + ), + ] export_event_log: Annotated[ - bool, + bool, Field( title="export_event_log", description="Set to true to export the OpenDSS event log.", default=True, alias="Export Event Log", - )] + ), + ] export_format: Annotated[ FileFormat, Field( @@ -465,26 +524,29 @@ class ExportsModel(InputsBaseModel): description="Controls the file format used if export_data_tables is true.", default=FileFormat.HDF5, alias="Export Format", - )] + ), + ] export_compression: Annotated[ - bool, + bool, Field( title="export_compression", description="Set to true to compress data exported with 'export_data_tables'.", default=False, alias="Export Compression", - )] + ), + ] hdf_max_chunk_bytes: Annotated[ - int, + int, Field( title="hdf_max_chunk_bytes", description="The chunk size in bytes to use for exported data in the HDF5 data store. " - "The value is passed to the h5py package. Refer to " - "http://docs.h5py.org/en/stable/high/dataset.html#chunked-storage for more " - "information.", + "The value is passed to the h5py package. Refer to " + "http://docs.h5py.org/en/stable/high/dataset.html#chunked-storage for more " + "information.", default=DEFAULT_MAX_CHUNK_BYTES, alias="HDF Max Chunk Bytes", - )] + ), + ] @model_validator(mode="before") @classmethod @@ -501,7 +563,7 @@ def check_hdf_max_chunk_bytes(cls, val): if val < min: raise ValueError(f"hdf_max_chunk_bytes must be >= {min}") if val % 512 != 0: - raise ValueError(f"hdf_max_chunk_bytes must be a multiple of 512") + raise ValueError("hdf_max_chunk_bytes must be a multiple of 512") return val @@ -509,67 +571,76 @@ class FrequencyModel(InputsBaseModel): """Defines the user inputs for defining frequency parameters.""" enable_frequency_sweep: Annotated[ - bool, + bool, Field( title="enable_frequency_sweep", description="Enable harmonic sweep. Works with only 'Static' and 'QSTS' simulation modes.", default=False, alias="Enable frequency sweep", - )] + ), + ] fundamental_frequency: Annotated[ - float, + float, Field( title="fundamental_frequency", description="Fundamental system frequeny in Hertz", default=60.0, alias="Fundamental frequency", - )] + ), + ] start_frequency: Annotated[ - float, + float, Field( title="start_frequency", description="Start system frequeny in Hertz", default=1.0, alias="Start frequency", - )] + ), + ] end_frequency: Annotated[ - float, + float, Field( title="end_frequency", description="End system frequeny in Hertz", default=15.0, alias="End frequency", - )] + ), + ] frequency_increment: Annotated[ - float, + float, Field( title="frequency_increment", description="As multiple of fundamental", default=2.0, alias="frequency increment", - )] + ), + ] neglect_shunt_admittance: Annotated[ - bool, + bool, Field( title="neglect_shunt_admittance", description="Neglect shunt addmittance for frequency sweep", default=False, alias="Neglect shunt admittance", - )] + ), + ] percentage_load_in_series: Annotated[ - float, + float, Field( title="percentage_load_in_series", description="Percent of load that is series RL for Harmonic studies", default=50.0, alias="Percentage load in series", - )] - - @model_validator(mode='after') - def check_end_frequency(self)-> 'FrequencyModel': + ), + ] + + @model_validator(mode="after") + def check_end_frequency(self) -> "FrequencyModel": start = self.start_frequency if start > self.end_frequency: - raise ValueError(f"start_frequency={start} must be less than end_frequency={self.end_frequency}") + raise ValueError( + f"start_frequency={start} must be less than end_frequency={self.end_frequency}" + ) return self @field_validator("fundamental_frequency") @@ -592,93 +663,104 @@ class HelicsModel(InputsBaseModel): """Defines the user inputs for HELICS.""" co_simulation_mode: Annotated[ - bool, + bool, Field( title="co_simulation_mode", description="Set to true to enable the HELICS interface for co-simulation.", default=False, alias="Co-simulation Mode", - )] + ), + ] iterative_mode: Annotated[ - bool, + bool, Field( title="iterative_mode", description="Iterative mode", default=False, alias="Iterative Mode", - )] + ), + ] error_tolerance: Annotated[ - float, + float, Field( title="error_tolerance", description="Error tolerance", default=0.0001, alias="Error tolerance", - )] + ), + ] max_co_iterations: Annotated[ - int, + int, Field( title="max_co_iterations", description="Max number of co-simulation iterations", default=15, alias="Max co-iterations", - )] + ), + ] broker: Annotated[ - str, + str, Field( title="broker", description="Broker name", default="mainbroker", alias="Broker", - )] + ), + ] broker_port: Annotated[ - int, + int, Field( title="broker_port", description="Broker port", default=0, alias="Broker port", - )] + ), + ] federate_name: Annotated[ - str, + str, Field( title="federate_name", description="Name of the federate", default="pydss", alias="Federate name", - )] + ), + ] time_delta: Annotated[ - float, + float, Field( title="time_delta", description="The property controlling the minimum time delta for a federate.", default=0.01, alias="Time delta", - )] + ), + ] core_type: Annotated[ - str, + str, Field( title="core_type", description="Core type to be use for communication", default="zmq", alias="Core type", - )] + ), + ] uninterruptible: Annotated[ - bool, + bool, Field( title="uninterruptible", description="Can the federate be interrupted", default=True, alias="Uninterruptible", - )] + ), + ] logging_level: Annotated[ - int, + int, Field( title="logging_level", description="Logging level for the federate. Refer to HELICS documentation.", default=5, alias="Helics logging level", - )] + ), + ] @field_validator("logging_level") @classmethod @@ -705,39 +787,44 @@ class LoggingModel(InputsBaseModel): description="Pydss minimum logging level", default=LoggingLevel.INFO, alias="Logging Level", - )] - enable_console: Annotated[ + ), + ] + enable_console: Annotated[ Optional[bool], Field( title="enable_console", description="Set to true to enable console logging.", default=True, alias="Display on screen", - )] - enable_file: Annotated[ + ), + ] + enable_file: Annotated[ Optional[bool], Field( title="enable_file", description="Set to true to enable logging to a file.", default=True, alias="Log to external file", - )] - clear_old_log_file: Annotated[ - Optional[bool], + ), + ] + clear_old_log_file: Annotated[ + Optional[bool], Field( title="clear_old_log_file", description="Set to true to clear and overwrite any existing log files.", default=False, alias="Clear old log file", - )] - log_time_step_updates: Annotated[ - Optional[bool], + ), + ] + log_time_step_updates: Annotated[ + Optional[bool], Field( title="log_time_step_updates", description="Set to true to log each completed time step.", default=True, alias="Log time step updates", - )] + ), + ] @model_validator(mode="before") @classmethod @@ -757,61 +844,66 @@ class MonteCarloModel(InputsBaseModel): """Defines the user inputs for Monte Carlo simulations.""" num_scenarios: Annotated[ - int, + int, Field( title="num_scenarios", description="Number of Monte Carlo scenarios", default=-1, alias="Number of Monte Carlo scenarios", - )] - + ), + ] class ProfilesModel(InputsBaseModel): """Defines user inputs for the Profile Manager.""" use_profile_manager: Annotated[ - bool, + bool, Field( title="use_profile_manager", description="Set to true to enable the Profile Manager.", default=False, alias="Use profile manager", - )] + ), + ] source_type: Optional[FileFormat] = Field( title="source_type", description="File format for source data", default=FileFormat.HDF5, ) source: Annotated[ - str, + str, Field( title="source", description="File containing source data", default="Profiles_backup.hdf5", - )] + ), + ] profile_mapping: Annotated[ - str, + str, Field( title="profile_mapping", description="Profile mapping", default="", alias="Profile mapping", - )] + ), + ] is_relative_path: Annotated[ - bool, + bool, Field( title="is_relative_path", description="Source file path is relative", default=True, - )] + ), + ] settings: Annotated[ - Dict, + Dict, Field( title="settings", description="Profiles settings", default={}, - )] + ), + ] @model_validator(mode="before") @classmethod @@ -825,223 +917,248 @@ class ReportBaseModel(InputsBaseModel): """Defines the base model for all report-specific user inputs.""" enabled: Annotated[ - bool, + bool, Field( title="enabled", description="Set to true to enable the report", default=False, - )] + ), + ] scenarios: Annotated[ - List[str], + List[str], Field( title="scenarios", description="Scenarios to which the report applies. Default is all scenarios.", default=[], - )] + ), + ] store_all_time_points: Annotated[ - bool, + bool, Field( title="store_all_time_points", description="Set to true to store data for all time points. If false, store aggregated " - "metrics in memory.", + "metrics in memory.", default=False, - )] + ), + ] class CapacitorStateChangeCountReportModel(ReportBaseModel): """Defines the user inputs for the Capacitor State Change Counts report.""" name: Annotated[ - str, + str, Field( title="name", description="Report name", default="Capacitor State Change Counts", - #internal=True, - )] + # internal=True, + ), + ] class FeederLossesReportModel(ReportBaseModel): """Defines the user inputs for the Feeder Losses report.""" name: Annotated[ - str, + str, Field( title="name", description="Report name", default="Feeder Losses", - #internal=True, - )] + # internal=True, + ), + ] class PvClippingReportModel(ReportBaseModel): """Defines the user inputs for the PV Clipping report.""" name: Annotated[ - str, + str, Field( title="name", description="Report name", default="PV Clipping", - #internal=True, - )] + # internal=True, + ), + ] diff_tolerance_percent_pmpp: Annotated[ - float, + float, Field( title="diff_tolerance_percent_pmpp", description="TBD", default=1.0, - )] + ), + ] denominator_tolerance_percent_pmpp: Annotated[ - float, + float, Field( title="denominator_tolerance_percent_pmpp", description="TBD", default=1.0, - )] + ), + ] class PvCurtailmentReportModel(ReportBaseModel): """Defines the user inputs for the PV Curtailment report.""" name: Annotated[ - str, + str, Field( title="name", description="Report name", default="PV Curtailment", - #internal=True, - )] + # internal=True, + ), + ] diff_tolerance_percent_pmpp: Annotated[ - float, + float, Field( title="diff_tolerance_percent_pmpp", description="TBD", default=1.0, - )] + ), + ] denominator_tolerance_percent_pmpp: Annotated[ - float, + float, Field( title="denominator_tolerance_percent_pmpp", description="TBD", default=1.0, - )] + ), + ] class RegControlTapNumberChangeCountsReportModel(ReportBaseModel): """Defines the user inputs for the RegControl Tap Number Change Counts report.""" name: Annotated[ - str, + str, Field( title="name", description="Report name", default="RegControl Tap Number Change Counts", - #internal=True, - )] + # internal=True, + ), + ] class ThermalMetricsReportModel(ReportBaseModel): """Defines the user inputs for the Thermal Metrics report.""" name: Annotated[ - str, + str, Field( title="name", description="Report name", default="Thermal Metrics", - #internal=True, - )] + # internal=True, + ), + ] transformer_window_size_hours: Annotated[ - int, + int, Field( title="transformer_window_size_hours", description="Transformer window size hours", default=2, - )] + ), + ] transformer_loading_percent_threshold: Annotated[ - int, + int, Field( title="transformer_loading_percent_threshold", description="Transformer loading percent threshold", default=150, - )] + ), + ] transformer_loading_percent_moving_average_threshold: Annotated[ - int, + int, Field( title="transformer_loading_percent_moving_average_threshold", description="Transformer loading percent moving average threshold", default=120, - )] + ), + ] line_window_size_hours: Annotated[ - int, + int, Field( title="line_window_size_hours", description="Line window size hours", default=1, - )] + ), + ] line_loading_percent_threshold: Annotated[ - int, + int, Field( title="line_loading_percent_threshold", description="Line loading percent threshold", default=120, - )] + ), + ] line_loading_percent_moving_average_threshold: Annotated[ - int, + int, Field( title="line_loading_percent_moving_average_threshold", description="Line loading percent moving average threshold", default=100, - )] + ), + ] store_per_element_data: Annotated[ - bool, + bool, Field( title="store_per_element_data", description="Set to true to store metrics for each line and transformer.", default=True, - )] + ), + ] class VoltageMetricsReportModel(ReportBaseModel): """Defines the user inputs for the Voltage Metrics report.""" name: Annotated[ - str, + str, Field( title="name", description="Report name", default="Voltage Metrics", - #internal=True, - )] + # internal=True, + ), + ] window_size_minutes: Annotated[ - int, + int, Field( title="window_size_minutes", description="Window size minutes", default=60, - )] + ), + ] range_a_limits: Annotated[ - List, + List, Field( title="range_a_limits", description="ANSI Range A voltage limits", default=[0.95, 1.05], - )] + ), + ] range_b_limits: Annotated[ - List, + List, Field( title="range_b_limits", description="ANSI Range B voltage limits", default=[0.90, 1.0583], - )] + ), + ] store_per_element_data: Annotated[ - bool, + bool, Field( title="store_per_element_data", description="Set to true to store metrics for each node.", default=True, - )] + ), + ] _REPORT_MAPPING = { @@ -1065,23 +1182,26 @@ class ReportsModel(InputsBaseModel): description="Controls the file format.", default=FileFormat.HDF5, alias="Format", - )] + ), + ] granularity: Annotated[ - ReportGranularity, + ReportGranularity, Field( title="granularity", description="Specifies the granularity on which data is collected.", default=ReportGranularity.PER_ELEMENT_PER_TIME_POINT, alias="Granularity", - )] + ), + ] types: Annotated[ - List[Any], + List[Any], Field( title="types", description="Reports to collect.", default=[], alias="Types", - )] + ), + ] @field_validator("types", mode="before") @classmethod @@ -1106,7 +1226,8 @@ class SimulationSettingsModel(InputsBaseModel): title="project", description="Project settings", alias="Project", - )] + ), + ] exports: Annotated[ ExportsModel, Field( @@ -1114,82 +1235,94 @@ class SimulationSettingsModel(InputsBaseModel): description="Exports settings", default=ExportsModel(), alias="Exports", - )] - frequency:Annotated[ + ), + ] + frequency: Annotated[ FrequencyModel, - Field( + Field( title="frequency", description="Frequency settings", default=FrequencyModel(), alias="Frequency", - )] + ), + ] helics: Annotated[ - HelicsModel, + HelicsModel, Field( title="helics", description="HELICS settings", default=HelicsModel(), alias="Helics", - )] + ), + ] logging: Annotated[ - LoggingModel, + LoggingModel, Field( title="logging", description="Logging settings", default=LoggingModel(), alias="Logging", - )] + ), + ] monte_carlo: Annotated[ - MonteCarloModel, + MonteCarloModel, Field( title="monte_carlo", description="Monte Carlo settings", default=MonteCarloModel(), alias="MonteCarlo", - )] + ), + ] profiles: Annotated[ - ProfilesModel, + ProfilesModel, Field( title="profiles", description="Profiles settings", default=ProfilesModel(), alias="Profiles", - )] + ), + ] reports: Annotated[ - ReportsModel, + ReportsModel, Field( title="reports", description="Reports settings", default=ReportsModel(), alias="Reports", - )] + ), + ] class ControllerMap(InputsBaseModel): - controller_type :Annotated[ - ControllerType, - Field( + controller_type: Annotated[ + ControllerType, + Field( title="controller_types", description="Should be a valid controller type registered with pydss", default=ControllerType.PV_CONTROLLER, - )] - - controller_file : Annotated[ - Path, - Field( + ), + ] + + controller_file: Annotated[ + Path, + Field( title="controller_file", description="Path to the controller settings TOML file", default=".", - )] + ), + ] + class MappedControllers(InputsBaseModel): - mapping : Annotated[ - List[ControllerMap], + mapping: Annotated[ + List[ControllerMap], Field( title="mapping", description="Mapping of contoller type to controller settings TOML file", - default=[] - )] + default=[], + ), + ] + def create_simulation_settings(path: Path, project_name: str, scenario_names: list, force=False): """Create a settings file with default values. diff --git a/src/pydss/SolveMode.py b/src/pydss/solve_mode.py similarity index 59% rename from src/pydss/SolveMode.py rename to src/pydss/solve_mode.py index 55c3b069..d5303737 100644 --- a/src/pydss/SolveMode.py +++ b/src/pydss/solve_mode.py @@ -3,23 +3,26 @@ from pydss.common import SimulationType from pydss.exceptions import InvalidParameter -from pydss.modes.Dynamic import Dynamic -from pydss.modes.Snapshot import Snapshot -from pydss.modes.QSTS import QSTS +from pydss.modes.dynamic import Dynamic +from pydss.modes.snapshot import Snapshot +from pydss.modes.qsts import QSTS from pydss.simulation_input_models import ProjectModel, SimulationSettingsModel -def GetSolver(settings: SimulationSettingsModel, dssInstance): - logger.info('Setting solver to %s mode.', settings.project.simulation_type.value) +def get_solver(settings: SimulationSettingsModel, dss_instance): + logger.info("Setting solver to %s mode.", settings.project.simulation_type.value) return get_solver_from_simulation_type(settings.project) def get_solver_from_simulation_type(settings: ProjectModel): """Return a solver from the simulation type.""" if settings.simulation_type == SimulationType.SNAPSHOT: - return Snapshot(dssInstance=dss, settings=settings) + return Snapshot(dss_instance=dss, settings=settings) elif settings.simulation_type == SimulationType.QSTS: - return QSTS(dssInstance=dss, settings=settings) + return QSTS(dss_instance=dss, settings=settings) elif settings.simulation_type == SimulationType.DYNAMIC: - return Dynamic(dssInstance=dss, settings=settings) + return Dynamic(dss_instance=dss, settings=settings) raise InvalidParameter(f"{settings.simulation_type} does not have a supported solver") + + +globals()["GetSolver"] = get_solver diff --git a/src/pydss/storage_filters.py b/src/pydss/storage_filters.py index a0ec8b86..05353797 100644 --- a/src/pydss/storage_filters.py +++ b/src/pydss/storage_filters.py @@ -1,4 +1,3 @@ - import copy import abc @@ -15,7 +14,10 @@ class StorageFilterBase(abc.ABC): Subclasses can perform custom filtering based on StoreValuesType. """ - def __init__(self, hdf_store, path, prop, num_steps, max_chunk_bytes, values, elem_names, **kwargs): + + def __init__( + self, hdf_store, path, prop, num_steps, max_chunk_bytes, values, elem_names, **kwargs + ): self._prop = prop self._container = self.make_container( hdf_store, @@ -95,6 +97,7 @@ def append_values(self, values, time_step): class StorageMin(StorageFilterBase): """Stores the min value across time points.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._min = None @@ -115,13 +118,13 @@ def _handle_values(self, values): else: for i, new_val in enumerate(values): cur_val = self._min[i] - if (np.isnan(cur_val.value) and not np.isnan(new_val.value)) or \ - new_val < cur_val: + if (np.isnan(cur_val.value) and not np.isnan(new_val.value)) or new_val < cur_val: self._min[i].set_value(new_val.value) class StorageMax(StorageFilterBase): """Stores the max value across time points.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._max = None @@ -142,13 +145,13 @@ def _handle_values(self, values): else: for i, new_val in enumerate(values): cur_val = self._max[i] - if (np.isnan(cur_val.value) and not np.isnan(new_val.value)) or \ - new_val > cur_val: + if (np.isnan(cur_val.value) and not np.isnan(new_val.value)) or new_val > cur_val: self._max[i].set_value(new_val.value) class StorageMovingAverage(StorageFilterBase): """Stores a moving average across time points.""" + def __init__(self, *args, **kwargs): """Constructor for StorageMovingAverage. @@ -188,6 +191,7 @@ def append_values(self, values, time_step): class StorageMovingAverageMax(StorageMax): """Stores the max value of a moving average across time points.""" + def __init__(self, *args, **kwargs): """Constructor for StorageMovingAverageMax. @@ -220,6 +224,7 @@ def append_values(self, values, time_step): class StorageSum(StorageFilterBase): """Keeps a running sum of all values and records the total.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._sum = None @@ -250,7 +255,7 @@ def _make_circular_buffers(num_elements, prop, window_sizes): STORAGE_TYPE_MAP = { StoreValuesType.ALL: StorageAll, - #StoreValuesType.CHANGE_COUNT: StorageChangeCount, + # StoreValuesType.CHANGE_COUNT: StorageChangeCount, StoreValuesType.MAX: StorageMax, StoreValuesType.MIN: StorageMin, StoreValuesType.MOVING_AVERAGE: StorageMovingAverage, diff --git a/src/pydss/thermal_metrics.py b/src/pydss/thermal_metrics.py index 302e804d..62db36ad 100644 --- a/src/pydss/thermal_metrics.py +++ b/src/pydss/thermal_metrics.py @@ -7,11 +7,19 @@ from pydantic import BaseModel, Field from pydss.utils.simulation_utils import CircularBufferHelper -from pydss.utils.utils import dump_data, load_data +from pydss.utils.utils import load_data from pydantic import ConfigDict + class ThermalMetricsBaseModel(BaseModel): - model_config = ConfigDict(title="ThermalMetricsBaseModel", str_strip_whitespace=True, validate_assignment=True, validate_default=True, extra="forbid", use_enum_values=False) + model_config = ConfigDict( + title="ThermalMetricsBaseModel", + str_strip_whitespace=True, + validate_assignment=True, + validate_default=True, + extra="forbid", + use_enum_values=False, + ) class ThermalMetricsModel(ThermalMetricsBaseModel): @@ -21,66 +29,77 @@ class ThermalMetricsModel(ThermalMetricsBaseModel): {}, title="max_instantaneous_loadings_pct", description="maximum instantaneous loading percent for each element", - )] + ), + ] max_instantaneous_loading_pct: Annotated[ float, Field( - default = 120, + default=120, title="max_instantaneous_loading_pct", description="maximum instantaneous loading percent overall", - )] + ), + ] max_moving_average_loadings_pct: Annotated[ Dict[str, float], Field( {}, title="max_moving_average_loadings_pct", description="maximum moving average loading percent for each element", - )] + ), + ] max_moving_average_loading_pct: Annotated[ float, Field( 100, title="max_moving_average_loading_pct", description="maximum moving average loading percent overall", - )] + ), + ] window_size_hours: Annotated[ Optional[int], Field( None, title="window_size_hours", description="window size used to calculate the moving average", - )] + ), + ] num_time_points_with_instantaneous_violations: Annotated[ Optional[int], Field( None, title="num_time_points_with_instantaneous_violations", description="number of time points where the instantaneous threshold was violated", - )] + ), + ] num_time_points_with_moving_average_violations: Annotated[ Optional[int], Field( None, title="num_time_points_with_moving_average_violations", description="number of time points where the moving average threshold was violated", - )] + ), + ] instantaneous_threshold: Annotated[ Optional[int], Field( None, title="instantaneous_threshold", description="instantaneous threshold", - )] + ), + ] moving_average_threshold: Annotated[ Optional[int], Field( None, title="moving_average_threshold", description="moving average threshold", - )] + ), + ] -def compare_thermal_metrics(metrics1: ThermalMetricsModel, metrics2: ThermalMetricsModel, rel_tol=0.001): +def compare_thermal_metrics( + metrics1: ThermalMetricsModel, metrics2: ThermalMetricsModel, rel_tol=0.001 +): """Compares the values of two instances of ThermalMetricsModel. Uses a tolerance of 0.001 for moving averages. @@ -92,10 +111,12 @@ def compare_thermal_metrics(metrics1: ThermalMetricsModel, metrics2: ThermalMetr """ match = True fields = ( - "max_instantaneous_loading_pct", "window_size_hours", + "max_instantaneous_loading_pct", + "window_size_hours", "num_time_points_with_instantaneous_violations", "num_time_points_with_moving_average_violations", - "instantaneous_threshold", "moving_average_threshold", + "instantaneous_threshold", + "moving_average_threshold", ) for field in fields: val1 = getattr(metrics1, field) @@ -104,14 +125,21 @@ def compare_thermal_metrics(metrics1: ThermalMetricsModel, metrics2: ThermalMetr logger.error("field=%s mismatch %s != %s", field, val1, val2) match = False - if not math.isclose(metrics1.max_moving_average_loading_pct, metrics2.max_instantaneous_loading_pct, rel_tol=rel_tol): - logger.error("max_moving_average_loading_pct mismatch %s != %s", - metrics1.max_moving_average_loading_pct, metrics2.max_instantaneous_loading_pct) + if not math.isclose( + metrics1.max_moving_average_loading_pct, + metrics2.max_moving_average_loading_pct, + rel_tol=rel_tol, + ): + logger.error( + "max_moving_average_loading_pct mismatch %s != %s", + metrics1.max_moving_average_loading_pct, + metrics2.max_instantaneous_loading_pct, + ) match = False for name, val1 in metrics1.max_instantaneous_loadings_pct.items(): val2 = metrics2.max_instantaneous_loadings_pct[name] - if val1 != val2: + if not math.isclose(val1, val2, rel_tol=rel_tol): logger.error("max_instantaneous_loadings_pct mismatch %s != %s", name, val1, val2) match = False @@ -130,13 +158,15 @@ class ThermalMetricsSummaryModel(ThermalMetricsBaseModel): Field( title="line_loadings", description="line loading metrics", - )] + ), + ] transformer_loadings: Annotated[ Union[ThermalMetricsModel, None], Field( title="transformer_loadings", description="transformer loading metrics", - )] + ), + ] class SimulationThermalMetricsModel(ThermalMetricsBaseModel): @@ -145,7 +175,8 @@ class SimulationThermalMetricsModel(ThermalMetricsBaseModel): Field( title="scenarios", description="thermal metrics by pydss scenario name", - )] + ), + ] def create_summary(filename): @@ -171,7 +202,9 @@ def create_summary_from_dict(data): summary = SimulationThermalMetricsModel(**data) report = defaultdict(dict) for scenario in summary.scenarios: - for model, elem_type in zip(("line_loadings", "transformer_loadings"), ("line", "transformer")): + for model, elem_type in zip( + ("line_loadings", "transformer_loadings"), ("line", "transformer") + ): model = getattr(summary.scenarios[scenario], model) if model is None: continue @@ -219,7 +252,9 @@ def __init__( self._transformer_loading_percent_threshold = transformer_loading_percent_threshold self._transformer_window_size_hours = transformer_window_size_hours self._transformer_window_size = transformer_window_size - self._transformer_loading_percent_mavg_threshold = transformer_loading_percent_moving_average_threshold + self._transformer_loading_percent_mavg_threshold = ( + transformer_loading_percent_moving_average_threshold + ) self._num_time_points_inst_line_violations = 0 self._num_time_points_mavg_line_violations = 0 self._num_time_points_inst_transformer_violations = 0 @@ -273,9 +308,13 @@ def generate_report(self, path): inst_violations_by_transformer = {} mavg_violations_by_transformer = {} for i in range(len(self._max_inst_transformer_violations)): - inst_violations_by_transformer[self._transformer_names[i]] = self._max_inst_transformer_violations[i] + inst_violations_by_transformer[self._transformer_names[i]] = ( + self._max_inst_transformer_violations[i] + ) for i in range(len(self._max_mavg_transformer_violations)): - mavg_violations_by_transformer[self._transformer_names[i]] = self._max_mavg_transformer_violations[i] + mavg_violations_by_transformer[self._transformer_names[i]] = ( + self._max_mavg_transformer_violations[i] + ) if self.has_transformers(): transformer_metric = ThermalMetricsModel( @@ -344,8 +383,13 @@ def update(self, time_step, line_loadings, transformer_loadings): """ if self._line_bufs is None: - self._line_bufs = [CircularBufferHelper(self._line_window_size) for _ in range(len(self._line_names))] - self._transformer_bufs = [CircularBufferHelper(self._transformer_window_size) for _ in range(len(self._transformer_names))] + self._line_bufs = [ + CircularBufferHelper(self._line_window_size) for _ in range(len(self._line_names)) + ] + self._transformer_bufs = [ + CircularBufferHelper(self._transformer_window_size) + for _ in range(len(self._transformer_names)) + ] self._max_inst_line_violations = [0.0] * len(self._line_names) self._max_mavg_line_violations = [0.0] * len(self._line_names) self._max_inst_transformer_violations = [0.0] * len(self._transformer_names) @@ -365,7 +409,10 @@ def update(self, time_step, line_loadings, transformer_loadings): if moving_avg > self._max_mavg_line_violations[i]: self._max_mavg_line_violations[i] = moving_avg - if not has_mavg_line_violation and moving_avg > self._line_loading_percent_mavg_threshold: + if ( + not has_mavg_line_violation + and moving_avg > self._line_loading_percent_mavg_threshold + ): has_mavg_line_violation = True has_inst_transformer_violation = False @@ -373,7 +420,10 @@ def update(self, time_step, line_loadings, transformer_loadings): for i, loading in enumerate(transformer_loadings): if loading.value > self._max_inst_transformer_violations[i]: self._max_inst_transformer_violations[i] = loading.value - if not has_inst_transformer_violation and loading.value > self._transformer_loading_percent_threshold: + if ( + not has_inst_transformer_violation + and loading.value > self._transformer_loading_percent_threshold + ): has_inst_transformer_violation = True buf = self._transformer_bufs[i] @@ -381,7 +431,10 @@ def update(self, time_step, line_loadings, transformer_loadings): moving_avg = buf.average() if moving_avg > self._max_mavg_transformer_violations[i]: self._max_mavg_transformer_violations[i] = moving_avg - if not has_mavg_transformer_violation and moving_avg > self._transformer_loading_percent_mavg_threshold: + if ( + not has_mavg_transformer_violation + and moving_avg > self._transformer_loading_percent_mavg_threshold + ): has_mavg_transformer_violation = True if has_inst_line_violation: diff --git a/src/pydss/unitDefinations.py b/src/pydss/unitDefinations.py deleted file mode 100644 index 7371732b..00000000 --- a/src/pydss/unitDefinations.py +++ /dev/null @@ -1,125 +0,0 @@ - -standard_mapper = { - 'current_mag' : ["CurrentsMagAng"], - 'current_angle' : ["CurrentsMagAng"], - 'current_real': ["Currents"], - 'current_imag': ["Currents"], - 'current_mag_rated': ["RatedCurrent", "NormalAmps", "normamps"], - 'current_mag_emgcy': ["EmergAmps"], - 'losses_real': ["Losses", 'PhaseLosses'], - 'losses_imag': ["Losses", 'PhaseLosses'], - 'sub_losses_real': ["SubstationLosses"], - 'sub_losses_imag': ["SubstationLosses"], - 'line_losses_real': ["LineLosses"], - 'line_losses_imag': ["LineLosses"], - 'power_real': ["Powers", 'TotalPower'], - 'power_imag': ["Powers", 'TotalPower'], - 'voltage_mag': ["puVmagAngle", "VoltagesMagAng"], - 'voltage_angle': ["puVmagAngle", "VoltagesMagAng"], - 'voltage_real': ["Voltages"], - 'voltage_imag': ["Voltages"], - 'voltage_mag_max_setpoint' : ['Vmaxpu'], - 'voltage_mag_min_setpoint' : ['Vminpu'], - 'frequency': ['Frequency'], - 'tap': ['Taps'], - 'soc': ['%stored'], - 'power_real_setpoint': ['kW'], - 'power_imagl_setpoint': ['kvar'], - 'voltage_setpoint': ['kV'], - 'power_mag_rated': ['kVARated'], - 'power_mag_base': ['kVABase'], - 'energy': ['kWh'], -} - -unit_info = { - 'CurrentsMagAng' : {'E' : 'amp', 'O' : 'deg'}, - 'Currents': {'E': 'amp', 'O': 'amp:'}, - 'RatedCurrent': 'amp', - 'EmergAmps': 'amp', - 'NormalAmps': 'amp', - 'normamps': 'amp', - 'Losses': {'E' : 'kw', 'O' : 'kvar'}, - 'PhaseLosses': {'E' : 'kw', 'O' : 'kvar'}, - 'Powers': {'E' : 'kw', 'O' : 'kvar'}, - 'TotalPower': {'E' : 'kw', 'O' : 'kvar'}, - 'LineLosses': {'E' : 'kw', 'O' : 'kvar'}, - 'SubstationLosses': {'E' : 'kw', 'O' : 'kvar'}, - 'kV': 'kV', - 'kVARated': 'kvar', - 'kvar': 'kvar', - 'kW': 'kw', - 'kVABase': 'kva', - 'kWh': 'kwh', - 'puVmagAngle' : {'E' : 'pu', 'O' : 'deg'}, - 'VoltagesMagAng': {'E' : 'v', 'O' : 'deg'}, - 'VMagAngle': {'E' : 'v', 'O' : 'deg'}, - 'Voltages': {'E': 'v', 'O': 'v'}, - 'Vmaxpu': 'pu', - 'Vminpu': 'pu', - 'Frequency' : 'Hz', - 'Taps' : 'pu', - '%stored' : 'SOC', - 'Distance' : 'mi' - } - -info_map = { - 'CurrentsMagAng' : {'E' : 'current_mag', 'O' : 'current_angle'}, - 'Currents': {'E': 'current_real', 'O': 'current_imag'}, - 'Losses': {'E' : 'losses_real', 'O' : 'losses_imag'}, - 'PhaseLosses': {'E' : 'losses_real', 'O' : 'losses_imag'}, - 'Powers': {'E' : 'power_real', 'O' : 'power_imag'}, - 'TotalPower': {'E' : 'power_real', 'O' : 'power_imag'}, - 'LineLosses': {'E' : 'line_losses_real', 'O' : 'line_losses_imag'}, - 'SubstationLosses': {'E' : 'sub_losses_real', 'O' : 'sub_losses_imag'}, - 'puVmagAngle' : {'E' : 'voltage_mag', 'O' : 'voltage_angle'}, - 'VoltagesMagAng': {'E' : 'voltage_mag', 'O' : 'voltage_angle'}, - 'VMagAngle': {'E' : 'voltage_mag', 'O' : 'voltage_angle'}, - 'Voltages': {'E': 'voltage_real', 'O': 'voltage_imag'}, - 'Vmaxpu': 'voltage_mag_max_setpoint', - 'Vminpu': 'voltage_mag_min_setpoint', - 'EmergAmps': 'current_mag_emgcy', - 'RatedCurrent': 'current_mag_rated', - 'NormalAmps': 'current_mag_rated', - 'normamps': 'current_mag_rated', - 'Frequency' : 'frequency', - 'Taps' : 'tap', - '%stored' : 'soc', - 'Distance' : 'distance', - 'kV': 'voltage_setpoint', - 'kVARated': 'power_mag_rated', - 'kvar': 'power_imag_setpoint', - 'kW': 'power_real_setpoint', - 'kVABase': 'power_mag_base', - 'kWh': 'energy', - } - -type_info = { - 'CurrentsMagAng': 'vector', - 'Currents': 'vector', - 'RatedCurrent': 'double', - 'EmergAmps': 'double', - 'NormalAmps': 'double', - 'normamps': 'Amp', - 'Losses': 'vector', - 'PhaseLosses': 'vector', - 'Powers': 'vector', - 'TotalPower': 'vector', - 'LineLosses': 'vector', - 'SubstationLosses': 'vector', - 'kV': 'double', - 'kVARated': 'double', - 'kvar': 'double', - 'kW': 'double', - 'kVABase': 'double', - 'kWh': 'double', - 'puVmagAngle': 'vector', - 'VoltagesMagAng': 'vector', - 'VMagAngle': 'vector', - 'Voltages': 'vector', - 'Vmaxpu': 'double', - 'Vminpu': 'double', - 'Frequency': 'double', - 'Taps': 'vector', - '%stored': 'double', - 'Distance': 'double' - } diff --git a/src/pydss/unit_definitions.py b/src/pydss/unit_definitions.py new file mode 100644 index 00000000..19a1041a --- /dev/null +++ b/src/pydss/unit_definitions.py @@ -0,0 +1,124 @@ +standard_mapper = { + "current_mag": ["CurrentsMagAng"], + "current_angle": ["CurrentsMagAng"], + "current_real": ["Currents"], + "current_imag": ["Currents"], + "current_mag_rated": ["RatedCurrent", "NormalAmps", "normamps"], + "current_mag_emgcy": ["EmergAmps"], + "losses_real": ["Losses", "PhaseLosses"], + "losses_imag": ["Losses", "PhaseLosses"], + "sub_losses_real": ["SubstationLosses"], + "sub_losses_imag": ["SubstationLosses"], + "line_losses_real": ["LineLosses"], + "line_losses_imag": ["LineLosses"], + "power_real": ["Powers", "TotalPower"], + "power_imag": ["Powers", "TotalPower"], + "voltage_mag": ["puVmagAngle", "VoltagesMagAng"], + "voltage_angle": ["puVmagAngle", "VoltagesMagAng"], + "voltage_real": ["Voltages"], + "voltage_imag": ["Voltages"], + "voltage_mag_max_setpoint": ["Vmaxpu"], + "voltage_mag_min_setpoint": ["Vminpu"], + "frequency": ["Frequency"], + "tap": ["Taps"], + "soc": ["%stored"], + "power_real_setpoint": ["kW"], + "power_imagl_setpoint": ["kvar"], + "voltage_setpoint": ["kV"], + "power_mag_rated": ["kVARated"], + "power_mag_base": ["kVABase"], + "energy": ["kWh"], +} + +unit_info = { + "CurrentsMagAng": {"E": "amp", "O": "deg"}, + "Currents": {"E": "amp", "O": "amp:"}, + "RatedCurrent": "amp", + "EmergAmps": "amp", + "NormalAmps": "amp", + "normamps": "amp", + "Losses": {"E": "kw", "O": "kvar"}, + "PhaseLosses": {"E": "kw", "O": "kvar"}, + "Powers": {"E": "kw", "O": "kvar"}, + "TotalPower": {"E": "kw", "O": "kvar"}, + "LineLosses": {"E": "kw", "O": "kvar"}, + "SubstationLosses": {"E": "kw", "O": "kvar"}, + "kV": "kV", + "kVARated": "kvar", + "kvar": "kvar", + "kW": "kw", + "kVABase": "kva", + "kWh": "kwh", + "puVmagAngle": {"E": "pu", "O": "deg"}, + "VoltagesMagAng": {"E": "v", "O": "deg"}, + "VMagAngle": {"E": "v", "O": "deg"}, + "Voltages": {"E": "v", "O": "v"}, + "Vmaxpu": "pu", + "Vminpu": "pu", + "Frequency": "Hz", + "Taps": "pu", + "%stored": "SOC", + "Distance": "mi", +} + +info_map = { + "CurrentsMagAng": {"E": "current_mag", "O": "current_angle"}, + "Currents": {"E": "current_real", "O": "current_imag"}, + "Losses": {"E": "losses_real", "O": "losses_imag"}, + "PhaseLosses": {"E": "losses_real", "O": "losses_imag"}, + "Powers": {"E": "power_real", "O": "power_imag"}, + "TotalPower": {"E": "power_real", "O": "power_imag"}, + "LineLosses": {"E": "line_losses_real", "O": "line_losses_imag"}, + "SubstationLosses": {"E": "sub_losses_real", "O": "sub_losses_imag"}, + "puVmagAngle": {"E": "voltage_mag", "O": "voltage_angle"}, + "VoltagesMagAng": {"E": "voltage_mag", "O": "voltage_angle"}, + "VMagAngle": {"E": "voltage_mag", "O": "voltage_angle"}, + "Voltages": {"E": "voltage_real", "O": "voltage_imag"}, + "Vmaxpu": "voltage_mag_max_setpoint", + "Vminpu": "voltage_mag_min_setpoint", + "EmergAmps": "current_mag_emgcy", + "RatedCurrent": "current_mag_rated", + "NormalAmps": "current_mag_rated", + "normamps": "current_mag_rated", + "Frequency": "frequency", + "Taps": "tap", + "%stored": "soc", + "Distance": "distance", + "kV": "voltage_setpoint", + "kVARated": "power_mag_rated", + "kvar": "power_imag_setpoint", + "kW": "power_real_setpoint", + "kVABase": "power_mag_base", + "kWh": "energy", +} + +type_info = { + "CurrentsMagAng": "vector", + "Currents": "vector", + "RatedCurrent": "double", + "EmergAmps": "double", + "NormalAmps": "double", + "normamps": "Amp", + "Losses": "vector", + "PhaseLosses": "vector", + "Powers": "vector", + "TotalPower": "vector", + "LineLosses": "vector", + "SubstationLosses": "vector", + "kV": "double", + "kVARated": "double", + "kvar": "double", + "kW": "double", + "kVABase": "double", + "kWh": "double", + "puVmagAngle": "vector", + "VoltagesMagAng": "vector", + "VMagAngle": "vector", + "Voltages": "vector", + "Vmaxpu": "double", + "Vminpu": "double", + "Frequency": "double", + "Taps": "vector", + "%stored": "double", + "Distance": "double", +} diff --git a/src/pydss/utils/dataframe_utils.py b/src/pydss/utils/dataframe_utils.py index dd91dbf4..e18c8284 100644 --- a/src/pydss/utils/dataframe_utils.py +++ b/src/pydss/utils/dataframe_utils.py @@ -1,4 +1,3 @@ - import shutil import gzip import os @@ -9,8 +8,15 @@ from pydss.exceptions import InvalidParameter -def read_dataframe(filename, index_col=None, columns=None, parse_dates=False, - remove_unnamed=True, strip_column_units=False, **kwargs): +def read_dataframe( + filename, + index_col=None, + columns=None, + parse_dates=False, + remove_unnamed=True, + strip_column_units=False, + **kwargs, +): """Convert filename to a dataframe. Supports .csv, .json, .h5. Handles compressed files. @@ -47,14 +53,13 @@ def read_dataframe(filename, index_col=None, columns=None, parse_dates=False, ext = os.path.splitext(filename) if ext[1] == ".gz": ext = os.path.splitext(ext[0])[1] - open_func = gzip.open else: ext = ext[1] - open_func = open if ext == ".csv": - df = pd.read_csv(filename, index_col=index_col, usecols=columns, - parse_dates=parse_dates, **kwargs) + df = pd.read_csv( + filename, index_col=index_col, usecols=columns, parse_dates=parse_dates, **kwargs + ) elif ext == ".json": df = pd.read_json(filename, **kwargs) elif ext == ".h5": @@ -85,9 +90,8 @@ def read_dataframe(filename, index_col=None, columns=None, parse_dates=False, return df -def write_dataframe(df, file_path, compress=False, keep_original=False, - **kwargs): +def write_dataframe(df, file_path, compress=False, keep_original=False, **kwargs): """Write the dataframe to a file with in a format matching the extension. Note that the h5 format does not support row indices. @@ -111,8 +115,9 @@ def write_dataframe(df, file_path, compress=False, keep_original=False, InvalidParameter if the DataFrame index is set. """ - if not isinstance(df.index, pd.RangeIndex) and not \ - isinstance(df.index, pd.core.indexes.base.Index): + if not isinstance(df.index, pd.RangeIndex) and not isinstance( + df.index, pd.core.indexes.base.Index + ): raise InvalidParameter("DataFrame index must not be set") ext = os.path.splitext(file_path)[1] @@ -127,9 +132,9 @@ def write_dataframe(df, file_path, compress=False, keep_original=False, complevel = 9 else: complevel = 0 - kwargs["complevel"] = complevel - kwargs["mode"]="w" - kwargs["key"]= "data" + kwargs["complevel"] = complevel + kwargs["mode"] = "w" + kwargs["key"] = "data" df.to_hdf(file_path, **kwargs) elif ext == ".json": df.to_json(file_path, **kwargs) diff --git a/src/pydss/utils/dss_utils.py b/src/pydss/utils/dss_utils.py index 388e56dc..ea1ad505 100644 --- a/src/pydss/utils/dss_utils.py +++ b/src/pydss/utils/dss_utils.py @@ -1,4 +1,3 @@ - import re import opendssdirect as dss @@ -66,7 +65,7 @@ def func(): # raise InvalidConfiguration( # f"SInterval for all LoadShapes must be the same: {res}" # ) - + return res[0] @@ -145,7 +144,7 @@ def list_element_names_by_class(element_class): if element_class is dss.PVsystems: class_name = "PVSystem" else: - class_name = element_class.__name__.split('.')[1] + class_name = element_class.__name__.split(".")[1] # TODO: confirm that this covers everything. if class_name.endswith("s"): class_name = class_name[:-1] diff --git a/src/pydss/utils/pydss_utils.py b/src/pydss/utils/pydss_utils.py index 3b88e153..b710b8a4 100644 --- a/src/pydss/utils/pydss_utils.py +++ b/src/pydss/utils/pydss_utils.py @@ -1,24 +1,32 @@ import numpy as np import math -def form_Yprim(values): + +def form_yprim(values): + dimension = int(math.sqrt(len(values) / 2)) + yprim = np.array([[complex(0, 0)] * dimension] * dimension) + for row in range(dimension): + for column in range(dimension): + yprim[row][column] = complex( + values[dimension * row * 2 + 2 * column], + values[dimension * row * 2 + 2 * column + 1], + ) + return yprim + + +def form_yprim_2(values): dimension = int(math.sqrt(len(values) / 2)) - Yprim = np.array([[complex(0, 0)] * dimension] * dimension) - for ii in range(dimension): - for jj in range(dimension): - Yprim[ii][jj] = complex(values[dimension * ii * 2 + 2 * jj], values[dimension * ii * 2 + 2 * jj + 1]) - return Yprim + real = np.array(values[0::2]).reshape((dimension, dimension)) + imag = np.array(values[1::2]).reshape((dimension, dimension)) + return real + 1j * imag -def form_Yprim_2(values): - Ydim = int(math.sqrt(len(values) / 2)) - Yreal = np.array(values[0::2]).reshape((Ydim, Ydim)) - Yimag = np.array(values[1::2]).reshape((Ydim, Ydim)) - Yprim = Yreal + 1j * Yimag - return Yprim +def get_yprime_matrix(dss_objects): + elements = dss_objects["Lines"] + dss_objects["Transformers"] + n_elements = len(elements) + np.array([[complex(0, 0)] * 2 * n_elements] * 2 * n_elements) -def get_Yprime_Matrix(dssObjects): - Elements = dssObjects["Lines"] + dssObjects["Transformers"] - nElements = len(Elements) - Ybranch_prim = np.array([[complex(0, 0)] * 2 * nElements] * 2 * nElements) +globals()["form_Yprim"] = form_yprim +globals()["form_Yprim_2"] = form_yprim_2 +globals()["get_Yprime_Matrix"] = get_yprime_matrix diff --git a/src/pydss/utils/simulation_utils.py b/src/pydss/utils/simulation_utils.py index 487915fd..6e4fd365 100644 --- a/src/pydss/utils/simulation_utils.py +++ b/src/pydss/utils/simulation_utils.py @@ -1,4 +1,3 @@ - from datetime import timedelta from collections import deque import time @@ -10,6 +9,7 @@ from pydss.simulation_input_models import SimulationSettingsModel from pydss.common import TIME_FORMAT + class CircularBufferHelper: def __init__(self, window_size): self._buf = deque(maxlen=window_size) @@ -29,6 +29,7 @@ def average(self): class SimulationFilteredTimeRange: """Provides filtering in a time range.""" + def __init__(self, start, end): self._start = time.strptime(start, TIME_FORMAT) self._end = time.strptime(end, TIME_FORMAT) @@ -70,12 +71,20 @@ def is_within_range(self, timestamp): if self._no_filtering: return True - ts = time.struct_time(( - self._start.tm_year, self._start.tm_mon, self._start.tm_mday, timestamp.hour, - timestamp.minute, timestamp.second, self._start.tm_wday, self._start.tm_yday, - self._start.tm_isdst - )) - return ts >= self._start and ts <=self._end + ts = time.struct_time( + ( + self._start.tm_year, + self._start.tm_mon, + self._start.tm_mday, + timestamp.hour, + timestamp.minute, + timestamp.second, + self._start.tm_wday, + self._start.tm_yday, + self._start.tm_isdst, + ) + ) + return ts >= self._start and ts <= self._end def get_start_time(settings: SimulationSettingsModel): diff --git a/src/pydss/utils/timing_utils.py b/src/pydss/utils/timing_utils.py index 710833e2..215162cd 100644 --- a/src/pydss/utils/timing_utils.py +++ b/src/pydss/utils/timing_utils.py @@ -5,8 +5,10 @@ from loguru import logger + def timed_info(func): """Decorator to measure and logger.info a function's execution time.""" + @functools.wraps(func) def timed_(*args, **kwargs): return _timed(func, logger.info, *args, **kwargs) @@ -16,6 +18,7 @@ def timed_(*args, **kwargs): def timed_debug(func): """Decorator to measure and logger.debug a function's execution time.""" + @functools.wraps(func) def timed_(*args, **kwargs): return _timed(func, logger.debug, *args, **kwargs) @@ -27,8 +30,7 @@ def _timed(func, log_func, *args, **kwargs): start = time.time() result = func(*args, **kwargs) total = time.time() - start - log_func("execution-time=%s func=%s", get_time_duration_string(total), - func.__name__) + log_func("execution-time=%s func=%s", get_time_duration_string(total), func.__name__) return result @@ -36,9 +38,9 @@ def get_time_duration_string(seconds): """Returns a string with the time converted to reasonable units.""" if seconds >= 1: val = "{:.3f} s".format(seconds) - elif seconds >= .001: + elif seconds >= 0.001: val = "{:.3f} ms".format(seconds * 1000) - elif seconds >= .000001: + elif seconds >= 0.000001: val = "{:.3f} us".format(seconds * 1000000) elif seconds == 0: val = "0 s" @@ -50,6 +52,7 @@ def get_time_duration_string(seconds): class TimerStats: """Tracks timing stats for one code block.""" + def __init__(self, name): self._name = name self._count = 0 @@ -99,6 +102,7 @@ def update(self, duration): class Timer: """Times a code block.""" + def __init__(self, timer_stats, name): self._start = None self._timer_stat = timer_stats.get_stat(name) @@ -110,9 +114,9 @@ def __exit__(self, exc, value, tb): self._timer_stat.update(time.perf_counter() - self._start) - class TimerStatsCollector: """Collects statistics for timed code segments.""" + def __init__(self): self._stats = {} @@ -177,9 +181,11 @@ def track_timing(timer_stats): timer_stats : TimerStatsCollector """ + def wrap(func): def timed_(*args, **kwargs): return _timed_func(timer_stats, func, *args, **kwargs) + return timed_ return wrap diff --git a/src/pydss/utils/utils.py b/src/pydss/utils/utils.py index 56b8ef63..b2c81a7a 100644 --- a/src/pydss/utils/utils.py +++ b/src/pydss/utils/utils.py @@ -19,14 +19,15 @@ MAX_PATH_LENGTH = 255 -DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f' # '%Y-%m-%d %H:%M:%S.%f', "%m/%d/%Y %H:%M:%S" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S.%f" # '%Y-%m-%d %H:%M:%S.%f', "%m/%d/%Y %H:%M:%S" + class TomlEnumEncoder(toml.TomlEncoder): """Encodes Enum values instead of Enum objects.""" def dump_value(self, v): if isinstance(v, enum.Enum): - return f"\"{v.value}\"" + return f'"{v.value}"' return super().dump_value(v) @@ -63,7 +64,9 @@ def dump_data(data, filename, **kwargs): with open(filename, "w") as f_out: mod.dump(data, f_out, **kwargs) - logger.debug(f"Dumped data to {filename}", ) + logger.debug( + f"Dumped data to {filename}", + ) def load_data(filename, **kwargs): @@ -83,7 +86,9 @@ def load_data(filename, **kwargs): with open(filename) as f_in: data = mod.load(f_in) - logger.debug(f"Loaded data from {filename}", ) + logger.debug( + f"Loaded data from {filename}", + ) return data @@ -170,7 +175,7 @@ def make_human_readable_size(size, decimals=2): str: Human reable size string with unit. """ - for unit in ["B","KB","MB","GB","TB"]: + for unit in ["B", "KB", "MB", "GB", "TB"]: if size < 1024.0: break size /= 1024.0 diff --git a/src/pydss/value_storage.py b/src/pydss/value_storage.py index 9f7304ab..c147705c 100644 --- a/src/pydss/value_storage.py +++ b/src/pydss/value_storage.py @@ -1,9 +1,7 @@ - import abc import os import re -from loguru import logger import numpy as np from pydss.common import DatasetPropertyType, INTEGER_NAN @@ -12,7 +10,6 @@ class ValueStorageBase(abc.ABC): - DELIMITER = "__" def __init__(self): @@ -220,10 +217,11 @@ def value_type(self): class ValueByList(ValueStorageBase): - """"Stores a list of lists of numbers by an arbitrary suffix. This is a generic method to handle lists returned from + """ "Stores a list of lists of numbers by an arbitrary suffix. This is a generic method to handle lists returned from a function call. An example would be returned values "taps" function for transformer elements. The class can be used for any methods that return a list. """ + def __init__(self, name, prop, values, label_suffixes): """Constructor for ValueByLabel @@ -245,8 +243,9 @@ def __init__(self, name, prop, values, label_suffixes): self._labels = [] self._value_type = None self._value = [] - assert (isinstance(values, list) and len(values) == len(label_suffixes)), \ + assert isinstance(values, list) and len(values) == len(label_suffixes), ( '"values" and "label_suffixes" should be lists of equal lengths' + ) for val, lab_suf in zip(values, label_suffixes): label = prop + self.DELIMITER + lab_suf self._labels.append(label) @@ -269,9 +268,7 @@ def is_nan(self): return np.isnan(self._value[0]) def make_columns(self): - return [ - self.DELIMITER.join((self._name, f"{x}")) for x in self._labels - ] + return [self.DELIMITER.join((self._name, f"{x}")) for x in self._labels] @property def num_columns(self): @@ -313,13 +310,14 @@ def value_type(self): class ValueByNumber(ValueStorageBase): """Stores a list of numbers for an element/property.""" + def __init__(self, name, prop, value): super().__init__() assert not isinstance(value, list), str(value) self._name = name self._prop = prop self._value_type = type(value) - if self._value_type == str: + if self._value_type is str: raise InvalidConfiguration( f"Data export feature does not support strings: name={name} prop={prop} value={value}" ) @@ -379,7 +377,8 @@ class ValueByLabel(ValueStorageBase): calls like Currents, currentMagAng where every two consecutive values in the returned list are representing one quantity. The class differentiates between complex and mag / angle representation and stores the values appropriately """ - def __init__(self, name, prop, value, Nodes, is_complex, units): + + def __init__(self, name, prop, value, nodes, is_complex, units): """Constructor for ValueByLabel Parameters @@ -396,28 +395,28 @@ def __init__(self, name, prop, value, Nodes, is_complex, units): """ super().__init__() phs = { - 1: 'A', - 2: 'B', - 3: 'C', - 0: 'N', + 1: "A", + 2: "B", + 3: "C", + 0: "N", } self._name = name self._prop = prop - self._nodes = Nodes + self._nodes = nodes self._labels = [] self._value = [] self._value_type = complex if is_complex else float self._is_complex = is_complex n = 2 - m = int(len(value) / (len(Nodes)*n)) + m = int(len(value) / (len(nodes) * n)) self._m = m self._n = n self._value_length = len(value) value = self._fix_value(value) - + # Chunk_list example # X = list(range(12)) , nList= 2 # Y = chunk_list(X, nList) -> [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]] @@ -430,15 +429,15 @@ def __init__(self, name, prop, value, Nodes, is_complex, units): for i, node_val in enumerate(zip(self._nodes, value)): node, val = node_val for v, x in zip(node, val): - label = '{}{}'.format(phs[v], str(i+1)) + label = "{}{}".format(phs[v], str(i + 1)) # Note that the value logic is duplicated in set_value_from_raw if self._is_complex: label += " " + units[0] self._labels.append(label) self._value += [complex(x[0], x[1])] else: - label_mag = label + self.DELIMITER + "mag" + ' ' + units[0] - label_ang = label + self.DELIMITER + "ang" + ' ' + units[1] + label_mag = label + self.DELIMITER + "mag" + " " + units[0] + label_ang = label + self.DELIMITER + "ang" + " " + units[1] self._labels.extend([label_mag, label_ang]) self._value += [x[0], x[1]] @@ -456,9 +455,12 @@ def value(self): return self._value @staticmethod - def chunk_list(values, nLists): + def chunk_list(values, n_lists): # TODO: this breaks for Bus.puVmagAngle in monte carlo example test - return [values[i * nLists:(i + 1) * nLists] for i in range((len(values) + nLists - 1) // nLists)] + return [ + values[i * n_lists : (i + 1) * n_lists] + for i in range((len(values) + n_lists - 1) // n_lists) + ] def _fix_value(self, value): value = self.chunk_list(value, self._n) @@ -471,9 +473,7 @@ def is_nan(self): return np.isnan(self._value[0]) def make_columns(self): - return [ - self.DELIMITER.join((self._name, f"{x}")) for x in self._labels - ] + return [self.DELIMITER.join((self._name, f"{x}")) for x in self._labels] @property def num_columns(self): @@ -497,10 +497,10 @@ def set_value(self, value): def set_value_from_raw(self, value): if len(value) != self._value_length: value = [np.nan for i in range(self._value_length)] - + value = self._fix_value(value) self._value.clear() - + for i, node_val in enumerate(zip(self._nodes, value)): node, val = node_val for v, x in zip(node, val): @@ -508,6 +508,7 @@ def set_value_from_raw(self, value): self._value += [complex(x[0], x[1])] else: self._value += [x[0], x[1]] + @property def value_type(self): return self._value_type @@ -516,8 +517,17 @@ def value_type(self): class ValueContainer: """Container for a sequence of instances of ValueStorageBase.""" - def __init__(self, values, hdf_store, path, max_size, elem_names, - dataset_property_type, max_chunk_bytes=None, store_time_step=False): + def __init__( + self, + values, + hdf_store, + path, + max_size, + elem_names, + dataset_property_type, + max_chunk_bytes=None, + store_time_step=False, + ): group_name = os.path.dirname(path) basename = os.path.basename(path) self.group_name = group_name @@ -528,14 +538,14 @@ def __init__(self, values, hdf_store, path, max_size, elem_names, except KeyError: # Don't bother checking each sub path. pass - - self._length={} + + self._length = {} for value in values: if isinstance(value, list): self._length[value] = len(value.value) else: self._length[value] = 1 - + dtype = values[0].value_type scaleoffset = None # There is no np.float128 on Windows. @@ -610,16 +620,18 @@ def append(self, values): list of ValueStorageBase """ - + if values: if isinstance(values[0].value, list): vals = [x for y in values for x in y.value] else: - vals = [INTEGER_NAN if (x.is_nan() and x._value_type == int) else x.value for x in values ] + vals = [ + INTEGER_NAN if (x.is_nan() and x._value_type is int) else x.value + for x in values + ] else: vals = [self.set_nan() for k, v in self._length.items() for x in range(v)] self._dataset.write_value(vals) - def append_by_time_step(self, value, time_step, elem_index): """Append a value to the container. @@ -631,7 +643,7 @@ def append_by_time_step(self, value, time_step, elem_index): elem_index : int """ - + if isinstance(value.value, list): vals = [x for x in value.value] else: @@ -639,8 +651,7 @@ def append_by_time_step(self, value, time_step, elem_index): self._dataset.write_value(vals) self._time_steps.write_value([time_step, elem_index]) - - + def flush_data(self): """Flush any outstanding data to disk.""" self._dataset.flush_data() diff --git a/tests/__init__.py b/tests/__init__.py index c2cc6f50..911557b8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1 @@ __version__ = "3.1.2" - -from . import * diff --git a/tests/common.py b/tests/common.py index a0f6cd08..4c0eab21 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,9 +1,7 @@ - import os import shutil import tarfile import zipfile -import tempfile from pathlib import Path import opendssdirect as dss @@ -16,31 +14,23 @@ RUN_PROJECT_PATH = os.path.join("tests", "data", "project") -CUSTOM_EXPORTS_PROJECT_PATH = os.path.join( - "tests", "data", "custom_exports_project" -) -PV_REPORTS_PROJECT_PATH = os.path.join( - "tests", "data", "pv_reports_project" -) -PV_REPORTS_PROJECT_STORE_ALL_PATH = os.path.join( - "tests", "data", "pv_reports_project_store_all" -) +CUSTOM_EXPORTS_PROJECT_PATH = os.path.join("tests", "data", "custom_exports_project") +PV_REPORTS_PROJECT_PATH = os.path.join("tests", "data", "pv_reports_project") +PV_REPORTS_PROJECT_STORE_ALL_PATH = os.path.join("tests", "data", "pv_reports_project_store_all") AUTO_SNAPSHOT_TIME_POINT_PROJECT_PATH = os.path.join( "tests", "data", "auto_snapshot_time_point_project" ) -EDLIFO_PROJECT_PATH = os.path.join( - "tests", "data", "edlifo-project") +EDLIFO_PROJECT_PATH = os.path.join("tests", "data", "edlifo-project") -DYNAMIC_VOLTAGE_SUPPORT_PATH = os.path.join( - "tests", "data", "dynamic_voltage_support_test_project" -) +DYNAMIC_VOLTAGE_SUPPORT_PATH = os.path.join("tests", "data", "dynamic_voltage_support_test_project") SCENARIO_NAME = "scenario1" class FakeElement: """Fake that behaves like an OpenDSS element""" + def __init__(self, full_name, name): self.FullName = full_name self.Name = name @@ -72,25 +62,25 @@ def cleanup_project(): os.chdir(project_path) if os.path.exists(PROJECT_TAR): with tarfile.open(PROJECT_TAR) as tar: + def is_within_directory(directory, target): - + abs_directory = os.path.abspath(directory) abs_target = os.path.abspath(target) - + prefix = os.path.commonprefix([abs_directory, abs_target]) - + return prefix == abs_directory - + def safe_extract(tar, path=".", members=None, *, numeric_owner=False): - + for member in tar.getmembers(): member_path = os.path.join(path, member.name) if not is_within_directory(path, member_path): raise Exception("Attempted Path Traversal in Tar File") - - tar.extractall(path, members, numeric_owner=numeric_owner) - - + + tar.extractall(path, members, numeric_owner=numeric_owner) + safe_extract(tar) os.remove(PROJECT_TAR) pass diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..1befa4fb --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ("tests.common",) diff --git a/tests/data/auto_snapshot_time_point_project/simulation.toml b/tests/data/auto_snapshot_time_point_project/simulation.toml index d70cc899..655dc59f 100644 --- a/tests/data/auto_snapshot_time_point_project/simulation.toml +++ b/tests/data/auto_snapshot_time_point_project/simulation.toml @@ -9,7 +9,6 @@ "Convergence error percent threshold" = 0 "Skip export on convergence error" = true "Control mode" = "Time" -"Disable pydss controllers" = false "Project Path" = "./tests/data" "Simulation Type" = "QSTS" "Active Project" = "auto_snapshot_time_point_project" diff --git a/tests/data/controllers/simulation.toml b/tests/data/controllers/simulation.toml index e090fe88..ddd6caf4 100644 --- a/tests/data/controllers/simulation.toml +++ b/tests/data/controllers/simulation.toml @@ -6,7 +6,6 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false "Simulation Type" = "QSTS" "Project Path" = "./tests/data" "Active Project" = "controllers" diff --git a/tests/data/controllers/simulation_motor_stall.toml b/tests/data/controllers/simulation_motor_stall.toml index d2224e6f..2b45952e 100644 --- a/tests/data/controllers/simulation_motor_stall.toml +++ b/tests/data/controllers/simulation_motor_stall.toml @@ -6,7 +6,6 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false "Simulation Type" = "QSTS" "Project Path" = "./tests/data" "Active Project" = "controllers" diff --git a/tests/data/controllers/simulation_motor_stall_simple.toml b/tests/data/controllers/simulation_motor_stall_simple.toml index f53aa068..b85508ca 100644 --- a/tests/data/controllers/simulation_motor_stall_simple.toml +++ b/tests/data/controllers/simulation_motor_stall_simple.toml @@ -6,7 +6,6 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false "Simulation Type" = "QSTS" "Project Path" = "./tests/data" "Active Project" = "controllers" diff --git a/tests/data/controllers/simulation_pv_controller.toml b/tests/data/controllers/simulation_pv_controller.toml index f53aa068..b85508ca 100644 --- a/tests/data/controllers/simulation_pv_controller.toml +++ b/tests/data/controllers/simulation_pv_controller.toml @@ -6,7 +6,6 @@ "Max Control Iterations" = 50 "Error tolerance" = 0.001 "Control mode" = "Static" -"Disable pydss controllers" = false "Simulation Type" = "QSTS" "Project Path" = "./tests/data" "Active Project" = "controllers" diff --git a/tests/data/custom_exports_project/simulation.toml b/tests/data/custom_exports_project/simulation.toml index d279357f..d0572137 100644 --- a/tests/data/custom_exports_project/simulation.toml +++ b/tests/data/custom_exports_project/simulation.toml @@ -7,7 +7,6 @@ "Error tolerance" = 0.001 "Skip export on convergence error" = false "Control mode" = "Time" -"Disable pydss controllers" = false "Project Path" = "./tests/data" "Simulation Type" = "QSTS" "Active Project" = "custom_exports_project" diff --git a/tests/data/edlifo-project/test_sim.toml b/tests/data/edlifo-project/test_sim.toml index b1338efa..d0dc47a7 100644 --- a/tests/data/edlifo-project/test_sim.toml +++ b/tests/data/edlifo-project/test_sim.toml @@ -12,7 +12,6 @@ "Convergence error percent threshold" = 0.0 "Skip export on convergence error" = true "Control mode" = "Static" -"Disable pydss controllers" = false "Simulation Type" = "QSTS" "Project Path" = "./tests/data" "Active Project" = "edlifo-project" diff --git a/tests/data/project/simulation.toml b/tests/data/project/simulation.toml index 1dab3bd2..32633e47 100644 --- a/tests/data/project/simulation.toml +++ b/tests/data/project/simulation.toml @@ -9,7 +9,6 @@ # Max Control Iterations- [Int] - Maximum outer loop control iterations # Error tolerance- [Float] - Error tolerance in per unit # Control mode- [String] - Control mode options are "STATIC" or "Time" -# Disable pydss controllers- [Bool] - Disable all pyController in pydss. All controller implemented in DSS files will still work. # Active Project- [String] - Name of project to run # Active Scenario- [String] - Project scenario to use # DSS File- [String] - The main OpenDSS file @@ -22,7 +21,6 @@ "Max Control Iterations" = 15 "Error tolerance" = 0.001 "Control mode" = "Time" -"Disable pydss controllers" = false "Project Path" = "./tests/data" "Simulation Type" = "QSTS" "Active Project" = "project" diff --git a/tests/data/pv_reports_project/simulation.toml b/tests/data/pv_reports_project/simulation.toml index c5485167..5f8cadd4 100644 --- a/tests/data/pv_reports_project/simulation.toml +++ b/tests/data/pv_reports_project/simulation.toml @@ -10,7 +10,6 @@ "Convergence error percent threshold" = 0 "Skip export on convergence error" = true "Control mode" = "Time" -"Disable pydss controllers" = false "Project Path" = "./tests/data" "Simulation Type" = "QSTS" "Active Project" = "pv_reports_project" diff --git a/tests/data/pv_reports_project_store_all/simulation.toml b/tests/data/pv_reports_project_store_all/simulation.toml index 38e37517..933bac21 100644 --- a/tests/data/pv_reports_project_store_all/simulation.toml +++ b/tests/data/pv_reports_project_store_all/simulation.toml @@ -10,7 +10,6 @@ "Convergence error percent threshold" = 0 "Skip export on convergence error" = true "Control mode" = "Time" -"Disable pydss controllers" = false "Project Path" = "./tests/data" "Simulation Type" = "QSTS" "Active Project" = "pv_reports_project_store_all" diff --git a/tests/test_add_scenario.py b/tests/test_add_scenario.py index 85bb171f..7ef96b72 100644 --- a/tests/test_add_scenario.py +++ b/tests/test_add_scenario.py @@ -1,9 +1,7 @@ -from click.testing import CliRunner from pathlib import Path import tempfile import shutil import toml -import os from pydss.cli.add_scenario import build_scenario @@ -11,17 +9,17 @@ MAPPING_FILE = Path(__file__).parent / "data" / "add_scenario" / "controller_map.toml" - def test_add_scenario(): - + with tempfile.TemporaryDirectory() as tmpdirname: tmpdirname_obj = Path(tmpdirname) / "tmp" shutil.copytree(PYDSS_PROJECT, str(tmpdirname_obj)) settings_path = tmpdirname_obj / "simulation.toml" settings = toml.load(settings_path) - settings['Project']['Project Path'] = str(tmpdirname_obj.absolute()) + settings["Project"]["Project Path"] = str(tmpdirname_obj.absolute()) toml.dump(settings, open(settings_path, "w")) build_scenario(str(tmpdirname_obj), "test_scenario", str(MAPPING_FILE)) assert (tmpdirname_obj / "Scenarios" / "test_scenario").exists() - assert (tmpdirname_obj / "Scenarios" / "test_scenario" / "pyControllerList" / "MotorStall.toml").exists() - \ No newline at end of file + assert ( + tmpdirname_obj / "Scenarios" / "test_scenario" / "pyControllerList" / "MotorStall.toml" + ).exists() diff --git a/tests/test_auto_snapshot_time_point.py b/tests/test_auto_snapshot_time_point.py index abb4798b..4a7716ba 100644 --- a/tests/test_auto_snapshot_time_point.py +++ b/tests/test_auto_snapshot_time_point.py @@ -1,4 +1,4 @@ -from tests.common import AUTO_SNAPSHOT_TIME_POINT_PROJECT_PATH, cleanup_project +from tests.common import AUTO_SNAPSHOT_TIME_POINT_PROJECT_PATH from pydss.common import SIMULATION_SETTINGS_FILENAME from pydss.pydss_project import PyDssProject @@ -11,4 +11,3 @@ def test_auto_snapshot_time_point(cleanup_project): project = PyDssProject.load_project(AUTO_SNAPSHOT_TIME_POINT_PROJECT_PATH) settings = project.read_scenario_time_settings("max_pv_load_ratio") assert str(settings["start_time"]) == "2020-01-01 11:15:00" - diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..1325c2b9 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,38 @@ +from typer.testing import CliRunner + +from pydss.cli.pydss import cli + + +def test_cli_commands_are_registered(): + runner = CliRunner() + + result = runner.invoke(cli, ["--help"]) + + assert result.exit_code == 0 + for command in ( + "add-post-process", + "add-scenario", + "controllers", + "convert", + "create-project", + "edit-scenario", + "export", + "extract", + "extract-element-files", + "reports", + "run", + ): + assert command in result.output + + +def test_nested_cli_commands_are_registered(): + runner = CliRunner() + + for arguments, command in ( + (["controllers", "--help"], "reset-defaults"), + (["convert", "--help"], "excel-to-toml"), + (["edit-scenario", "--help"], "update-controllers"), + ): + result = runner.invoke(cli, arguments) + assert result.exit_code == 0 + assert command in result.output diff --git a/tests/test_controllers.py b/tests/test_controllers.py index e3596fee..14c9ced6 100644 --- a/tests/test_controllers.py +++ b/tests/test_controllers.py @@ -1,10 +1,10 @@ from pathlib import Path -from pydss.common import SIMULATION_SETTINGS_FILENAME from pydss.pydss_project import PyDssProject base_path = Path(__file__).parent.absolute() + def test_voltage_ride_through(): pydss_project = base_path / "data" / "controllers" @@ -15,6 +15,7 @@ def test_voltage_ride_through(): # simulation_file=SIMULATION_SETTINGS_FILENAME, # ) + def test_simple_motor_stall(): pydss_project = base_path / "data" / "controllers" @@ -23,7 +24,8 @@ def test_simple_motor_stall(): simulation_file="simulation_motor_stall_simple.toml", ) project.run() - + + def test_motor_stall(): pydss_project = base_path / "data" / "controllers" @@ -32,7 +34,8 @@ def test_motor_stall(): simulation_file="simulation_motor_stall.toml", ) project.run() - + + def test_pv_controller(): pydss_project = base_path / "data" / "controllers" @@ -40,4 +43,4 @@ def test_pv_controller(): pydss_project, simulation_file="simulation_pv_controller.toml", ) - project.run() \ No newline at end of file + project.run() diff --git a/tests/test_custom_exports.py b/tests/test_custom_exports.py index 35095ba1..48a6e474 100644 --- a/tests/test_custom_exports.py +++ b/tests/test_custom_exports.py @@ -1,11 +1,9 @@ - import math import os import numpy as np import pandas as pd -from pandas.testing import assert_series_equal from pydss.utils.utils import load_data @@ -14,13 +12,12 @@ from tests.common import ( run_project_with_custom_exports, CUSTOM_EXPORTS_PROJECT_PATH, - cleanup_project, ) from pydss.common import SIMULATION_SETTINGS_FILENAME def test_custom_exports(cleanup_project): - all_node_voltages = _get_all_node_voltages() + _get_all_node_voltages() PyDssProject.run_project( CUSTOM_EXPORTS_PROJECT_PATH, @@ -39,20 +36,20 @@ def test_custom_exports(cleanup_project): df = scenario.get_dataframe("Buses", "DistanceAvg", "t9") assert isinstance(df, pd.DataFrame) assert len(df) == int(96) - #assert len(df) == int(96 / 5) + # assert len(df) == int(96 / 5) for val in df.iloc[9:, 0]: assert round(val, 3) == 0.082 # TODO DT: these values are no longer correct. What should they be? # Filtered value on custom function. - #df = scenario.get_dataframe("Lines", "LoadingPercent", "Line.sl_22") - #assert len(df) == 14 + # df = scenario.get_dataframe("Lines", "LoadingPercent", "Line.sl_22") + # assert len(df) == 14 - #df = scenario.get_dataframe("Lines", "LoadingPercentAvg", "Line.sl_22") + # df = scenario.get_dataframe("Lines", "LoadingPercentAvg", "Line.sl_22") # This was computed from raw data. - #assert len(df) == 9 + # assert len(df) == 9 # TODO incorrect after more decimal points - #assert round(df.iloc[:, 0].values[8], 2) == 22.79 + # assert round(df.iloc[:, 0].values[8], 2) == 22.79 # Subset of names. VoltagesMagAng has specific names, CurrentsMagAng has regex for name in ("Line.pvl_110", "Line.pvl_111", "Line.pvl_112", "Line.pvl_113"): @@ -60,18 +57,18 @@ def test_custom_exports(cleanup_project): assert "VoltagesMagAng" in properties assert "CurrentsMagAng" in properties - properties = scenario.list_element_properties("Lines", element_name="Line.SL_14") + properties = scenario.list_element_properties("Lines", element_name="Line.SL_14") assert "VoltagesMagAng" not in properties assert "CurrentsMagAng" not in properties # TODO: This metric no longer stores voltages in a dataframe. # That functionality could be recovered in PyDSS/metrics.py or we could implement this with # a different export property. - #node_names = scenario.list_element_names("Nodes", "VoltageMetric") - #dfs = scenario.get_filtered_dataframes("Nodes", "VoltageMetric") - #assert len(node_names) == len(dfs) - #assert sorted(node_names) == sorted(dfs.keys()) - #for i, node_name in enumerate(node_names): + # node_names = scenario.list_element_names("Nodes", "VoltageMetric") + # dfs = scenario.get_filtered_dataframes("Nodes", "VoltageMetric") + # assert len(node_names) == len(dfs) + # assert sorted(node_names) == sorted(dfs.keys()) + # for i, node_name in enumerate(node_names): # column = node_name + "__Voltage" # df = dfs[node_name] # # TODO: Slight rounding errors make this intermittent. @@ -89,10 +86,7 @@ def test_custom_exports(cleanup_project): scenario.get_element_property_value("Circuits", "LossesSum", "Circuit.heco19021") sums_json = os.path.join( - CUSTOM_EXPORTS_PROJECT_PATH, - "Exports", - "scenario1", - "element_property_values.json" + CUSTOM_EXPORTS_PROJECT_PATH, "Exports", "scenario1", "element_property_values.json" ) assert os.path.exists(sums_json) data = load_data(sums_json) @@ -133,7 +127,7 @@ def test_export_moving_averages(cleanup_project): run_project_with_custom_exports(path, "scenario1", sim_file, data) results = PyDssResults(path) assert len(results.scenarios) == 1 - scenario = results.scenarios[0] + results.scenarios[0] # This DataFrame will have moving averages. df2 = _get_dataframe(path, "Circuits", "LineLossesAvg", circuit, real_only=True) @@ -172,7 +166,9 @@ def test_pv_powers_by_customer_type(cleanup_project): } run_project_with_custom_exports(path, "scenario1", SIMULATION_SETTINGS_FILENAME, data) total_sum2 = sum(_get_summed_element_total(path, "PVSystems", "PowersSum").values()) - assert math.isclose(total_sum1.real, total_sum2.real) and math.isclose(total_sum1.imag, total_sum2.imag) + assert math.isclose(total_sum1.real, total_sum2.real) and math.isclose( + total_sum1.imag, total_sum2.imag + ) # Collect power for PVSystems aggregated by customer type at every time point. data = { @@ -187,7 +183,7 @@ def test_pv_powers_by_customer_type(cleanup_project): { "name": "res", "elements": list(res_pv_systems), - } + }, ], }, } @@ -211,7 +207,7 @@ def test_pv_powers_by_customer_type(cleanup_project): { "name": "res", "elements": list(res_pv_systems), - } + }, ], }, } @@ -278,4 +274,4 @@ def _get_all_node_voltages(): run_project_with_custom_exports(path, "scenario1", sim_file, data) results = PyDssResults(path) assert len(results.scenarios) == 1 - scenario = results.scenarios[0] + results.scenarios[0] diff --git a/tests/test_dataset_buffer.py b/tests/test_dataset_buffer.py index ee764362..99509bc0 100644 --- a/tests/test_dataset_buffer.py +++ b/tests/test_dataset_buffer.py @@ -1,4 +1,3 @@ - import os import tempfile @@ -11,23 +10,25 @@ def test_dataset_buffer__compute_chunk_count(): one_year_at_5_minutes = 60 / 5 * 24 * 365 - assert DatasetBuffer.compute_chunk_count( - num_columns=4, - max_size=96, - dtype=float - ) == 96 - assert DatasetBuffer.compute_chunk_count( - num_columns=4, - max_size=one_year_at_5_minutes, - dtype=float, - max_chunk_bytes=128 * 1024, - ) == 4096 - assert DatasetBuffer.compute_chunk_count( - num_columns=6, - max_size=one_year_at_5_minutes, - dtype=complex, - max_chunk_bytes=128 * 1024, - ) == 1365 + assert DatasetBuffer.compute_chunk_count(num_columns=4, max_size=96, dtype=float) == 96 + assert ( + DatasetBuffer.compute_chunk_count( + num_columns=4, + max_size=one_year_at_5_minutes, + dtype=float, + max_chunk_bytes=128 * 1024, + ) + == 4096 + ) + assert ( + DatasetBuffer.compute_chunk_count( + num_columns=6, + max_size=one_year_at_5_minutes, + dtype=complex, + max_chunk_bytes=128 * 1024, + ) + == 1365 + ) def test_dataset_buffer__max_num_bytes(): @@ -48,8 +49,9 @@ def test_dataset_buffer__write_value(): with h5py.File(filename, "w") as store: columns = ("1", "2", "3", "4") max_size = 5000 - dataset = DatasetBuffer(store, "data", max_size, float, columns, - max_chunk_bytes=128 * 1024) + dataset = DatasetBuffer( + store, "data", max_size, float, columns, max_chunk_bytes=128 * 1024 + ) assert dataset.chunk_count == 4096 for i in range(max_size): data = np.ones(4) diff --git a/tests/test_dynamic_voltage_support.py b/tests/test_dynamic_voltage_support.py index ff4329fb..f5876a37 100644 --- a/tests/test_dynamic_voltage_support.py +++ b/tests/test_dynamic_voltage_support.py @@ -1,42 +1,48 @@ -import re -import os from pydss.pydss_project import PyDssProject from pydss.pydss_results import PyDssResults -from tests.common import ( - DYNAMIC_VOLTAGE_SUPPORT_PATH, - cleanup_project +from tests.common import DYNAMIC_VOLTAGE_SUPPORT_PATH +from pydss.common import ( + SIMULATION_SETTINGS_FILENAME, ) -from pydss.common import (SIMULATION_SETTINGS_FILENAME, - RUN_SIMULATION_FILENAME, -) -from pydss.pydss_fs_interface import STORE_FILENAME + def test_dynamic_voltage_support(cleanup_project): PyDssProject.run_project( - path=DYNAMIC_VOLTAGE_SUPPORT_PATH, - simulation_file=SIMULATION_SETTINGS_FILENAME + path=DYNAMIC_VOLTAGE_SUPPORT_PATH, simulation_file=SIMULATION_SETTINGS_FILENAME ) - results=PyDssResults(DYNAMIC_VOLTAGE_SUPPORT_PATH) - scenario_DVS_only=results.scenarios[0] #DVS Only - scenario_no_vrt=results.scenarios[1] #DVS and VRT with instantaneous trip - scenario_vrt=results.scenarios[2] #DVS with 1547 VRT + results = PyDssResults(DYNAMIC_VOLTAGE_SUPPORT_PATH) + scenario_dvs_only = results.scenarios[0] # DVS Only + scenario_no_vrt = results.scenarios[1] # DVS and VRT with instantaneous trip + scenario_vrt = results.scenarios[2] # DVS with 1547 VRT + + kvar_df = scenario_dvs_only.get_full_dataframe( + "Generators", "kvar" + ) # var from DVS only scenario + kvar_df_vrt = scenario_vrt.get_full_dataframe( + "Generators", "kvar" + ) # kvar from DVS + VRT scenario + class_df_no_vrt = scenario_no_vrt.get_full_dataframe( + "Generators", "class" + ) # class from DVS + instantaneous trip + scenario_vrt.get_full_dataframe("Generators", "class") # class from DVS + 1547 VRT + + initial_kvar_dvs_only = kvar_df.iloc[0][kvar_df.columns[0]] # initial kvar value DVS only + last_kvar_dvs_only = kvar_df.iloc[-1][kvar_df.columns[0]] # final kvar value DVS only - kvar_df=scenario_DVS_only.get_full_dataframe("Generators", "kvar") #var from DVS only scenario - kvar_df_vrt=scenario_vrt.get_full_dataframe("Generators", "kvar") #kvar from DVS + VRT scenario - class_df_no_vrt=scenario_no_vrt.get_full_dataframe("Generators", "class")# class from DVS + instantaneous trip - class_df_vrt=scenario_vrt.get_full_dataframe("Generators", "class") #class from DVS + 1547 VRT - - initial_kvar_DVS_only=kvar_df.iloc[0][kvar_df.columns[0]] #initial kvar value DVS only - last_kvar_DVS_only=kvar_df.iloc[-1][kvar_df.columns[0]] #final kvar value DVS only + kvar_df_vrt.iloc[0][kvar_df_vrt.columns[0]] # initial kvar value DVS + IEEE 1547 VRT + kvar_df_vrt.iloc[-1][kvar_df_vrt.columns[0]] # final kvar value DVS + IEEE 1547 VRT - initial_kvar_VRT=kvar_df_vrt.iloc[0][kvar_df_vrt.columns[0]]#initial kvar value DVS + IEEE 1547 VRT - last_kvar_VRT=kvar_df_vrt.iloc[-1][kvar_df_vrt.columns[0]]#final kvar value DVS + IEEE 1547 VRT + assert not kvar_df[ + kvar_df[kvar_df.columns[0]] != initial_kvar_dvs_only + ].empty # check that controller changes generator kvar values in DVS only + assert ( + initial_kvar_dvs_only == last_kvar_dvs_only + ) # check that it returns values to their original state after fault has cleared in DVS only - assert not kvar_df[kvar_df[kvar_df.columns[0]]!=initial_kvar_DVS_only].empty #check that controller changes generator kvar values in DVS only - assert initial_kvar_DVS_only == last_kvar_DVS_only #check that it returns values to their original state after fault has cleared in DVS only - - assert 1.0 in class_df_no_vrt[class_df_no_vrt.columns[0]].unique() #check that generator trips offline in DVS + instantaneous trip + assert ( + 1.0 in class_df_no_vrt[class_df_no_vrt.columns[0]].unique() + ) # check that generator trips offline in DVS + instantaneous trip # TODO: Fix after scipy lsim2->lsim migration (different numerical method changes ride-through behavior) # assert 1.0 not in class_df_vrt[class_df_vrt.columns[0]].unique() #check that generator rides through in DVS + 1547 VRT # assert not kvar_df_vrt[kvar_df_vrt[kvar_df_vrt.columns[0]]!=initial_kvar_VRT].empty #check that controller changes generator kvar values in DVS + 1547 VRT - # assert initial_kvar_VRT == last_kvar_VRT #check that it returns values to their original state after fault has cleared in DVS + 1547 VRT \ No newline at end of file + # assert initial_kvar_VRT == last_kvar_VRT #check that it returns values to their original state after fault has cleared in DVS + 1547 VRT diff --git a/tests/test_element_options.py b/tests/test_element_options.py index 0d2e0636..dd8888f6 100644 --- a/tests/test_element_options.py +++ b/tests/test_element_options.py @@ -1,8 +1,7 @@ - from pydss.element_options import ElementOptions -def test_element_options__is_option_valid(): +def test_element_options__list_options(): options = ElementOptions() assert options.is_option_valid("Lines", "Currents", "phase_terminal") assert not options.is_option_valid("Lines", "Currents", "bad") diff --git a/tests/test_export_list_reader.py b/tests/test_export_list_reader.py index 824c930d..b48de0d5 100644 --- a/tests/test_export_list_reader.py +++ b/tests/test_export_list_reader.py @@ -1,6 +1,3 @@ - -import os - import pytest from pydss.common import LimitsFilter, StoreValuesType @@ -14,10 +11,15 @@ def test_export_list_reader(): reader = ExportListReader(EXPORT_LIST_FILE) - assert reader.list_element_classes() == \ - ["Buses", "Circuits", "Lines", "Loads", "PVSystems", "Transformers"] - assert reader.list_element_property_names("Buses") == \ - ["Distance", "puVmagAngle"] + assert reader.list_element_classes() == [ + "Buses", + "Circuits", + "Lines", + "Loads", + "PVSystems", + "Transformers", + ] + assert reader.list_element_property_names("Buses") == ["Distance", "puVmagAngle"] prop = reader.get_element_properties("Buses", "puVmagAngle")[0] assert prop.store_values_type == StoreValuesType.ALL assert prop.should_store_name("bus2") @@ -50,7 +52,7 @@ def test_export_list_reader__name_regexes(): def test_export_list_reader__name_and_name_regexes(): data = {"property": "puVmagAngle", "names": ["bus1"], "name_regexes": [r"busFoo\d+"]} with pytest.raises(InvalidConfiguration): - export_prop = ExportListProperty("Buses", data) + ExportListProperty("Buses", data) def test_export_list_reader__limits(): @@ -69,7 +71,9 @@ def test_export_list_reader__limits(): assert export_prop.limits.max == 1.0 assert not export_prop.should_store_value(-2.0) assert not export_prop.should_store_value(2.0) - assert export_prop.should_store_value(-0.5,) + assert export_prop.should_store_value( + -0.5, + ) assert export_prop.should_store_value(0.5) with pytest.raises(InvalidConfiguration): @@ -81,10 +85,15 @@ def test_export_list_reader__limits(): def test_export_list_reader__legacy_file(): reader = ExportListReader(LEGACY_FILE) - assert reader.list_element_classes() == \ - ["Buses", "Circuits", "Lines", "Loads", "Storages", "Transformers"] - assert reader.list_element_property_names("Buses") == \ - ["Distance", "puVmagAngle"] + assert reader.list_element_classes() == [ + "Buses", + "Circuits", + "Lines", + "Loads", + "Storages", + "Transformers", + ] + assert reader.list_element_property_names("Buses") == ["Distance", "puVmagAngle"] prop = reader.get_element_properties("Buses", "puVmagAngle")[0] assert prop.store_values_type == StoreValuesType.ALL assert prop.should_store_name("bus2") diff --git a/tests/test_independent_controls.py b/tests/test_independent_controls.py deleted file mode 100644 index baa4bcba..00000000 --- a/tests/test_independent_controls.py +++ /dev/null @@ -1,7 +0,0 @@ -import shlex -import subprocess - - -def test_independent_controls(): - cmd = "python examples/independent_control_demo.py" - subprocess.check_call(shlex.split(cmd)) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 3eecc6f9..80fddfdd 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -1,5 +1,3 @@ - - from pathlib import Path import tempfile import shutil @@ -15,10 +13,7 @@ from pydss.export_list_reader import ExportListProperty from pydss.metrics import MultiValueTypeMetricBase from pydss.dataset_buffer import DatasetBuffer -from pydss.simulation_input_models import ( - create_simulation_settings, - load_simulation_settings -) +from pydss.simulation_input_models import create_simulation_settings, load_simulation_settings from pydss.common import LimitsFilter from tests.common import FakeElement @@ -26,7 +21,10 @@ STORE_FILENAME = os.path.join(tempfile.gettempdir(), "store.h5") FLOATS = (1.0, 2.0, 3.0, 4.0, 5.0) COMPLEX_NUMS = ( - complex(1, 2), complex(3, 4), complex(5, 6), complex(7, 8), + complex(1, 2), + complex(3, 4), + complex(5, 6), + complex(7, 8), complex(9, 10), ) LIST_COMPLEX_NUMS = ( @@ -59,7 +57,6 @@ def simulation_settings(): class FakeMetric(MultiValueTypeMetricBase): - def __init__(self, prop, dss_objs, options, values): super().__init__(prop, dss_objs, options) self._elem_index = 0 @@ -72,7 +69,9 @@ def _get_value(self, obj, timestamp): val = ValueByList(obj.FullName, prop.name, self._values[self._val_index], ["", ""]) else: val = ValueByNumber(obj.FullName, prop.name, self._values[self._val_index]) - logger.debug("elem_index=%s val_index=%s, val=%s", self._elem_index, self._val_index, val.value) + logger.debug( + "elem_index=%s val_index=%s, val=%s", self._elem_index, self._val_index, val.value + ) self._elem_index += 1 if self._elem_index == len(self._dss_objs): self._elem_index = 0 @@ -106,7 +105,7 @@ def test_metrics_store_all(simulation_settings): for column in df.columns: for val1, val2 in zip(df[column].values, values): assert val1 == val2 - assert metric.max_num_bytes() == len(values) * len(OBJS) * 8 + assert metric.max_num_bytes() == len(values) * len(OBJS) * 8 def test_metrics_store_all_complex_abs(simulation_settings): @@ -273,7 +272,7 @@ def test_metrics_store_moving_average_with_limits(simulation_settings): "limits_filter": LimitsFilter.OUTSIDE, } values = [float(x) for x in range(1, 101)] - expected_values = [x for x in values if x < limits[0] or x > limits[1]] + [x for x in values if x < limits[0] or x > limits[1]] base_df = pd.DataFrame(values) base_series = base_df.iloc[:, 0] base_rm = base_series.rolling(window_size).mean() diff --git a/tests/test_opendss_export_overloads.py b/tests/test_opendss_export_overloads.py index 1d440ff1..724eef7d 100644 --- a/tests/test_opendss_export_overloads.py +++ b/tests/test_opendss_export_overloads.py @@ -11,10 +11,7 @@ from pydss.export_list_reader import ExportListProperty from pydss.dataset_buffer import DatasetBuffer from pydss.metrics import ExportLoadingsMetric -from pydss.simulation_input_models import ( - create_simulation_settings, - load_simulation_settings -) +from pydss.simulation_input_models import create_simulation_settings, load_simulation_settings from tests.common import FakeElement OBJS = [ @@ -48,6 +45,8 @@ def simulation_settings(): overloads_file_id = 1 + + def mock_run_command(): filename = f"{EXPORTED_LOADINGS_BASE_FILENAME}{overloads_file_id}.CSV" return filename diff --git a/tests/test_opendss_export_powers.py b/tests/test_opendss_export_powers.py index 27429d6f..79c20632 100644 --- a/tests/test_opendss_export_powers.py +++ b/tests/test_opendss_export_powers.py @@ -11,10 +11,7 @@ from pydss.export_list_reader import ExportListProperty from pydss.dataset_buffer import DatasetBuffer from pydss.metrics import ExportPowersMetric -from pydss.simulation_input_models import ( - create_simulation_settings, - load_simulation_settings -) +from pydss.simulation_input_models import create_simulation_settings, load_simulation_settings from tests.common import FakeElement OBJS = [ @@ -52,6 +49,8 @@ def simulation_settings(): powers_file_id = 1 + + def mock_run_command(): filename = f"{EXPORTED_POWERS_BASE_FILENAME}{powers_file_id}.CSV" return filename diff --git a/tests/test_openmdao_components.py b/tests/test_openmdao_components.py new file mode 100644 index 00000000..d1095838 --- /dev/null +++ b/tests/test_openmdao_components.py @@ -0,0 +1,241 @@ +import numpy as np +import openmdao.api as om +import pytest + +from pydss.openmdao_components import ( + CircuitAdapter, + CircuitExplicitComponent, + ControllerExplicitComponent, + OpenDSSCircuitAdapter, + VariableSpec, +) +from pydss.openmdao_model import build_openmdao_problem + + +class FakeCircuit(CircuitAdapter): + def __init__(self): + self.command = 0.0 + self.apply_count = 0 + self.solve_count = 0 + self.commit_count = 0 + + def apply_commands(self, commands): + self.apply_count += 1 + self.command = float(commands["command"][0]) + + def solve(self): + self.solve_count += 1 + return True + + def read_measurements(self, names): + assert names == ("voltage",) + return {"voltage": 1.0 + self.command} + + def commit_step(self): + self.commit_count += 1 + + +class VoltageController(ControllerExplicitComponent): + def setup(self): + super().setup() + self.add_input("target", val=1.0) + + def compute_commands(self, inputs): + return {"command": inputs["target"] - inputs["voltage"]} + + +def test_openmdao_requires_positive_iteration_limit(): + circuit = CircuitExplicitComponent( + adapter=FakeCircuit(), + measurement_specs=(VariableSpec("voltage"),), + ) + + with pytest.raises(ValueError, match="at least 1"): + build_openmdao_problem(circuit, max_iterations=0) + + +def test_openmdao_controller_circuit_cycle_and_commit(): + adapter = FakeCircuit() + circuit = CircuitExplicitComponent( + adapter=adapter, + command_specs=(VariableSpec("command"),), + measurement_specs=(VariableSpec("voltage"),), + ) + controller = VoltageController( + input_specs=(VariableSpec("voltage"),), + output_specs=(VariableSpec("command"),), + ) + problem = build_openmdao_problem( + circuit, + [("controller", controller)], + [("circuit.voltage", "controller.voltage"), ("controller.command", "circuit.command")], + max_iterations=20, + tolerance=1e-10, + ) + + problem.run_model() + + np.testing.assert_allclose(problem.get_val("circuit.voltage"), 1.0, atol=1e-7) + assert adapter.apply_count == adapter.solve_count + assert adapter.commit_count == 0 + + problem.model.commit_step() + + assert adapter.commit_count == 1 + + +def test_openmdao_controller_cycle_reports_nonconvergence(): + circuit = CircuitExplicitComponent( + adapter=FakeCircuit(), + command_specs=(VariableSpec("command"),), + measurement_specs=(VariableSpec("voltage"),), + ) + controller = VoltageController( + input_specs=(VariableSpec("voltage"),), + output_specs=(VariableSpec("command"),), + ) + problem = build_openmdao_problem( + circuit, + [("controller", controller)], + [("circuit.voltage", "controller.voltage"), ("controller.command", "circuit.command")], + max_iterations=1, + tolerance=1e-10, + ) + + with pytest.raises(om.AnalysisError, match="failed to converge"): + problem.run_model() + + +class FakeDssElement: + Bus = ("source",) + + def __init__(self): + self.parameters = {} + + def set_parameter(self, name, value): + self.parameters[name] = value + + def __getattr__(self, name): + if name == "SetParameter": + return self.set_parameter + raise AttributeError(name) + + def get_value(self, name): + assert name == "Powers" + return [2.0, 3.0] + + +class FakeDss: + class Circuit: + active_bus = None + + @classmethod + def set_active_bus(cls, name): + cls.active_bus = name + + SetActiveBus = set_active_bus + + def __getattr__(self, name): + if name == "SetActiveBus": + return self.set_active_bus + raise AttributeError(name) + + class Bus: + @staticmethod + def pu_vmag_angle(): + return [1.02, 0.0] + + def __getattr__(self, name): + if name == "puVmagAngle": + return self.pu_vmag_angle + raise AttributeError(name) + + class Solution: + @staticmethod + def converged(): + return True + + Converged = converged + + def __getattr__(self, name): + if name == "Converged": + return self.converged + raise AttributeError(name) + + +class FakeSolver: + def __init__(self): + self.solve_count = 0 + self.time_index = 0 + + def resolve(self): + self.solve_count += 1 + + def increment_step(self): + self.time_index += 1 + + IncStep = increment_step + + def __getattr__(self, name): + external_names = {"reSolve": self.resolve, "IncStep": self.increment_step} + if name in external_names: + return external_names[name] + raise AttributeError(name) + + +setattr(FakeDss.Bus, "puVmagAngle", FakeDss.Bus.pu_vmag_angle) +setattr(FakeSolver, "reSolve", FakeSolver.resolve) + + +def test_opendss_circuit_adapter_maps_generic_io(): + element = FakeDssElement() + adapter = OpenDSSCircuitAdapter( + FakeDss(), + FakeSolver(), + {"PVSystem.pv1": element}, + { + "command_active_power": ("PVSystem.pv1", "kW"), + "command_reactive_power": ("PVSystem.pv1", "kvar"), + "command_enabled": ("PVSystem.pv1", "enabled"), + }, + { + "measurement_voltage_pu": ("PVSystem.pv1", "measurement_voltage_pu"), + "measurement_active_power": ("PVSystem.pv1", "measurement_active_power"), + "measurement_reactive_power": ("PVSystem.pv1", "measurement_reactive_power"), + "time_seconds": ("PVSystem.pv1", "time_seconds"), + }, + ) + + adapter.set_time(60.0) + adapter.apply_commands( + { + "command_active_power": np.asarray(4.0), + "command_reactive_power": np.asarray(-1.0), + "command_enabled": np.asarray(1.0), + } + ) + + assert element.parameters == {"kW": 4.0, "kvar": -1.0, "enabled": "Yes"} + assert adapter.solve() + assert adapter.read_measurements(tuple(adapter._measurement_map)) == { + "measurement_voltage_pu": 1.02, + "measurement_active_power": -2.0, + "measurement_reactive_power": -3.0, + "time_seconds": 60.0, + } + + +def test_open_dss_resolve_does_not_advance_time_index(): + solver = FakeSolver() + element = FakeDssElement() + adapter = OpenDSSCircuitAdapter(FakeDss(), solver, {"PVSystem.pv1": element}, {}, {}) + + adapter.solve() + adapter.solve() + + assert solver.solve_count == 2 + assert solver.time_index == 0 + + solver.IncStep() + + assert solver.time_index == 1 diff --git a/tests/test_openmdao_controllers.py b/tests/test_openmdao_controllers.py new file mode 100644 index 00000000..d2c02317 --- /dev/null +++ b/tests/test_openmdao_controllers.py @@ -0,0 +1,286 @@ +import openmdao.api as om +import pytest +import numpy as np + +from pydss.py_controllers.controllers.dynamic_voltage_support import DynamicVoltageSupport +from pydss.py_controllers.controllers.motor_stall_simple import MotorStallSimple +from pydss.py_controllers.controllers.pv_voltage_ride_thru import PvVoltageRideThru +from pydss.py_controllers.controllers.fault_controller import FaultController +from pydss.py_controllers.controllers.gen_controller import GenController +from pydss.py_controllers.controllers.motor_stall import MotorStall +from pydss.py_controllers.controllers.motor_stall_backup import MotorStallBackup +from pydss.py_controllers.controllers.pv_controller import PvController +from pydss.py_controllers.controllers.pv_dynamic import PvDynamic +from pydss.py_controllers.controllers.pv_frequency_ride_thru import PvFrequencyRideThru +from pydss.py_controllers.controllers.socket_controller import SocketController +from pydss.py_controllers.controllers.storage_controller import StorageController +from pydss.py_controllers.controllers.thermostatic_load import ThermostaticLoad +from pydss.py_controllers.controllers.xfmr_controller import XfmrController +from pydss.py_controllers.models import PvControllerModel +from pydss.py_controllers.py_controller import ControllerTypes, Create + + +def test_all_registered_controllers_are_openmdao_components(): + group = om.Group() + controllers = [] + for index, controller_type in enumerate(ControllerTypes): + controller = Create(controller_type, {}, element_name=f"element_{index}") + group.add_subsystem(f"controller_{index}", controller) + controllers.append(controller) + + problem = om.Problem(model=group, reports=False) + problem.setup() + problem.run_model() + assert set(problem.model.controller_0._var_rel_names["input"]) >= { + "measurement_voltage_pu", + "measurement_active_power", + "measurement_reactive_power", + "time_seconds", + } + assert set(problem.model.controller_0._var_rel_names["output"]) >= { + "command_active_power", + "command_reactive_power", + "command_enabled", + "diagnostic_residual", + } + for controller in controllers: + controller.commit_step() + assert controller.step_count == 1 + + +def test_legacy_controller_settings_are_rejected(): + with pytest.raises(ValueError, match="Legacy controller settings"): + PvControllerModel(Control1="VVar") + + +def test_legacy_controller_disable_setting_is_rejected(): + from pydss.simulation_input_models import ProjectModel + + with pytest.raises(ValueError, match="Extra inputs are not permitted"): + ProjectModel(**{"Disable pydss controllers": False}) + + +def _controller_runner(controller): + group = om.Group() + group.add_subsystem("controller", controller) + problem = om.Problem(model=group, reports=False) + problem.setup() + + def run(voltage, active, reactive, time): + problem.set_val("controller.measurement_voltage_pu", voltage) + problem.set_val("controller.measurement_active_power", active) + problem.set_val("controller.measurement_reactive_power", reactive) + problem.set_val("controller.time_seconds", time) + problem.run_model() + values = { + name: float(np.asarray(problem.get_val(f"controller.{name}")).reshape(-1)[0]) + for name in ("command_active_power", "command_reactive_power", "command_enabled") + } + controller.commit_step() + return values + + return run + + +def test_pv_voltage_ride_through_trips_and_reconnects(): + controller = PvVoltageRideThru( + settings={ + "uv_2_pu": 0.5, + "uv_2_ct_sec": 1.0, + "reconnect_deadtime_sec": 2.0, + } + ) + run = _controller_runner(controller) + assert run(0.4, 5.0, 1.0, 0.0)["command_enabled"] == 1.0 + assert run(0.4, 5.0, 1.0, 1.1)["command_enabled"] == 0.0 + assert run(1.0, 5.0, 1.0, 2.0)["command_enabled"] == 0.0 + assert run(1.0, 5.0, 1.0, 4.0)["command_enabled"] == 1.0 + + +def test_motor_stall_simple_multiplies_then_disconnects_and_reconnects(): + controller = MotorStallSimple( + settings={ + "v_stall": 0.6, + "p_fault": 3.0, + "q_fault": 4.0, + "t_protection": 1.0, + "t_reconnect": 6.0, + } + ) + run = _controller_runner(controller) + stalled = run(0.5, 2.0, 1.0, 0.0) + assert stalled["command_active_power"] == pytest.approx(6.0) + assert stalled["command_reactive_power"] == pytest.approx(4.0) + assert run(0.5, 2.0, 1.0, 1.1)["command_enabled"] == 0.0 + assert run(1.0, 2.0, 1.0, 8.0)["command_enabled"] == 1.0 + + +def test_dynamic_voltage_support_injects_or_absorbs_reactive_power(): + settings = { + "Trv": 0.001, + "Tinv": 0.001, + "dbd1": -0.1, + "dbd2": 0.1, + "Kqv": 10.0, + "kvar_max": 1.0, + "kvar_min": -1.0, + "kva": 10.0, + } + controller = DynamicVoltageSupport(settings=settings) + run = _controller_runner(controller) + assert run(0.8, 2.0, 0.0, 0.0)["command_reactive_power"] > 0.0 + assert run(1.2, 2.0, 0.0, 1.0)["command_reactive_power"] < 0.0 + + +def _run_declared_inputs(controller, values): + group = om.Group() + group.add_subsystem("controller", controller) + problem = om.Problem(model=group, reports=False) + problem.setup() + input_names = {name.rsplit(".", 1)[-1] for name in controller._var_rel_names["input"]} + for name, value in values.items(): + if name in input_names: + problem.set_val(f"controller.{name}", value) + problem.run_model() + result = { + name: float(np.asarray(problem.get_val(f"controller.{name}")).reshape(-1)[0]) + for name in controller._var_rel_names["output"] + } + return result + + +def test_migrated_time_and_voltage_families_use_synthetic_commands_only(): + fault = FaultController(settings={"start_time_seconds": 2.0, "duration_seconds": 1.0}) + assert _run_declared_inputs(fault, {"time_seconds": 2.5})["command_enabled"] == 1.0 + fault.commit_step() + assert fault.step_count == 1 + + generator = GenController( + settings={"u_min": 0.9, "u_db_min": 0.95, "u_db_max": 1.05, "u_max": 1.1} + ) + assert ( + _run_declared_inputs( + generator, + { + "measurement_voltage_pu": 0.9, + "measurement_active_power": 4.0, + "measurement_reactive_power": 0.0, + }, + )["command_reactive_power"] + > 0.0 + ) + + pv = PvController(settings={"uMin": 0.94, "uDbMin": 0.97, "uDbMax": 1.03, "uMax": 1.06}) + assert ( + _run_declared_inputs( + pv, + { + "measurement_voltage_pu": 1.1, + "measurement_active_power": 2.0, + "measurement_reactive_power": 0.0, + }, + )["command_reactive_power"] + < 0.0 + ) + + dynamic = PvDynamic(settings={"response_fraction": 0.5}) + first = dynamic.compute_commands( + { + "measurement_active_power": 4.0, + "measurement_reactive_power": 2.0, + "measurement_voltage_pu": 1.0, + "time_seconds": 0.0, + } + ) + dynamic.commit_step() + second = dynamic.compute_commands( + { + "measurement_active_power": 0.0, + "measurement_reactive_power": 0.0, + "measurement_voltage_pu": 1.0, + "time_seconds": 1.0, + } + ) + assert first["command_active_power"] == 4.0 + assert second["command_active_power"] == 2.0 + + +def test_migrated_stall_and_frequency_controllers_commit_state(): + for controller in ( + MotorStall(settings={"v_stall": 0.6, "t_protection": 1.0}), + MotorStallBackup(settings={"v_stall": 0.6}), + ): + result = _run_declared_inputs( + controller, + { + "measurement_voltage_pu": 0.5, + "measurement_active_power": 2.0, + "measurement_reactive_power": 1.0, + "time_seconds": 0.0, + }, + ) + expected_enabled = 0.0 if isinstance(controller, MotorStallBackup) else 1.0 + assert result["command_enabled"] == expected_enabled + controller.commit_step() + assert controller.step_count == 1 + + ride_through = PvFrequencyRideThru( + settings={"f_lower": 59.0, "f_upper": 61.0, "trip_delay_sec": 1.0} + ) + first = ride_through.compute_commands( + { + "measurement_frequency_hz": 58.0, + "measurement_active_power": 2.0, + "measurement_reactive_power": 1.0, + "time_seconds": 0.0, + } + ) + assert first["command_enabled"] == 1.0 + ride_through.commit_step() + second = ride_through.compute_commands( + { + "measurement_frequency_hz": 58.0, + "measurement_active_power": 2.0, + "measurement_reactive_power": 1.0, + "time_seconds": 1.1, + } + ) + assert second["command_enabled"] == 0.0 + + +def test_migrated_boundary_storage_thermal_and_transformer_controllers(): + socket_controller = SocketController(settings={"active_power": 3.0, "reactive_power": -1.0}) + socket_result = _run_declared_inputs( + socket_controller, {"measurement_active_power": 0.0, "measurement_reactive_power": 0.0} + ) + assert socket_result["command_active_power"] == 3.0 + + storage = StorageController(settings={"control": "PS", "PS_ub": 5.0, "DampCoef": 1.0}) + assert ( + _run_declared_inputs( + storage, {"measurement_active_power": 8.0, "measurement_reactive_power": 0.0} + )["command_active_power"] + == 5.0 + ) + + thermostat = ThermostaticLoad( + settings={ + "Tmin": 18.0, + "Tmax": 20.0, + "kw": 2.0, + "initial_temperature": 19.0, + "initial_on": True, + } + ) + assert ( + _run_declared_inputs(thermostat, {"time_seconds": 0.0, "measurement_active_power": 2.0})[ + "command_active_power" + ] + == 2.0 + ) + + transformer = XfmrController(settings={"RPF locking": True}) + assert ( + _run_declared_inputs(transformer, {"measurement_active_power": -1.0})["command_enabled"] + == 0.0 + ) diff --git a/tests/test_pv_reports.py b/tests/test_pv_reports.py index 5c5a58bd..2fcaa093 100644 --- a/tests/test_pv_reports.py +++ b/tests/test_pv_reports.py @@ -1,12 +1,10 @@ - - from pathlib import Path import math import os import numpy as np -from tests.common import PV_REPORTS_PROJECT_PATH, PV_REPORTS_PROJECT_STORE_ALL_PATH, cleanup_project +from tests.common import PV_REPORTS_PROJECT_PATH, PV_REPORTS_PROJECT_STORE_ALL_PATH from pydss.reports.feeder_losses import SimulationFeederLossesMetricsModel, compare_feeder_losses from pydss.node_voltage_metrics import SimulationVoltageMetricsModel, compare_voltage_metrics from pydss.thermal_metrics import SimulationThermalMetricsModel, compare_thermal_metrics @@ -21,12 +19,14 @@ import warnings import sys + def warn_with_traceback(message, category, filename, lineno, file=None, line=None): - log = file if hasattr(file,'write') else sys.stderr + log = file if hasattr(file, "write") else sys.stderr traceback.print_stack(file=log) log.write(warnings.formatwarning(message, category, filename, lineno, line)) + warnings.showwarning = warn_with_traceback BASE_FILENAME = os.path.join(PV_REPORTS_PROJECT_PATH, SIMULATION_SETTINGS_FILENAME) @@ -53,7 +53,7 @@ def test_pv_reports_per_element_per_time_point(cleanup_project): **load_data(Path(PV_REPORTS_PROJECT_STORE_ALL_PATH) / "Reports" / "voltage_metrics.json") ) baseline_feeder_losses = SimulationFeederLossesMetricsModel( - **load_data( Path(PV_REPORTS_PROJECT_STORE_ALL_PATH) / "Reports" / "feeder_losses.json") + **load_data(Path(PV_REPORTS_PROJECT_STORE_ALL_PATH) / "Reports" / "feeder_losses.json") ) granularities = [x for x in ReportGranularity] @@ -99,16 +99,16 @@ def verify_pv_reports(granularity): ReportGranularity.ALL_ELEMENTS_PER_TIME_POINT, ): clipping_name = os.path.join(PV_REPORTS_PROJECT_PATH, "Reports", "pv_clipping.h5") - clipping = read_dataframe(clipping_name) + read_dataframe(clipping_name) curtailment_name = os.path.join(PV_REPORTS_PROJECT_PATH, "Reports", "pv_curtailment.h5") - curtailment = read_dataframe(curtailment_name) + read_dataframe(curtailment_name) else: clipping_name = os.path.join(PV_REPORTS_PROJECT_PATH, "Reports", "pv_clipping.json") - clipping = load_data(clipping_name) + load_data(clipping_name) curtailment_name = os.path.join(PV_REPORTS_PROJECT_PATH, "Reports", "pv_curtailment.json") - curtailment = load_data(curtailment_name) + load_data(curtailment_name) - total_cm_p1ulv53232_1_2_pv = 2237.4654 + total_cm_p1ulv53232_1_2_pv = 2233.5718 total_cm_p1ulv57596_1_2_3_pv = 650.3959 overall_total_cm = total_cm_p1ulv53232_1_2_pv + total_cm_p1ulv57596_1_2_3_pv total_pf1_p1ulv53232_1_2_pv = 2389.4002 @@ -116,21 +116,61 @@ def verify_pv_reports(granularity): overall_total_pf1 = total_pf1_p1ulv53232_1_2_pv + total_pf1_p1ulv57596_1_2_3_pv if granularity == ReportGranularity.PER_ELEMENT_PER_TIME_POINT: df = s_cm.get_full_dataframe("PVSystems", "Powers") - assert math.isclose(df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].sum(), total_cm_p1ulv53232_1_2_pv, rel_tol=1e-04) - assert math.isclose(df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].sum(), total_cm_p1ulv57596_1_2_3_pv, rel_tol=1e-04) + assert math.isclose( + df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].sum(), + total_cm_p1ulv53232_1_2_pv, + rel_tol=1e-04, + ) + assert math.isclose( + df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].sum(), + total_cm_p1ulv57596_1_2_3_pv, + rel_tol=1e-04, + ) df = s_pf1.get_full_dataframe("PVSystems", "Powers") - assert math.isclose(df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].sum(), total_pf1_p1ulv53232_1_2_pv, rel_tol=1e-04) - assert math.isclose(df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].sum(), total_pf1_p1ulv57596_1_2_3_pv, rel_tol=1e-04) + assert math.isclose( + df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].sum(), + total_pf1_p1ulv53232_1_2_pv, + rel_tol=1e-04, + ) + assert math.isclose( + df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].sum(), + total_pf1_p1ulv57596_1_2_3_pv, + rel_tol=1e-04, + ) elif granularity == ReportGranularity.PER_ELEMENT_TOTAL: df = s_cm.get_full_dataframe("PVSystems", "PowersSum") - assert math.isclose(df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].values[0], total_cm_p1ulv53232_1_2_pv, rel_tol=1e-04) - assert math.isclose(df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].values[0], total_cm_p1ulv57596_1_2_3_pv, rel_tol=1e-04) + assert math.isclose( + df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].values[0], + total_cm_p1ulv53232_1_2_pv, + rel_tol=1e-04, + ) + assert math.isclose( + df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].values[0], + total_cm_p1ulv57596_1_2_3_pv, + rel_tol=1e-04, + ) df = s_pf1.get_full_dataframe("PVSystems", "PowersSum") - assert math.isclose(df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].values[0], total_pf1_p1ulv53232_1_2_pv, rel_tol=1e-04) - assert math.isclose(df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].values[0], total_pf1_p1ulv57596_1_2_3_pv, rel_tol=1e-04) + assert math.isclose( + df["PVSystem.small_p1ulv53232_1_2_pv__Powers"].values[0], + total_pf1_p1ulv53232_1_2_pv, + rel_tol=1e-04, + ) + assert math.isclose( + df["PVSystem.small_p1ulv57596_1_2_3_pv__Powers"].values[0], + total_pf1_p1ulv57596_1_2_3_pv, + rel_tol=1e-04, + ) elif granularity == ReportGranularity.ALL_ELEMENTS_TOTAL: - assert math.isclose(s_cm.get_summed_element_total("PVSystems", "PowersSum")['Total__Powers'], overall_total_cm, rel_tol=1e-04) - assert math.isclose(s_pf1.get_summed_element_total("PVSystems", "PowersSum")['Total__Powers'], overall_total_pf1, rel_tol=1e-04) + assert math.isclose( + s_cm.get_summed_element_total("PVSystems", "PowersSum")["Total__Powers"], + overall_total_cm, + rel_tol=1e-04, + ) + assert math.isclose( + s_pf1.get_summed_element_total("PVSystems", "PowersSum")["Total__Powers"], + overall_total_pf1, + rel_tol=1e-04, + ) elif granularity == ReportGranularity.ALL_ELEMENTS_PER_TIME_POINT: df = s_cm.get_summed_element_dataframe("PVSystems", "Powers") assert math.isclose(df["Total__Powers"].sum(), overall_total_cm, rel_tol=1e-04) diff --git a/tests/test_pydss_examples.py b/tests/test_pydss_examples.py index 0c1fb125..e150811e 100644 --- a/tests/test_pydss_examples.py +++ b/tests/test_pydss_examples.py @@ -1,6 +1,4 @@ - from distutils.dir_util import copy_tree -import socket import subprocess import tempfile import time @@ -14,53 +12,59 @@ IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" EXAMPLES_path = "examples" + @pytest.fixture def pydss_project(): - pydss_project = tempfile.mkdtemp() + pydss_project = tempfile.mkdtemp() print(pydss_project) yield pydss_project - #shutil.rmtree(pydss_project) + # shutil.rmtree(pydss_project) + base_projects_path = None + + def copy_examples_to_temp_folder(pydss_project, example_name): if "/" in example_name: example_name = example_name.split("/")[0] - #assert os.path.exists(EXAMPLES_path) - proc = None + # assert os.path.exists(EXAMPLES_path) base_projects_path = os.path.join(pydss_project, example_name) os.makedirs(base_projects_path, exist_ok=True) assert os.path.exists(base_projects_path) copy_tree(EXAMPLES_path, base_projects_path) return base_projects_path + def test_helics_interface_example(pydss_project): example_name = "external_interfaces/pydss_project" scenarios = [ { - 'TOML': 'helics.toml', - 'file': r"external_interfaces/Helics_example/run_dummy_federate.py", + "TOML": "helics.toml", + "file": r"external_interfaces/Helics_example/run_dummy_federate.py", }, ] run_example(pydss_project, example_name, scenarios) return + def test_helics_interface_iterative_example(pydss_project): example_name = "external_interfaces/pydss_project" scenarios = [ { - 'TOML': 'helics_itr.toml', - 'file': r"external_interfaces/Helics_example/run_dummy_federate.py", + "TOML": "helics_itr.toml", + "file": r"external_interfaces/Helics_example/run_dummy_federate.py", }, ] run_example(pydss_project, example_name, scenarios) return + def test_socket_interface_example(pydss_project): example_name = "external_interfaces/pydss_project" scenarios = [ { - 'TOML': 'socket.toml', - 'file': r"external_interfaces/Socket_example/run_socket_controller.py", + "TOML": "socket.toml", + "file": r"external_interfaces/Socket_example/run_socket_controller.py", }, ] run_example(pydss_project, example_name, scenarios) @@ -71,19 +75,20 @@ def test_monte_carlo_example(pydss_project): example_name = "monte_carlo" scenarios = [ { - 'TOML': 'simulation.toml', - 'file': None, + "TOML": "simulation.toml", + "file": None, }, ] run_example(pydss_project, example_name, scenarios) return + def test_custom_contols_example(pydss_project): example_name = "custom_contols" scenarios = [ { - 'TOML' : 'simulation.toml', - 'file' : None, + "TOML": "simulation.toml", + "file": None, }, ] run_example(pydss_project, example_name, scenarios) @@ -94,8 +99,8 @@ def test_harmonics_spanshot_example(pydss_project): example_name = "harmonics" scenarios = [ { - 'TOML': 'snapshot.toml', - 'file': None, + "TOML": "snapshot.toml", + "file": None, }, ] run_example(pydss_project, example_name, scenarios) @@ -106,8 +111,8 @@ def test_harmonics_timeseries_example(pydss_project): example_name = "harmonics" scenarios = [ { - 'TOML': 'simulation.toml', - 'file': None, + "TOML": "simulation.toml", + "file": None, }, ] run_example(pydss_project, example_name, scenarios) @@ -118,26 +123,27 @@ def test_dynamics_example(pydss_project): example_name = "dynamics" scenarios = [ { - 'TOML': 'simulation.toml', - 'file': None, + "TOML": "simulation.toml", + "file": None, }, ] run_example(pydss_project, example_name, scenarios) return + def run_example(pydss_project, example_name, scenarios): proc = None assert isinstance(example_name, str) assert isinstance(scenarios, list) base_projects_path = copy_examples_to_temp_folder(pydss_project, example_name) - for S in scenarios: - assert isinstance(S, dict) - sim_file = S["TOML"] - sup_file = S["file"] + for scenario in scenarios: + assert isinstance(scenario, dict) + sim_file = scenario["TOML"] + sup_file = scenario["file"] - logger.info('Running scenario %s for example %s', example_name, sim_file) - if sup_file != None: + logger.info("Running scenario %s for example %s", example_name, sim_file) + if sup_file is not None: sup_file_path = os.path.join(base_projects_path, sup_file) assert os.path.exists(sup_file_path), f"{sup_file_path} does not exist" dir_path = os.path.dirname(sup_file_path) @@ -153,12 +159,16 @@ def run_example(pydss_project, example_name, scenarios): if sim_file: project_path = os.path.join(base_projects_path, example_name) assert os.path.exists(base_projects_path) - PyDssProject.run_project(project_path, options=None, tar_project=False, zip_project=False, - simulation_file=sim_file) + PyDssProject.run_project( + project_path, + options=None, + tar_project=False, + zip_project=False, + simulation_file=sim_file, + ) finally: - if proc != None: + if proc is not None: print("killing process") proc.terminate() - #os.kill(proc.pid, signal.SIGTERM) + # os.kill(proc.pid, signal.SIGTERM) return - diff --git a/tests/test_pydss_project.py b/tests/test_pydss_project.py index fbff6378..cc853d0d 100644 --- a/tests/test_pydss_project.py +++ b/tests/test_pydss_project.py @@ -1,4 +1,3 @@ - import datetime import os import re @@ -10,10 +9,10 @@ from pydss.common import PROJECT_TAR, PROJECT_ZIP from pydss.exceptions import InvalidParameter -from pydss.pydss_fs_interface import PROJECT_DIRECTORIES, SCENARIOS, STORE_FILENAME +from pydss.pydss_fs_interface import SCENARIOS from pydss.pydss_project import PyDssProject, PyDssScenario, DATA_FORMAT_VERSION from pydss.pydss_results import PyDssResults, PyDssScenarioResults -from tests.common import RUN_PROJECT_PATH, SCENARIO_NAME, cleanup_project +from tests.common import RUN_PROJECT_PATH from pydss.common import SIMULATION_SETTINGS_FILENAME @@ -104,13 +103,13 @@ def run_test_project_by_property(tar_project, zip_project): zip_project=zip_project, simulation_file=SIMULATION_SETTINGS_FILENAME, ) - + results = PyDssResults(RUN_PROJECT_PATH) assert len(results.scenarios) == 1 assert results._hdf_store.attrs["version"] == DATA_FORMAT_VERSION scenario = results.scenarios[0] print(scenario) - + assert isinstance(scenario, PyDssScenarioResults) elem_classes = scenario.list_element_classes() expected_elem_classes = list(EXPECTED_ELEM_CLASSES_PROPERTIES.keys()) @@ -140,12 +139,16 @@ def run_test_project_by_property(tar_project, zip_project): step = datetime.timedelta(seconds=project.simulation_config.project.step_resolution_sec) assert df.index[1] - df.index[0] == step - df = scenario.get_dataframe("Lines", "CurrentsMagAng", "Line.sw0", phase_terminal="A1", mag_ang="mag") + df = scenario.get_dataframe( + "Lines", "CurrentsMagAng", "Line.sw0", phase_terminal="A1", mag_ang="mag" + ) assert isinstance(df, pd.DataFrame) assert len(df) == 96 assert len(df.columns) == 1 - df = scenario.get_dataframe("Lines", "CurrentsMagAng", "Line.sw0", phase_terminal=None, mag_ang="ang") + df = scenario.get_dataframe( + "Lines", "CurrentsMagAng", "Line.sw0", phase_terminal=None, mag_ang="ang" + ) assert isinstance(df, pd.DataFrame) assert len(df.columns) == 2 assert len(df) == 96 @@ -176,4 +179,4 @@ def run_test_project_by_property(tar_project, zip_project): df = scenario.read_element_info_file("PVSystems") assert isinstance(df, pd.DataFrame) - cap_changes = scenario.read_capacitor_changes() + scenario.read_capacitor_changes() diff --git a/tests/test_registry.py b/tests/test_registry.py index c206d5f1..c4f2845a 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -2,12 +2,11 @@ import copy import os -import tempfile from pathlib import Path import pytest -from pydss.common import ControllerType, CONTROLLER_TYPES +from pydss.common import ControllerType from pydss.registry import Registry, DEFAULT_REGISTRY @@ -54,7 +53,9 @@ def test_registry__register_controllers(registry_fixture): def test_registry__is_controller_registered(registry_fixture): registry = Registry(registry_filename=TEST_FILENAME) registry.reset_defaults() - assert registry.is_controller_registered(CTYPE, DEFAULT_REGISTRY["Controllers"][CTYPE][0]["name"]) + assert registry.is_controller_registered( + CTYPE, DEFAULT_REGISTRY["Controllers"][CTYPE][0]["name"] + ) def test_registry__reset_defaults(registry_fixture): diff --git a/tests/test_simple.py b/tests/test_simple.py index b34dcd4a..f102a9ca 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -1,3 +1 @@ __version__ = "0.0.1" - -from . import * diff --git a/tests/test_space_estimation.py b/tests/test_space_estimation.py index 39cd029c..482e3b61 100644 --- a/tests/test_space_estimation.py +++ b/tests/test_space_estimation.py @@ -1,11 +1,8 @@ import os -import pytest from pydss.pydss_project import PyDssProject -from tests.common import cleanup_project - RUN_PROJECT_PATH = os.path.join("tests", "data", "project")