├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── PVDER_schematic.png ├── README.md ├── config_der.json ├── docs ├── Makefile ├── PVDER_flags_variables_methods.md ├── PV_DER_model_specification_rev3.docx ├── make.bat ├── pvder_model_configuration_parameters.md ├── requirements.txt ├── software_architecture.png └── source │ ├── conf.py │ ├── index.rst │ ├── license_link.rst │ ├── notebook_links.nblink │ └── pvder_modules.rst ├── examples ├── PV-DER_parameter_update_example.ipynb ├── PV-DER_usage_example_LFRT_with_trip.ipynb ├── PV-DER_usage_example_LVRT_momentary_cessation_with_recovery.ipynb ├── PV-DER_usage_example_loop_mode.ipynb ├── PV-DER_wrapper_usage_example.ipynb └── voltage_time_series_for_loop_mode_testing.csv ├── logs └── .gitignore ├── pvder ├── DER_check_and_initialize.py ├── DER_components.py ├── DER_components_single_phase.py ├── DER_components_single_phase_constant_Vdc.py ├── DER_components_three_phase.py ├── DER_components_three_phase_balanced.py ├── DER_components_three_phase_constant_Vdc.py ├── DER_components_three_phase_no_Vrms_filter.py ├── DER_components_three_phase_numba.py ├── DER_features.py ├── DER_utilities.py ├── DER_wrapper.py ├── __init__.py ├── _version.py ├── config_default.json ├── constants.py ├── defaults.py ├── dynamic_simulation.py ├── exceptionutil.py ├── grid_components.py ├── logs │ └── .gitignore ├── logutil.py ├── properties.py ├── simulation_events.py ├── simulation_utilities.py ├── specifications.py ├── templates.py ├── templates_experimental.py ├── utilities_experimental.py ├── utility_classes.py ├── utility_functions.py └── utility_functions_numba.py ├── requirements.txt ├── setup.py └── tests ├── test_PVDER_DynamicSimulation.py ├── test_PVDER_HVRT.py ├── test_PVDER_LVRT.py ├── test_PVDER_SinglePhase.py ├── test_PVDER_ThreePhase.py └── unittest_utilities.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Python version:** 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: sibyjackgrove 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | docs/_static/ 69 | docs/_templates/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | install: 7 | - pip install -e . 8 | - pip install -r requirements.txt 9 | script: 10 | - python tests/test_PVDER_LVRT.py 11 | - python tests/test_PVDER_HVRT.py 12 | - python tests/test_PVDER_DynamicSimulation.py 13 | - python tests/test_PVDER_ThreePhase.py 14 | - python tests/test_PVDER_SinglePhase.py -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.0 - 2023-09-27 4 | 5 | ### Bug fix 6 | 7 | - Removed support for Python 2 8 | - Changes to creating DER model instance using "DERModel" class. 9 | - Removed the need to specify "modelType" when creating a model instance. The model type is specified in the config file now. 10 | - "derID" is now a required argument. 11 | - "powerRating" and "VrmsRating" can only be supplied through the config file now. 12 | - Minor refactoring and code cleanup. 13 | - Added Dockerfile for using high-performance Julia ODE solvers library - Acknowledgement to Abel Siqueira (Netherlands eScience Center) 14 | 15 | ## 0.5.3 - 2021-09-13 16 | 17 | ### Bug fix 18 | 19 | - Removed pathlib module from setup.py 20 | 21 | ## 0.5.2 - 2021-09-09 22 | 23 | ### Bug fix 24 | 25 | - Added Numba to setup.py 26 | 27 | ## 0.5.1 - 2021-07-28 28 | 29 | ### Bug fix 30 | 31 | - Fixed bug due to logs folder not being found. 32 | 33 | ## 0.5.0 - 2021-07-23 34 | 35 | ### Features 36 | 37 | - Added current reference ramp rate limiter for all models. 38 | 39 | ### Bug fix 40 | 41 | - Fixed bug in HVRT logging. 42 | - Fixed bug that multiplied *m_limit* with *1e1* in the current control loop integral reset logic. 43 | - Changed controller gains for *ConstantVdc* models to make their response slightly underdamped. Previously the response was overdamped. 44 | 45 | ## 0.4.0 - 2021-06-02 46 | 47 | ### Features 48 | 49 | - Added minimum ride-through time before momentary cessation. 50 | - Added additional logging information during voltage anomalies. 51 | 52 | ### Bug fix 53 | 54 | - Minor bug fixes and code clean up 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ARG PYTHON_VERSION=3.11.5 4 | ARG JULIA_VERSION=1.9.3 5 | 6 | ENV container docker 7 | ENV DEBIAN_FRONTEND noninteractive 8 | ENV LANG en_US.utf8 9 | ENV MAKEFLAGS -j4 10 | 11 | RUN mkdir /app 12 | WORKDIR /app 13 | 14 | # DEPENDENCIES 15 | #=========================================== 16 | RUN apt-get update -y && \ 17 | apt-get install -y gcc make wget zlib1g-dev libffi-dev libssl-dev libbz2-dev 18 | 19 | # INSTALL PYTHON 20 | #=========================================== 21 | RUN wget https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tgz && \ 22 | tar -zxf Python-$PYTHON_VERSION.tgz && \ 23 | cd Python-$PYTHON_VERSION && \ 24 | ./configure --with-ensurepip=install --enable-shared && make && make install && \ 25 | ldconfig && \ 26 | ln -sf python3 /usr/local/bin/python 27 | RUN python -m pip install --upgrade pip setuptools wheel && \ 28 | python -m pip install julia diffeqpy numpy networkx pandas matplotlib scipy boto3 29 | 30 | # INSTALL JULIA AND DIFFEQPY 31 | #==================================== 32 | RUN wget https://raw.githubusercontent.com/abelsiqueira/jill/main/jill.sh && \ 33 | bash /app/jill.sh -y -v $JULIA_VERSION && \ 34 | export PYTHON="python" && \ 35 | julia -e 'using Pkg; Pkg.add(["PyCall","Sundials","LSODA"])' && \ 36 | python -c 'import julia; julia.install()' 37 | RUN python3 -c "import diffeqpy; diffeqpy.install()" 38 | 39 | RUN mkdir /home/pvder 40 | WORKDIR /home/pvder 41 | 42 | # CLEAN UP 43 | #=========================================== 44 | RUN rm -rf /app/jill.sh \ 45 | /opt/julias/*.tar.gz \ 46 | /app/Python-$PYTHON_VERSION.tgz 47 | 48 | RUN apt-get purge -y gcc make wget zlib1g-dev libffi-dev libssl-dev && \ 49 | apt-get autoremove -y 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2019, UChicago Argonne, LLC 2 | All Rights Reserved 3 | Photovoltaic Distributed Energy Resource (PV-DER) Simulation Utility 4 | Argonne National Laboratory 5 | OPEN SOURCE LICENSE 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following 8 | conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 11 | disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided with the distribution. 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | products derived from this software without specific prior written permission. 16 | 17 | *************************************************************************************************** 18 | DISCLAIMER 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 21 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 22 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | *************************************************************************************************** 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pvder/logs/*.gitignore 2 | include tests/*.py 3 | include config_der.json -------------------------------------------------------------------------------- /PVDER_schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdcosim/SolarPV-DER-simulation-tool/52337c452b1d82816276e2af6c9aadffad3dbd39/PVDER_schematic.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Status:** Expect regular updates and bug fixes. 2 | 3 | # Tool for simulating dynamics of PV-DER 4 | [![Build Status](https://travis-ci.org/sibyjackgrove/SolarPV-DER-simulation-utility.svg?branch=master)](https://travis-ci.org/sibyjackgrove/SolarPV-DER-simulation-utility) 5 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/pvder?label=PyPI%20Downloads) 6 | [![CodeFactor](https://www.codefactor.io/repository/github/tdcosim/solarpv-der-simulation-tool/badge)](https://www.codefactor.io/repository/github/tdcosim/solarpv-der-simulation-utility) 7 | 8 | Solar photovoltaic distributed energy resources (PV-DER) are power electronic inverter based generation (IBG) connected to the electric power distribution system (eg. roof top solar PV systems). This tool can be used to simulate the dynamics of a single DER connected to a stiff voltage source as shown in the following schematic: 9 | 10 | ![schematic of PV-DER](PVDER_schematic.png) 11 | 12 | ## Basics 13 | The dynamics of the DER are modelled using dynamic phasors. Detailed description of the concepts behind this tool can be found in the IEEE publication [Dynamic Modeling of Solar PV Systems for Distribution System Stability Analysis](https://www.researchgate.net/publication/333985171_Dynamic_Modeling_of_Solar_PV_Systems_for_Distribution_System_Stability_Analysis) and detailed list of equations can be found in the [Model specification document.](docs/PV_DER_model_specification_rev3.docx) 14 | 15 | ### Features 16 | The following features are available currently: 17 | 1. Single phase, three phase balanced, and three phase unbalanced (phase voltages may be unbalanced) DER models. 18 | 2. Run simulation in stand alone mode with internal grid voltage source (stiff) model. 19 | 3. Run simulation in loop mode where grid voltage is supplied every time step by a third party program. 20 | 4. Customize all aspects of the model through a [JSON](config_der.json) file which provides access to parameters in all the model components. 21 | 5. Visualize or retrieve simulation results for voltages, current, active, and reactive power. 22 | 5. Introduce solar insolation events (in all modes), grid voltage, and frequency change events (in stand alone mode). 23 | 6. Retrieve and modify model parameters from a third party program. 24 | 7. Following smart inverter features are available: Low/High voltage ride through (LVRT/HVRT), Low frequency ride through (LFRT), and Volt-VAR control logic. 25 | 26 | ## Links 27 | * Source code repository: https://github.com/sibyjackgrove/SolarPV-DER-simulation-tool 28 | * API Documentation: https://solarpv-der-simulation-utility.readthedocs.io/en/latest/ 29 | * Additional documentation: [Description of attributes and methods](docs/PVDER_flags_variables_methods.md) 30 | 31 | ## Installation 32 | 33 | Dependencies: 34 | 35 | - SciPy >= 1.2.1 36 | - Numpy >= 1.16.2 37 | - Matlplotlib >= 3.0.3 38 | 39 | Install latest release: 40 | ``` 41 | pip install pvder 42 | ``` 43 | 44 | Install from source: 45 | ``` 46 | git clone https://github.com/tdcosim/SolarPV-DER-simulation-tool.git 47 | cd SolarPV-DER-simulation-tool 48 | pip install -e . 49 | ``` 50 | 51 | ## Use cases 52 | Following projects are using Solar PV-DER simulation tool: 53 | 1. [Argonne Transmission and Distribution systems Co-Simulation tool (TDcoSim)](https://github.com/tdcosim/TDcoSim) 54 | 2. [OpenAI Gym Distributed Energy Resource Environment (Gym-DER)](https://github.com/sibyjackgrove/gym-SolarPVDER-environment) 55 | 56 | ## Using the tool 57 | This tool can be imported as a normal python module: 58 | 59 | ```python 60 | import pvder 61 | ``` 62 | 63 | ### Using the stand alone single phase DER model with 10 kW power rating 64 | The following steps are required. Additional documentation on attributes and methods are available [here](docs/PVDER_flags_variables_methods.md). 65 | 1. First import the following classes: 66 | ``` 67 | from pvder.DER_components_single_phase import SolarPV_DER_SinglePhase 68 | from pvder.grid_components import Grid 69 | from pvder.dynamic_simulation import DynamicSimulation 70 | from pvder.simulation_events import SimulationEvents 71 | from pvder.simulation_utilities import SimulationResults 72 | ``` 73 | 1. Create a **_SimulationEvents_** object: This object is used to add or remove disturbance events occurs during the simulation. 74 | ``` 75 | events = SimulationEvents() 76 | ``` 77 | 2. Create a **Grid** object: This object describes the steady state model for the grid voltage source. It needs to be supplied with an **_SimulationEvents_** object. 78 | ``` 79 | grid = Grid(events=events) 80 | ``` 81 | 3. Create a **SolarPV_DER_SinglePhase** or **SolarPV_DER_ThreePhase** object: This object describes the dynamic DER model. It needs both an **_SimulationEvents_** object, and a path name for JSON file containing the DER configuration parameters. It also needs a **Grid** object in stand alone mode). Additionaly either the power rating of the DER or the id for the parameter dictionary should be provided. 82 | ``` 83 | PV_DER = SolarPV_DER_SinglePhase(events=events,configFile=r'config_der.json',gridModel=grid,derId= '10',standAlone = True) 84 | ``` 85 | 4. Create a **DynamicSimulation** object: This object runs the simulation and stores the solution. It takes **_SimulationEvents_**, **Grid** and, **SolarPV_DER_SinglePhase** objects as arguments. 86 | ``` 87 | sim = DynamicSimulation(grid_model=grid,PV_model=PV_DER,events = events) 88 | ``` 89 | 5. Create a **SimulationResults** object: This object is used to visualize the simulation results. 90 | ``` 91 | results = SimulationResults(simulation = sim) 92 | ``` 93 | 6. Add an event (for e.g. solar insolation change at 10.0 s): 94 | ``` 95 | events.add_solar_event(10,90) 96 | ``` 97 | 7. Specify simulation flags (for e.g. set the DEBUG_SIMULATION and DEBUG_POWER flag to true to observe the power at each time step.): 98 | ``` 99 | sim.DEBUG_SIMULATION = False 100 | sim.DEBUG_POWER = False 101 | ``` 102 | 8. Specify simulation stop time (for e.g. 20.0 s): 103 | ``` 104 | sim.tStop = 20.0 105 | ``` 106 | 9. Run the simulation: 107 | ``` 108 | sim.run_simulation() 109 | ``` 110 | 10. Visualize the results (for e.g. the power output at PCC-LV side): 111 | ``` 112 | results.PER_UNIT = False 113 | results.plot_DER_simulation(plot_type='active_power_Ppv_Pac_PCC') 114 | ``` 115 | 116 | ### Examples 117 | Try out Jupyter notebooks with usage examples in Google Colab: 118 | 119 | Basic usage: 120 | [![Basic usage](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/sibyjackgrove/SolarPV-DER-simulation-tool/blob/master/examples/PV-DER_usage_example.ipynb) 121 | 122 | Running simulation in loop mode: 123 | [![Updating model parameters](https://colab.research.google.com/assets/colab-badge.svg)](https://github.com/sibyjackgrove/SolarPV-DER-simulation-tool/blob/master/examples/PV-DER_usage_example_loop_mode.ipynb) 124 | 125 | Updating model parameters: 126 | [![Updating model parameters](https://colab.research.google.com/assets/colab-badge.svg)](https://github.com/sibyjackgrove/SolarPV-DER-simulation-tool/blob/master/examples/PV-DER_parameter_update_example.ipynb) 127 | 128 | Voltage anomaly, ride through, and momentary cessation: 129 | [![Voltage anomaly](https://colab.research.google.com/assets/colab-badge.svg)](https://github.com/sibyjackgrove/SolarPV-DER-simulation-tool/blob/master/examples/PV-DER_usage_example_LVRT_momentary_cessation_with_recovery.ipynb) 130 | 131 | Frequency anomaly, ride through, and trip: 132 | [![Frequency anomaly](https://colab.research.google.com/assets/colab-badge.svg)](https://github.com/sibyjackgrove/SolarPV-DER-simulation-tool/blob/master/examples/PV-DER_usage_example_LFRT_with_trip.ipynb) 133 | 134 | ## Module details 135 | A schematic of the relationship between differen classes in the module is shown in the figure below: 136 | ![schematic of software architecture](docs/software_architecture.png) 137 | 138 | ## Issues 139 | Please feel free to raise an issue for bugs or feature requests. 140 | 141 | ## Who is responsible? 142 | 143 | **Core developer:** 144 | - Siby Jose Plathottam splathottam@anl.gov 145 | 146 | **Contributor:** 147 | 148 | - Karthikeyan Balasubramaniam kbalasubramaniam@anl.gov 149 | 150 | ## Acknowledgement 151 | 152 | This project was supported by Kemal Celik, [U.S. DOE Office of Electricity, Solar Energy Technology Office](https://www.energy.gov/eere/solar/solar-energy-technologies-office) through the [SuNLaMP](https://www.energy.gov/eere/solar/sunshot-national-laboratory-multiyear-partnership-sunlamp) program. 153 | 154 | The authors would like to acknowledge [Shrirang Abhyankar](https://github.com/abhyshr) and Puspal Hazra for their contribution. 155 | 156 | ## Citation 157 | If you use this code please cite it as: 158 | ``` 159 | @misc{pvder, 160 | title = {{SolarPV-DER-simulation-tool}: A simulation tool for or solar photovoltaic distributed energy resources}, 161 | author = "{Siby Jose Plathottam,Karthikeyan Balasubramaniam}", 162 | howpublished = {\url{https://github.com/sibyjackgrove/SolarPV-DER-simulation-tool}}, 163 | url = "https://github.com/sibyjackgrove/SolarPV-DER-simulation-tool", 164 | year = 2019, 165 | note = "[Online; accessed 19-March-2019]" 166 | } 167 | ``` 168 | ### Copyright and License 169 | Copyright © 2019, UChicago Argonne, LLC 170 | 171 | Photovoltaic Distributed Energy Resource (PV-DER) Simulation tool is distributed under the terms of [BSD-3 OSS License.](LICENSE) 172 | -------------------------------------------------------------------------------- /config_der.json: -------------------------------------------------------------------------------- 1 | { 2 | "50":{"parent_config":"", 3 | "basic_specs":{"model_type":"SolarPVDERThreePhase"}, 4 | "basic_options":{"Sinsol":100.0,"current_gradient_limiter":false}, 5 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 6 | 7 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 8 | 9 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6, 10 | "C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 11 | 12 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0, 13 | "Kp_DC":-2.0,"Ki_DC":-10.0, 14 | "Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 15 | 16 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 17 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 18 | "xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28}, 19 | "verbosity":"INFO", 20 | "LVRT":{"config_id":"LVRT_1547cat3"}, 21 | "VRT_delays":{"config_id":"VRT_delay_cat3"} 22 | }, 23 | 24 | "50_numba":{"parent_config":"50", 25 | "basic_specs":{"model_type":"SolarPVDERThreePhaseNumba"} 26 | }, 27 | "50_type1":{"parent_config":"50", 28 | "inverter_ratings":{"Ioverload":1.1} 29 | }, 30 | 31 | "LVRT_1547cat3":{"config":{"1":{"V_threshold":0.88, 32 | "t_threshold":5.0, 33 | "t_min_ridethrough":20, 34 | "mode":"momentary_cessation", 35 | "t_start":0.0, 36 | "threshold_breach":false}, 37 | "2":{"V_threshold":0.7, 38 | "t_threshold":3.5, 39 | "t_min_ridethrough":2.5, 40 | "mode":"momentary_cessation", 41 | "t_start":0.0, 42 | "threshold_breach":false}, 43 | "3":{"V_threshold":0.5, 44 | "t_threshold":0.25, 45 | "t_min_ridethrough":1, 46 | "mode":"momentary_cessation", 47 | "t_start":0.0, 48 | "threshold_breach":false} 49 | } 50 | }, 51 | "VRT_delay_cat3":{"config":{"output_cessation_delay":0.01, 52 | "output_restore_delay":0.1, 53 | "restore_Vdc":false} 54 | }, 55 | "250":{"parent_config":"", 56 | "basic_specs":{"model_type":"SolarPVDERThreePhase"}, 57 | "basic_options":{"Sinsol":100.0,"current_gradient_limiter":true}, 58 | "module_parameters":{"Np":45,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 750.0,"Vdcmpp_max": 1000.0}, 59 | 60 | "inverter_ratings":{"Srated":250e3,"Vdcrated":750.0,"Ioverload":1.3,"Vrmsrated":230.0,"Iramp_max_gradient_real":1.5,"Iramp_max_gradient_imag":1.0}, 61 | 62 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":300.0e-6, 63 | "C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 64 | 65 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0, 66 | "Kp_DC":-2.0,"Ki_DC":-10.0, 67 | "Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 68 | 69 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 70 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 71 | "xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28}, 72 | "verbosity":"WARNING", 73 | "LVRT":{"config_id":"LVRT_1547cat3"} 74 | }, 75 | 76 | "250_numba":{"parent_config":"250", 77 | "basic_specs":{"model_type":"SolarPVDERThreePhaseNumba"} 78 | }, 79 | 80 | "10":{"parent_config":"", 81 | "basic_specs":{"model_type":"SolarPVDERSinglePhase"}, 82 | "basic_options":{"Sinsol":100.0,"current_gradient_limiter":true}, 83 | "module_parameters":{"Np":2,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 650.0,"Vdcmpp_max": 800.0}, 84 | 85 | "inverter_ratings":{"Srated":10e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 86 | 87 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6, 88 | "C_actual":30.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 89 | 90 | "controller_gains":{"Kp_GCC":12000.0,"Ki_GCC":4000.0, 91 | "Kp_DC":-4.0,"Ki_DC":-20.0, 92 | "Kp_Q":0.4,"Ki_Q":20.0,"wp": 20e4}, 93 | 94 | "steadystate_values":{"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01}, 95 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 96 | "xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28} 97 | 98 | }, 99 | 100 | "50_balanced":{"parent_config":"", 101 | "basic_specs":{"model_type":"SolarPVDERThreePhaseBalanced"}, 102 | "basic_options":{"Sinsol":100.0,"current_gradient_limiter":false}, 103 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 104 | 105 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":0.9,"Iramp_max_gradient_imag":0.9}, 106 | 107 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6, 108 | "C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 109 | 110 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0, 111 | "Kp_DC":-2.0,"Ki_DC":-10.0, 112 | "Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 113 | 114 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 115 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 116 | "xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28}, 117 | "verbosity":"INFO", 118 | "LVRT":{"config_id":"LVRT_1547cat3"} 119 | }, 120 | 121 | "10_constantVdc":{"parent_config":"", 122 | "basic_specs":{"model_type":"SolarPVDERSinglePhaseConstantVdc"}, 123 | "basic_options":{"Sinsol":100.0,"current_gradient_limiter":true}, 124 | "module_parameters":{"Np":2,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 650.0,"Vdcmpp_max": 800.0}, 125 | 126 | "inverter_ratings":{"Srated":10e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 127 | 128 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6, 129 | "C_actual":30.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 130 | 131 | "controller_gains":{"Kp_GCC":12000.0,"Ki_GCC":4000.0, 132 | "Kp_P":0.1,"Ki_P":40.0, 133 | "Kp_Q":0.04,"Ki_Q":2.0,"wp": 20e4}, 134 | 135 | "steadystate_values":{"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01}, 136 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 137 | "xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28} 138 | }, 139 | 140 | "50_constantVdc":{"parent_config":"", 141 | "basic_specs":{"model_type":"SolarPVDERThreePhaseConstantVdc"}, 142 | "basic_options":{"Sinsol":100.0,"current_gradient_limiter":false}, 143 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 144 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 145 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 146 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0,"Kp_P":0.1,"Ki_P":2.0,"Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 147 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 148 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 149 | "ibR":0,"ibI":0.0,"xbR":0.0,"xbI":0.0,"ubR":0.0,"ubI":0.0, 150 | "icR":0,"icI":0.0,"xcR":0.0,"xcI":0.0,"ucR":0.0,"ucI":0.0, 151 | "xP":0.0,"xQ":0.0, 152 | "xPLL":0.0,"wte":6.28}, 153 | "verbosity":"INFO" 154 | }, 155 | 156 | "1":{"parent_config":"", 157 | "basic_specs":{"model_type":"SolarPVDERSinglePhase"}, 158 | "basic_options":{"Sinsol":100.0}, 159 | "module_parameters":{"Np":2,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 650.0,"Vdcmpp_max": 800.0}, 160 | "inverter_ratings":{"Srated":10e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0}, 161 | 162 | "circuit_parameters":{"Rf_actual":0.01,"Lf_actual":3.0e-3, 163 | "C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 164 | 165 | "controller_gains":{"Kp_GCC":12000.0,"Ki_GCC":4000.0, 166 | "Kp_DC":-4.0,"Ki_DC":-20.0, 167 | "Kp_Q":0.4,"Ki_Q":20.0,"wp": 20e4}, 168 | 169 | "steadystate_values":{"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01}, 170 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 171 | "xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28} 172 | } 173 | } -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/PVDER_flags_variables_methods.md: -------------------------------------------------------------------------------- 1 | # PV-DER Simulation Utility initialization, variables, flags, and methods 2 | 3 | This document is a supplement to the [Jupyter notebook usage example](../examples/PV-DER_usage_example.ipynb). It provides information on important arguments during object creation, functionalities of various binary flags/variables (object attributes), and attached methods. 4 | 5 | ### Common initialization arguments 6 | 7 | 1. **verbosity (string):** Logging level for objects (available: 'DEBUG','INFO,'WARNING'). 8 | 2. **identifier (string):** User defined identification string that will be added to the object name. 9 | ### DER model objects 10 | Object type name: *SolarPV_DER_ThreePhase*,*SolarPV_DER_ThreePhaseBalanced*,*SolarPV_DER_SinglePhase* 11 | #### Essential initialization arguments 12 | 1. **Sinverter_rated (float):** Inverter power rating in W (Available: 10.0e3, 50.0e3, 250.0e3, default: 50.0e3). 13 | 1. **Vrms_rated (float):** Inverter voltage rating (L-G RMS) in V (default: None). 14 | 1. **standAlone (Boolean):** If this flag is **True**, the DER model is expected to work as a stand alone model connected to a stif voltage source. If **False**, the DER model expects to recieve voltage values from an external program (default: **True**). 15 | 1. **STEADY_STATE_INITIALIZATION (Boolean):** If this flag is **True**, the states in the dynamic DER model will be initialized with values corresponding to its steady state operating point. If **False** the states will be initialized with zeroes (default: **False**). 16 | 17 | #### Voltage ride through variables and flags 18 | 1. **LVRT_ENABLE (Boolean):** If this flag is **True**, the low voltage ride through and protection logic will be enabled. If **False** the DER instance will neither trip nor enter momentary cessation when abnormal low voltage conditions are encountered (default: **True**). 19 | 2. **HVRT_ENABLE (Boolean):** If this flag is **True**, the high voltage ride through and protection logic will be enabled. If **False** the DER instance will neither trip nor enter momentary cessation when abnormal high voltage conditions are encountered (default: **True**). 20 | 3. **pvderConfig (dict)**: Specifies the user defined LVRT and HVRT settings. Can be passed as argument when initializing DER object. An arbitrary number of voltage threshold levels can be specified. Each level also has a mode that defines the DER behavior during the ride through. In both modes, once the t_threshold times are breached, the DER is tripped (i.e. power output reduced to zero permanently). The definition of the modes are as follows: 21 | * *momentary_cessation*: Power output reduced to zero during the ride through. 22 | * *mandatory_operation*: Try to maintain rated power output during the ride through. 23 | * Example for LVRT setting: 'LVRT':{'0':{'V_threshold':0.5, 't_threshold':1.0, 'mode': 'mandatory_operation'} } 24 | * Example for HVRT setting: 'HVRT':{'0':{'V_threshold':1.12, 't_threshold':0.5, 'mode': 'momentary_cessation'} } 25 | 26 | #### Other essential variables and flags 27 | 28 | 1. **LFRT_ENABLE (Boolean):** If this flag is **True**, the low frequency ride through and protection logic will be enabled. If **False** the DER instance will never trip when abnormal low frequency conditions are encountered (default: **False**). 29 | 2. **VOLT_VAR_ENABLE (Boolean):** If this flag is **True**, Volt-VAR control is enabled during voltage sags within a specified voltage range. If **False** the DER will neither supply or absorb reactive power when voltage sags are encountered (default: **False**). 30 | 3. **use_frequency_estimate (Boolean):** If this flag is **True**, grid frequency is estimated using the difference between phase angles at two consecutive time steps (default: **False**). 31 | 32 | #### Essential methods 33 | 1. **show_PV_DER_states(quantity):** Show the values for the specified DER variable (default: 'voltage'). 34 | 1. **show_PV_DER_parameters(parameter_type):** Show the values for the specified DER parameter (default: 'inverter_ratings'). 35 | 2. **initialize_parameter_dict(parameter_ID,source_parameter_ID):** Initialize a new parameter dictionary. 36 | 2. **update_parameter_dict(parameter_ID,parameter_type,parameter_dict):** Update an existing parameter dictionary with new values. 37 | 38 | ### Dynamic simulation model objects 39 | Object types: *DynamicSimulation* 40 | 41 | #### Essential initialization arguments 42 | 1. **LOOP_MODE (Boolean):** If this flag is **True**, the simulation is expected to run in a loop and get updated grid voltages at every iteration in the loop (default: **False**). 43 | 2. **COLLECT_SOLUTION (Boolean):** If this flag is **True**, the time series data for states and other variables are collected at the end of every call to the ODE solver (default: **True**). 44 | 45 | #### Essential variables and flags 46 | 1. **tStart (float):** Start time for simulation in seconds (default: 0.0 s). 47 | 2. **tEnd (float):** End time for simulation in seconds (default: 0.5 s). 48 | 2. **tInc (float):** Time step for simulation in seconds (default: 0.001 s). 49 | 1. **jacFlag (Boolean):** If this flag is **True**, the analytical Jacobian will be passed to the SciPy ODE solver which may improve solution time. If this flag is **False** the solver will have to numerically calculate the Jacobian (default: False). 50 | 2. **DEBUG_SIMULATION (Boolean):** If this flag is **True**, the value of the model variables at each time step will be printed to the terminal at each time step. If this flag is **False** only information from ride through logic will be printed (default: False). 51 | 2. **DEBUG_SOLVER (Boolean):** If this flag is **True**, solution status from ODE solver is printed during each call to solver. If **False**, solution status will only be printed if there is an exception (default: False). 52 | 2. **PER_UNIT (Boolean):** If this flag is **True**, all the displayed electrical quantities will be in per unit values. If **False**, all the displayed quantities will be in actual values (default: True). 53 | #### Essential methods 54 | 1a. **run_simulation():** If LOOP_MODE is True the simulation is run from **tStart** to **tEnd** with time step of **tInc**. 55 | 1b. **run_simulation(gridVoltagePhaseA, gridVoltagePhaseB, gridVoltagePhaseC, y0, t):** If LOOP_MODE is True the voltages, states, and time steps need to to be provided at every iteration. 56 | 57 | 58 | ### Simulation events object 59 | Object type name: *SimulationEvents* 60 | **Note:** Grid events are introduced during the simulation only if **standAlone** flag is True in *DER model* and **LOOP_MODE** is False in *DynamicSimulation*. 61 | 62 | #### Essential initialization arguments 63 | 1. **SOLAR_EVENT_ENABLE (Boolean):** If this flag is **True**, any solar events that were added will be introduced during the simulation (default: **True**). 64 | 1. **GRID_EVENT_ENABLE (Boolean):** If this flag is **True**, any grid events that were added will be introduced during the simulation (default: **True**). 65 | 66 | #### Essential methods 67 | 68 | 1. **add_grid_event(T, Vgrid, Vgrid_angle, fgrid):** Add grid event at T (default: Vgrid=1.0, Vgrid_angle = 0.0, fgrid = 60). 69 | 2. **add_solar_event(T, Sinsol):** Add solar event at T (default: Sinsol=100). 70 | 3. **remove_grid_event(T):** Remove all grid events (voltage, phase angle, or frequency) at T. 71 | 4. **remove_solar_event(T):** Remove solar event at T. 72 | 5. **show_events():** Show list of all simulation events in chronological order. 73 | 74 | ### Results object 75 | Object type name: *SimulationResults* 76 | 77 | #### Essential initialization arguments 78 | 1. **PER_UNIT (Boolean):** If this flag is **True**, all the plots for electrical quantities will be in per unit values. If **False**, all the displayed quantities will be in actual values (default: **True**). 79 | 80 | #### Essential variables 81 | 1. **font_size (int):** Font size of text within the plots (default: 18). 82 | 83 | #### Essential methods 84 | 1. **plot_DER_simulation(plot_type):** Time series plot for specified electrical quantity (Available: 'power','active_power','active_power_Ppv_Pac_PCC','reactive_power','reactive_power_Q_PCC','voltage','voltage_Vdc','voltage_HV','voltage_HV_imbalance','voltage_LV','voltage_Vpcclv','current','duty_cycle','voltage_Vpcclv_all_phases'; default:'power', ) 85 | -------------------------------------------------------------------------------- /docs/PV_DER_model_specification_rev3.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdcosim/SolarPV-DER-simulation-tool/52337c452b1d82816276e2af6c9aadffad3dbd39/docs/PV_DER_model_specification_rev3.docx -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/pvder_model_configuration_parameters.md: -------------------------------------------------------------------------------- 1 | # DER model parameters 2 | 3 | The parameters used in the DER model and available for customization through the config file are are described here. These parameters can impact both the dynamic and steady state behaviour of a DER model. If the user doesn't specify any parameter for a given model type, they are automatically populated from the default values. 4 | 5 | * **Module parameters** 6 | * *Np/Ns (int):* Parallel/series connected solar cells. Determines rated power output from the model. 7 | * *Vdcmpp0 (float):* Default maximum power point DC link voltage. 8 | 9 | * * **Inverter ratings** 10 | * *Srated (float):* Rated apparent power rating of the inverter (unit: kVA). 11 | * *Vrmsrated (float):* Rated voltage (RMS) of the inverter (unit: Volts). 12 | * *Vdcrated (float):* Rated DC link voltage of the inverter (unit: Volts). 13 | * *Ioverload (float):* Overload capacity of the inverter. 14 | 15 | * * **Circuit parameters** 16 | * *Rf_actual (float):* Inverter output filter resistance (unit: Ohm). 17 | * *Lf_actual (float):* Inverter output filter inductance (unit: Henry). 18 | * *C_actual (float):* Inverter DC link capacitor capacitance (unit: Farad). 19 | 20 | * * **Controller gains** 21 | * *Kp_GCC (float):* Current controller propotional gain. 22 | * *Ki_GCC (float):* Current controller integral gain. 23 | * *wp (float):* Current controller input filter time constant. 24 | * *Kp_P (float):* Active power controller propotional gain (only available in constant Vdc models). 25 | * *Ki_P (float):* Active power controller integral gain (only available in constant Vdc models). 26 | * *Kp_DC (float):* DC link voltage controller propotional gain . 27 | * *Ki_DC (float):* DC link voltage controller integral gain . 28 | * *Kp_Q (float):* Reactive power controller propotional gain. 29 | * *Ki_Q (float):* Reactive power controller integral gain. 30 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.4 2 | ipykernel 3 | sphinx-rtd-theme 4 | nbsphinx 5 | nbsphinx-link -------------------------------------------------------------------------------- /docs/software_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdcosim/SolarPV-DER-simulation-tool/52337c452b1d82816276e2af6c9aadffad3dbd39/docs/software_architecture.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | sys.path.insert(0, os.path.abspath('../examples')) 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'PV-DER Simulation Utility' 21 | copyright = '2019, Siby Jose Plathottam' 22 | author = 'Siby Jose Plathottam' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.0.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 34 | 'sphinx.ext.napoleon','nbsphinx','nbsphinx_link'] # 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | source_suffix = ['.rst', '.md'] 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**.ipynb_checkpoints'] 51 | 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = "sphinx_rtd_theme"#'alabaster' 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ['_static'] 64 | 65 | #Allow errors in notebooks when document is being built. 66 | nbsphinx_allow_errors = True 67 | 68 | autoclass_content = 'both' -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | API documentation for PV-DER Simulation Utility 2 | *********************************************** 3 | 4 | This is a reference for all the classes and methods available in PV-DER Simulation Utility. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | pvder_modules 11 | license_link 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | -------------------------------------------------------------------------------- /docs/source/license_link.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. include:: ../../LICENSE -------------------------------------------------------------------------------- /docs/source/notebook_links.nblink: -------------------------------------------------------------------------------- 1 | { 2 | "path": "../../examples/PV-DER_usage_example.ipynb" 3 | } 4 | -------------------------------------------------------------------------------- /docs/source/pvder_modules.rst: -------------------------------------------------------------------------------- 1 | PV-DER package 2 | ============== 3 | 4 | DER models 5 | ---------- 6 | 7 | pvder.DER_components_three_phase 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | .. automodule:: pvder.DER_components_three_phase 11 | :members: 12 | :show-inheritance: 13 | 14 | pvder.DER_components_single_phase 15 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 16 | 17 | .. automodule:: pvder.DER_components_single_phase 18 | :members: 19 | :show-inheritance: 20 | 21 | pvder.DER_check_and_initialize 22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | .. automodule:: pvder.DER_check_and_initialize 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | pvder.DER_utilities 30 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 31 | 32 | .. automodule:: pvder.DER_utilities 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | pvder.DER_features 38 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | 40 | .. automodule:: pvder.DER_features 41 | :members: 42 | :undoc-members: 43 | :show-inheritance: 44 | 45 | DER Simulation 46 | -------------- 47 | 48 | pvder.dynamic_simulation 49 | ~~~~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | .. automodule:: pvder.dynamic_simulation 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | 56 | pvder.simulation_events 57 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | .. automodule:: pvder.simulation_events 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | 64 | pvder.simulation_utilities 65 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 66 | 67 | .. automodule:: pvder.simulation_utilities 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: -------------------------------------------------------------------------------- /examples/PV-DER_parameter_update_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "colab_type": "text", 7 | "id": "view-in-github" 8 | }, 9 | "source": [ 10 | "\"Open" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "## Accessing and updating model parameters" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": { 23 | "colab_type": "text", 24 | "id": "J4tKN3DvS5U8" 25 | }, 26 | "source": [ 27 | "### Import classes\n", 28 | "Import classes neccessary to define instances of the PV-DER model, events, simulation, and results." 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 1, 34 | "metadata": { 35 | "colab": {}, 36 | "colab_type": "code", 37 | "id": "JwLCIIkdS5U_" 38 | }, 39 | "outputs": [ 40 | { 41 | "name": "stdout", 42 | "output_type": "stream", 43 | "text": [ 44 | "Scipy version: 1.4.1\n", 45 | "Numpy version: 1.20.3\n", 46 | "Matplotlib version: 3.4.2\n", 47 | "PVDER version: 0.5.5\n" 48 | ] 49 | } 50 | ], 51 | "source": [ 52 | "from pvder.DER_wrapper import DERModel\n", 53 | "\n", 54 | "from pvder.grid_components import Grid\n", 55 | "from pvder.dynamic_simulation import DynamicSimulation\n", 56 | "from pvder.simulation_events import SimulationEvents\n", 57 | "from pvder.simulation_utilities import SimulationUtilities,SimulationResults\n", 58 | "from pvder import utility_functions\n", 59 | "import pvder\n", 60 | "\n", 61 | "#Import Python modules\n", 62 | "import logging\n", 63 | "import numpy as np\n", 64 | "import scipy as sci\n", 65 | "import math\n", 66 | "import cmath\n", 67 | "import pandas\n", 68 | "\n", 69 | "\n", 70 | "import matplotlib as matplotlib\n", 71 | "print('Scipy version:',sci.__version__)\n", 72 | "print('Numpy version:',np.__version__)\n", 73 | "print('Matplotlib version:',matplotlib.__version__)\n", 74 | "print('PVDER version:',pvder.__version__)\n", 75 | "#%matplotlib inline #To show interactive plot in the notebook cell" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "### Specify voltage values\n", 83 | "Specify voltage values for non-standalone model." 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 2, 89 | "metadata": {}, 90 | "outputs": [ 91 | { 92 | "name": "stdout", 93 | "output_type": "stream", 94 | "text": [ 95 | "Vrms:146.07\n", 96 | "Va:164.78-124.57j,Vb:-190.96-78.26j,Vc:26.24+206.56j\n", 97 | "V0:0.06+3.73j\n" 98 | ] 99 | } 100 | ], 101 | "source": [ 102 | "logging.debug('test')\n", 103 | "Va=164.78-124.57j\n", 104 | "Vb=-190.96-78.26j\n", 105 | "Vc=26.24+206.56j\n", 106 | "Vrms = abs(Va)/math.sqrt(2)\n", 107 | "print('Vrms:{:.2f}'.format(Vrms))\n", 108 | "print('Va:{:.2f},Vb:{:.2f},Vc:{:.2f}'.format(Va,Vb,Vc))\n", 109 | "print('V0:{:.2f}'.format(Va+Vb+Vc))\n", 110 | "\n", 111 | "events1 = SimulationEvents(verbosity = 'DEBUG')\n", 112 | "grid1 = Grid(events=events1,unbalance_ratio_b=1.0,unbalance_ratio_c=1.0)\n" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "metadata": {}, 118 | "source": [ 119 | "## Select options" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 3, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "STAND_ALONE = False\n", 129 | "STEADY_STATE = True\n", 130 | "LOOP=False\n", 131 | "\n", 132 | "model_type = 'ThreePhaseUnbalanced' #Model type\n", 133 | "#three phase model types: 'ThreePhaseBalanced','ThreePhaseUnbalanced','ThreePhaseUnbalancedConstantVdc'\n", 134 | "#single phase model types: 'SinglePhase'\n", 135 | "\n", 136 | "der_verbosity = 'INFO' #Logging verbosity\n", 137 | "config_file = r'../config_der.json'\n", 138 | "parameter1 = '50'#'50_type1'#'50','10' '50_constantVdc','50_balanced','250'" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "metadata": { 144 | "colab_type": "text", 145 | "id": "IrB7IEwGS5VD" 146 | }, 147 | "source": [ 148 | "### Create objects required to simulate either single phase or three phase PV-DER" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 4, 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "name": "stderr", 158 | "output_type": "stream", 159 | "text": [ 160 | "INFO:pvder_logger:DER configuration with ID:50 was found in ../config_der.json\n", 161 | "INFO:pvder_logger:-SolarPVDERThreePhase_1:Instance created with DER parameter ID: 50; Specifications - Srated:50.0 kVA, Ppv:45.8 kW, Vrms:146.1 V, Steady state:True,LVRT Enable:True,HVRT Enable:True\n" 162 | ] 163 | }, 164 | { 165 | "name": "stdout", 166 | "output_type": "stream", 167 | "text": [ 168 | "Optimization results:[ 0.6030582 -0.45266374 1.16945499 -0.89241647]\n" 169 | ] 170 | } 171 | ], 172 | "source": [ 173 | "PV_DER1 = DERModel(modelType=model_type,events=events1,configFile=config_file,\n", 174 | " Vrmsrated = Vrms,\n", 175 | " gridVoltagePhaseA = Va,gridVoltagePhaseB = Vb,gridVoltagePhaseC = Vc,gridFrequency=2*math.pi*60.0,\n", 176 | " derId=parameter1,\n", 177 | " standAlone = STAND_ALONE,steadyStateInitialization=STEADY_STATE,\n", 178 | " verbosity = der_verbosity) \n", 179 | " \n", 180 | "sim1 = DynamicSimulation(gridModel=grid1,PV_model=PV_DER1.DER_model,events = events1,verbosity = 'INFO',solverType='odeint',LOOP_MODE=LOOP) #'odeint','ode-vode-bdf'\n", 181 | "results1 = SimulationResults(simulation = sim1,PER_UNIT=True,verbosity = 'INFO')" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "## Create and use a user defined parameter dictionary\n", 189 | "### 1. Show existing parameter dictionaries using show_parameter_dictionaries() and show_parameter_types().\n" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 5, 195 | "metadata": {}, 196 | "outputs": [ 197 | { 198 | "name": "stdout", 199 | "output_type": "stream", 200 | "text": [ 201 | "-----Parameter dictionary: Parameter IDs-----\n", 202 | "module_parameters : 50\n", 203 | "inverter_ratings : 50\n", 204 | "circuit_parameters : 50\n", 205 | "controller_gains : 50\n", 206 | "steadystate_values : 50\n", 207 | "-----Parameter dictionary: Parameter types-----\n", 208 | "module_parameters : Np,Ns,Vdcmpp0,Vdcmpp_min,Vdcmpp_max\n", 209 | "inverter_ratings : Srated,Vdcrated,Ioverload,Vrmsrated,Iramp_max_gradient_real,Iramp_max_gradient_imag\n", 210 | "circuit_parameters : Rf_actual,Lf_actual,C_actual,R1_actual,X1_actual,Z1_actual\n", 211 | "controller_gains : Kp_GCC,Ki_GCC,Kp_DC,Ki_DC,Kp_Q,Ki_Q,wp\n", 212 | "steadystate_values : maR0,maI0,iaR0,iaI0\n" 213 | ] 214 | } 215 | ], 216 | "source": [ 217 | "PV_DER1.DER_model.show_parameter_dictionaries()\n", 218 | "PV_DER1.DER_model.show_parameter_types()" 219 | ] 220 | }, 221 | { 222 | "cell_type": "markdown", 223 | "metadata": {}, 224 | "source": [ 225 | "### 2. Get parameter values using get_parameter_values()" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 6, 231 | "metadata": {}, 232 | "outputs": [ 233 | { 234 | "name": "stdout", 235 | "output_type": "stream", 236 | "text": [ 237 | "{ 'C_actual': 0.0003,\n", 238 | " 'Ioverload': 1.3,\n", 239 | " 'Iramp_max_gradient_imag': 1.0,\n", 240 | " 'Iramp_max_gradient_real': 1.0,\n", 241 | " 'Ki_DC': -10.0,\n", 242 | " 'Ki_GCC': 2000.0,\n", 243 | " 'Ki_Q': 10.0,\n", 244 | " 'Kp_DC': -2.0,\n", 245 | " 'Kp_GCC': 6000.0,\n", 246 | " 'Kp_Q': 0.2,\n", 247 | " 'Lf_actual': 2.5e-05,\n", 248 | " 'Np': 11,\n", 249 | " 'Ns': 735,\n", 250 | " 'R1_actual': 0.0019,\n", 251 | " 'Rf_actual': 0.002,\n", 252 | " 'Srated': 50000.0,\n", 253 | " 'Vdcmpp0': 550.0,\n", 254 | " 'Vdcmpp_max': 650.0,\n", 255 | " 'Vdcmpp_min': 525.0,\n", 256 | " 'Vdcrated': 550.0,\n", 257 | " 'Vrmsrated': 146.06528215150922,\n", 258 | " 'X1_actual': 0.0561,\n", 259 | " 'Z1_actual': (0.0019+0.0561j),\n", 260 | " 'iaI0': 0.001,\n", 261 | " 'iaR0': 1.0,\n", 262 | " 'maI0': 0.0,\n", 263 | " 'maR0': 0.89,\n", 264 | " 'wp': 200000.0}\n" 265 | ] 266 | } 267 | ], 268 | "source": [ 269 | "DER_parameters = PV_DER1.DER_model.get_parameter_dictionary(parameter_type='all',parameter_ID='50')" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "### 3. Select name of new parameter dictionary (e.g. 'my_DER') and source parameter ID (e.g. '50').\n", 277 | "### 4. Create and initialize new parameter dictionary using initialize_parameter_dict()." 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": 7, 283 | "metadata": {}, 284 | "outputs": [ 285 | { 286 | "name": "stderr", 287 | "output_type": "stream", 288 | "text": [ 289 | "INFO:pvder_logger:-SolarPVDERThreePhase_1:Created and initialized new parameter dicitonary my_DER with source dictionary 50.\n" 290 | ] 291 | } 292 | ], 293 | "source": [ 294 | "PV_DER1.DER_model.initialize_parameter_dict(parameter_ID='my_DER',source_parameter_ID='50')" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": {}, 300 | "source": [ 301 | "### 5. Update the new parameter dictionary with desired values (e.g. {'Np':5} in 'module_parameters' and {'C_actual':5} in 'circuit_parameters') using update_parameter_dict()." 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": 8, 307 | "metadata": {}, 308 | "outputs": [], 309 | "source": [ 310 | "PV_DER1.DER_model.update_parameter_dict(parameter_ID='my_DER',parameter_type='module_parameters',parameter_dict={'Np':5})\n", 311 | "PV_DER1.DER_model.update_parameter_dict(parameter_ID='my_DER',parameter_type='circuit_parameters',parameter_dict={'C_actual':0.0005})" 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "metadata": {}, 317 | "source": [ 318 | "### 6. Update the PV-DER model with parameters from the new parameter dictionary using modify_DER_parameters()" 319 | ] 320 | }, 321 | { 322 | "cell_type": "code", 323 | "execution_count": 9, 324 | "metadata": {}, 325 | "outputs": [ 326 | { 327 | "name": "stderr", 328 | "output_type": "stream", 329 | "text": [ 330 | "INFO:pvder_logger:-SolarPVDERThreePhase_1:PV-DER parameters updated with parameters fromparameter dictionary my_DER!\n" 331 | ] 332 | }, 333 | { 334 | "name": "stdout", 335 | "output_type": "stream", 336 | "text": [ 337 | "Optimization results:[ 0.60092891 -0.45449525 0.5319794 -0.40595435]\n" 338 | ] 339 | } 340 | ], 341 | "source": [ 342 | "PV_DER1.DER_model.modify_DER_parameters(parameter_ID='my_DER')" 343 | ] 344 | }, 345 | { 346 | "cell_type": "markdown", 347 | "metadata": {}, 348 | "source": [ 349 | "### 7. Save and load parameter dictionaries using save_parameter_dictionary() and load_parameter_dictionary()" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 10, 355 | "metadata": {}, 356 | "outputs": [ 357 | { 358 | "name": "stderr", 359 | "output_type": "stream", 360 | "text": [ 361 | "INFO:pvder_logger:-SolarPVDERThreePhase_1:Saved all the parameter dicitonaries as a pickle file in my_DER.pkl.\n" 362 | ] 363 | } 364 | ], 365 | "source": [ 366 | "file_name = PV_DER1.DER_model.save_parameter_dictionary(parameter_ID='my_DER',save_format='pickle')" 367 | ] 368 | }, 369 | { 370 | "cell_type": "code", 371 | "execution_count": 11, 372 | "metadata": {}, 373 | "outputs": [ 374 | { 375 | "name": "stderr", 376 | "output_type": "stream", 377 | "text": [ 378 | "INFO:pvder_logger:-SolarPVDERThreePhase_1:Created and initialized new parameter dicitonary my_DER with source dictionary 50.\n", 379 | "INFO:pvder_logger:-SolarPVDERThreePhase_1:Succesfully loaded parameters from my_DER.pkl into DER parameter dictionary with parameter ID my_DER.\n" 380 | ] 381 | }, 382 | { 383 | "name": "stdout", 384 | "output_type": "stream", 385 | "text": [ 386 | "Read following dictionary from my_DER.pkl:\n", 387 | "{ 'C_actual': 0.0005,\n", 388 | " 'Ioverload': 1.3,\n", 389 | " 'Iramp_max_gradient_imag': 1.0,\n", 390 | " 'Iramp_max_gradient_real': 1.0,\n", 391 | " 'Ki_DC': -10.0,\n", 392 | " 'Ki_GCC': 2000.0,\n", 393 | " 'Ki_Q': 10.0,\n", 394 | " 'Kp_DC': -2.0,\n", 395 | " 'Kp_GCC': 6000.0,\n", 396 | " 'Kp_Q': 0.2,\n", 397 | " 'Lf_actual': 2.5e-05,\n", 398 | " 'Np': 5,\n", 399 | " 'Ns': 735,\n", 400 | " 'R1_actual': 0.0019,\n", 401 | " 'Rf_actual': 0.002,\n", 402 | " 'Srated': 50000.0,\n", 403 | " 'Vdcmpp0': 550.0,\n", 404 | " 'Vdcmpp_max': 650.0,\n", 405 | " 'Vdcmpp_min': 525.0,\n", 406 | " 'Vdcrated': 550.0,\n", 407 | " 'Vrmsrated': 146.06528215150922,\n", 408 | " 'X1_actual': 0.0561,\n", 409 | " 'Z1_actual': (0.0019+0.0561j),\n", 410 | " 'iaI0': 0.001,\n", 411 | " 'iaR0': 1.0,\n", 412 | " 'maI0': 0.0,\n", 413 | " 'maR0': 0.89,\n", 414 | " 'wp': 200000.0}\n" 415 | ] 416 | } 417 | ], 418 | "source": [ 419 | "_=PV_DER1.DER_model.load_parameter_dictionary(file_name='my_DER.pkl')" 420 | ] 421 | }, 422 | { 423 | "cell_type": "markdown", 424 | "metadata": { 425 | "colab_type": "text", 426 | "id": "wsv8bkkuS5VQ" 427 | }, 428 | "source": [ 429 | "## Specify run time flags, simulation time, and run simulation " 430 | ] 431 | }, 432 | { 433 | "cell_type": "code", 434 | "execution_count": 12, 435 | "metadata": { 436 | "colab": {}, 437 | "colab_type": "code", 438 | "id": "ltNDDII1S5VR", 439 | "scrolled": true 440 | }, 441 | "outputs": [ 442 | { 443 | "name": "stdout", 444 | "output_type": "stream", 445 | "text": [ 446 | "None-sim_1:Simulation started at 0.0 s and will end at 10.0 s\n", 447 | "None-sim_1:Simulation was completed in 00:00:00\n", 448 | "Active power output error:0.2776\n", 449 | "Reactive power output error:0.0002\n", 450 | "Inverter filter active power loss error:0.0000\n", 451 | "Inverter filter reactive power loss error:0.0000\n", 452 | "\n", 453 | "______-SolarPVDERThreePhase_1 - Voltage_____\n", 454 | "Vdc:550.00\n", 455 | "Vta:165.27-124.15j V\n", 456 | "Vtb:-190.84-78.89j V,Vtb:25.63+206.77j V\n", 457 | "Vtn:0.06+3.73j V\n", 458 | "Va:164.78-124.57j V\n", 459 | "Vb:-190.96-78.26j V,Vc:26.24+206.56j V\n", 460 | "Vn:0.06+3.73j V\n", 461 | "Vtrms:146.51 V\n", 462 | "Vpccrms:146.41 V\n", 463 | "\n", 464 | "______-SolarPVDERThreePhase_1 - Current_____\n", 465 | "ia:53.20-40.60j A\n", 466 | "ib:-61.76-25.77j A,ic:8.56+66.37j A\n", 467 | "In:-0.00-0.00j A\n", 468 | "Irms:47.32 V\n", 469 | "\n", 470 | "______-SolarPVDERThreePhase_1 - Power_____\n", 471 | "Ppv:20797.7 W\n", 472 | "S:20797.7+63.3j VA\n", 473 | "S_PCC:20784.2-0.0j VA\n", 474 | "\n", 475 | "______-SolarPVDERThreePhase_1 - Duty cycle_____\n", 476 | "ma:0.60-0.45j\n", 477 | "mb:-0.69-0.29j,mc:0.09+0.75j\n", 478 | "m0:0.00+0.01j\n", 479 | "CPU times: user 1.27 s, sys: 5.61 s, total: 6.88 s\n", 480 | "Wall time: 450 ms\n" 481 | ] 482 | }, 483 | { 484 | "name": "stderr", 485 | "output_type": "stream", 486 | "text": [ 487 | "/home/splathottam/.local/lib/python3.7/site-packages/scipy/integrate/odepack.py:251: ODEintWarning: Integration successful.\n", 488 | " warnings.warn(warning_msg, ODEintWarning)\n" 489 | ] 490 | } 491 | ], 492 | "source": [ 493 | "%%time\n", 494 | "sim1.jacFlag = True\n", 495 | "sim1.DEBUG_SIMULATION = False\n", 496 | "sim1.PER_UNIT = True\n", 497 | "sim1.DEBUG_SOLVER = False\n", 498 | "sim1.tStop = 10.0\n", 499 | "sim1.tInc = 1/120.# 0.001\n", 500 | "sim1.run_simulation()\n", 501 | "PV_DER1.DER_model.validate_model()\n", 502 | "PV_DER1.DER_model.show_PV_DER_states('voltage')\n", 503 | "PV_DER1.DER_model.show_PV_DER_states('current')\n", 504 | "PV_DER1.DER_model.show_PV_DER_states('power')\n", 505 | "PV_DER1.DER_model.show_PV_DER_states('duty cycle')" 506 | ] 507 | }, 508 | { 509 | "cell_type": "markdown", 510 | "metadata": {}, 511 | "source": [ 512 | "### Get trajectories for current, voltage, and power as a dictionary" 513 | ] 514 | }, 515 | { 516 | "cell_type": "code", 517 | "execution_count": 13, 518 | "metadata": {}, 519 | "outputs": [], 520 | "source": [ 521 | "trajectories = sim1.get_trajectories()" 522 | ] 523 | } 524 | ], 525 | "metadata": { 526 | "colab": { 527 | "include_colab_link": true, 528 | "name": "PV-DER_usage_example.ipynb", 529 | "provenance": [], 530 | "version": "0.3.2" 531 | }, 532 | "kernelspec": { 533 | "display_name": "pvder", 534 | "language": "python", 535 | "name": "pvder" 536 | }, 537 | "language_info": { 538 | "codemirror_mode": { 539 | "name": "ipython", 540 | "version": 3 541 | }, 542 | "file_extension": ".py", 543 | "mimetype": "text/x-python", 544 | "name": "python", 545 | "nbconvert_exporter": "python", 546 | "pygments_lexer": "ipython3", 547 | "version": "3.7.10" 548 | } 549 | }, 550 | "nbformat": 4, 551 | "nbformat_minor": 4 552 | } 553 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /pvder/DER_components.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division 3 | """ 4 | Created on Wed Mar 25 09:36:27 2020 5 | 6 | @author: splathottam 7 | """ 8 | 9 | """PV-DER base class.""" 10 | 11 | import os 12 | import math 13 | import pdb 14 | 15 | import numpy as np 16 | import scipy 17 | from scipy.optimize import fsolve, minimize 18 | 19 | from pvder.DER_check_and_initialize import PVDER_SetupUtilities 20 | from pvder.DER_features import PVDER_SmartFeatures 21 | from pvder.DER_utilities import PVDER_ModelUtilities 22 | from pvder.grid_components import BaseValues 23 | from pvder import utility_functions 24 | from pvder import defaults,templates,specifications 25 | from pvder.logutil import LogUtil 26 | 27 | 28 | class SolarPVDER(PVDER_SetupUtilities,PVDER_SmartFeatures,PVDER_ModelUtilities,BaseValues): 29 | """ 30 | Class for describing a Solar Photo-voltaic Distributed Energy Resource consisting of panel, converters, and 31 | control systems. 32 | 33 | Attributes: 34 | count (int): Number of instances of `SolarPV_DER_SinglePhase`. 35 | n_ODE (int): Number of ODE's. 36 | 37 | """ 38 | 39 | #PLL controller parameters 40 | Kp_PLL = 180 #1800 41 | Ki_PLL = 320 #32000 42 | 43 | winv = we = 2.0*math.pi*60.0 #Frequency of fundamental waveform 44 | fswitching = 10e3 #Inverter switching frequency (not used by model) 45 | 46 | def setup_DER(self,events,configFile,derID,**kwargs): 47 | """Setup pvder instance""" 48 | try: 49 | self.events = events 50 | self.DER_model_type = type(self).__name__ 51 | DER_config,DER_parent_config,config_dict = self.create_DER_config(configFile,derID) 52 | 53 | DER_arguments = self.get_DER_arguments(DER_config,**kwargs) 54 | 55 | self.name_instance(DER_arguments['identifier']) #Generate a name for the instance 56 | self.initialize_logger(DER_arguments['verbosity']) #Set logging level - {DEBUG,INFO,WARNING,ERROR} 57 | 58 | self.check_model_type(DER_config,DER_parent_config) 59 | self.update_DER_config(DER_config,DER_parent_config,DER_arguments,self.parameter_ID,self.DER_parent_ID) #DER_arguments #No more updating DER parameters from DER arguments 60 | self.check_basic_specs() 61 | 62 | self.RT_config = {} 63 | self.update_RT_config(config_dict) #Checks and updates RT_config if any entries are missing 64 | self.check_RT_config() 65 | 66 | return DER_arguments 67 | except: 68 | LogUtil.exception_handler() 69 | 70 | def create_DER_config(self,configFile,derID): 71 | """Create a valid DER configuration.""" 72 | try: 73 | self.parameter_ID = derID #DER parameter id must be passed as an argument 74 | self.create_template() 75 | DER_config,config_dict = self.get_DER_config(configFile,self.parameter_ID) 76 | self.DER_parent_ID = self.get_DER_parent_id(DER_config) 77 | DER_parent_config = self.get_DER_parent_config(configFile,self.DER_parent_ID) 78 | DER_config, DER_parent_config = self.get_completed_DER_config(DER_config,DER_parent_config) 79 | DER_config = self.update_DER_config_specs(DER_config,DER_parent_config) 80 | return DER_config,DER_parent_config,config_dict 81 | except: 82 | LogUtil.exception_handler() 83 | 84 | def get_DER_parent_id(self,DER_config): 85 | """Check if user has specified a parent configuration.""" 86 | try: 87 | if 'parent_config' in DER_config: 88 | DER_parent_id = DER_config['parent_config'] 89 | else: 90 | DER_parent_id = '' 91 | 92 | return DER_parent_id 93 | except: 94 | LogUtil.exception_handler() 95 | 96 | def get_DER_parent_config(self,configFile,derParentId): 97 | """Check if user has specified a parent configuration.""" 98 | try: 99 | if derParentId: 100 | LogUtil.logger.log(20,'{}:Reading parent DER config:{} for DER config:{}'.format(self.name,derParentId,self.parameter_ID)) 101 | DER_parent_config,_ = self.get_DER_config(configFile,derParentId) 102 | else: 103 | DER_parent_config = {} 104 | 105 | return DER_parent_config 106 | except: 107 | LogUtil.exception_handler() 108 | 109 | def get_DER_config(self,configFile,derId): 110 | """Check DER ID in config file.""" 111 | try: 112 | config_dict = self.read_config(configFile) #Read configuration dictionary 113 | 114 | available_ids = list(config_dict.keys()) 115 | if derId in available_ids: 116 | LogUtil.logger.info('DER configuration with ID:{} was found in {}'.format(derId,configFile)) 117 | else: 118 | LogUtil.logger.error('DER configuration with ID:{} could not be found in {}! - Available IDs are:{}'.format(derId,configFile,available_ids)) 119 | return config_dict[derId],config_dict 120 | except: 121 | LogUtil.exception_handler() 122 | 123 | def update_DER_config_specs(self,DER_config,DER_parent_config): 124 | """Check if user has specified a parent configuration.""" 125 | try: 126 | spec_config = {"basic_specs":["model_type"],"inverter_ratings":["Srated","Vrmsrated"]} 127 | for spec_type in list(spec_config.keys()): 128 | for spec in spec_config[spec_type]: 129 | if spec_type in DER_config and spec in DER_config[spec_type]: 130 | pass 131 | elif spec_type in DER_parent_config and spec in DER_parent_config[spec_type]: 132 | if spec_type in DER_config: 133 | DER_config[spec_type].update({spec:DER_parent_config[spec_type][spec]}) 134 | else: 135 | DER_config.update({spec_type:{spec:DER_parent_config[spec_type][spec]}}) 136 | else: 137 | raise ValueError(f"{spec_type}-{spec} should be available either in DER config or DER parent config!") 138 | return DER_config 139 | except: 140 | LogUtil.exception_handler() 141 | 142 | def get_completed_DER_config(self,DER_config,DER_parent_config): 143 | """Completes missing elements in DER config compared to to DER design template""" 144 | 145 | for DER_component in self.DER_design_template: 146 | if DER_component not in DER_config: 147 | DER_config.update({DER_component:{}}) 148 | if DER_component not in DER_parent_config: 149 | DER_parent_config.update({DER_component:{}}) 150 | 151 | return DER_config, DER_parent_config 152 | 153 | def read_config(self,configFile): 154 | """Load config json file and return dictionary.""" 155 | try: 156 | LogUtil.logger.log(10,'Reading configuration file:{}'.format(configFile)) 157 | confDict = utility_functions.read_json(configFile) 158 | 159 | return confDict 160 | except: 161 | LogUtil.exception_handler() 162 | 163 | def create_template(self): 164 | """Create templates for DER model.""" 165 | try: 166 | self.DER_design_template = templates.DER_design_template[self.DER_model_type] 167 | self.DER_config = dict((key, {}) for key in self.DER_design_template.keys()) 168 | except: 169 | LogUtil.exception_handler() 170 | 171 | def get_DER_arguments(self,DER_config,**kwargs): 172 | """Initialize flags""" 173 | try: 174 | DER_arguments = {} 175 | found_arguments = kwargs.keys() #Arguments which were passed 176 | used_arguments =[] 177 | 178 | for key, value in specifications.DER_argument_spec .items(): 179 | 180 | if key in kwargs.keys(): 181 | if isinstance(kwargs[key],specifications.DER_argument_spec [key]['type']): 182 | DER_arguments.update({key:kwargs[key]}) 183 | used_arguments.append(key) 184 | else: 185 | LogUtil.logger.log(40,'Found {} to have type:{} - Valid type:{}'.format(key,type(kwargs[key]),specifications.DER_argument_spec [key]['type'])) 186 | elif key in DER_config: #Check if key available in config file 187 | if isinstance(DER_config[key],specifications.DER_argument_spec[key]['type']): 188 | DER_arguments.update({key:DER_config[key]}) 189 | LogUtil.logger.info('Updated DER argument {} from DER_config'.format(key)) 190 | else: 191 | LogUtil.logger.info('Found {} to have type:{} - Valid type:{}'.format(key,type(DER_config[key]),specifications.DER_argument_spec [key]['type'])) 192 | elif key in specifications.DER_argument_spec: 193 | if specifications.DER_argument_spec [key]['default_value'] is not None: 194 | DER_arguments.update({key:specifications.DER_argument_spec [key]['default_value']}) 195 | 196 | LogUtil.logger.log(10,'Used arguments:{} and Invalid arguments:{}'.format(\ 197 | used_arguments,list(set(found_arguments).difference(set(used_arguments))))) 198 | 199 | #if 'powerRating' in found_arguments: 200 | # DER_arguments.update({'Srated':kwargs['powerRating']}) #power rating can only be supplied through config file 201 | 202 | #if 'VrmsRating' in found_arguments: 203 | # DER_arguments.update({'Vrmsrated':kwargs['VrmsRating']}) #Vrms rating can only be supplied through config file 204 | 205 | return DER_arguments 206 | except: 207 | LogUtil.exception_handler() 208 | 209 | def initialize_DER(self,DER_arguments): 210 | """Initialize flags""" 211 | try: 212 | self.initialize_basic_specs() 213 | self.initialize_basic_options() 214 | 215 | self.initialize_flags(DER_arguments) 216 | self.attach_grid_model(DER_arguments) 217 | 218 | self.initialize_grid_measurements(DER_arguments) 219 | self.initialize_DER_model() #DER model parameters 220 | self.RT_initialize() #VRT and FRT settings 221 | 222 | self.initialize_jacobian() 223 | 224 | self.update_Qref(t=0.0) #Reference 225 | 226 | self.initialize_states(DER_arguments) #initialize_states 227 | 228 | self.initialize_derived_quantities() 229 | self.initialize_Volt_VAR() #Volt-VAR settings 230 | 231 | self.reset_reference_counters() 232 | 233 | if self.standAlone: 234 | self._vag_previous = self.grid_model.vag 235 | self._va_previous = self.va 236 | except: 237 | LogUtil.exception_handler() 238 | 239 | def initialize_flags(self,DER_arguments): 240 | """Initialize flags""" 241 | try: 242 | self.standAlone = DER_arguments['standAlone'] 243 | self.steady_state_initialization = DER_arguments['steadyStateInitialization'] 244 | self.allow_unbalanced_m = DER_arguments['allowUnbalancedM'] 245 | except: 246 | LogUtil.exception_handler() 247 | 248 | def check_model_type(self,DER_config,DER_parent_config): 249 | """Check basic specs in configuration.""" 250 | try: 251 | if 'model_type' in DER_config['basic_specs']: 252 | DER_model_type_in_config = DER_config['basic_specs']['model_type'] 253 | elif 'model_type' in DER_parent_config['basic_specs']: 254 | DER_model_type_in_config = DER_parent_config['basic_specs']['model_type'] 255 | else: 256 | LogUtil.logger.log(40,'{}:Model type was not found for parameter ID {}!'.format(self.name,self.parameter_ID)) 257 | 258 | if not self.DER_model_type == DER_model_type_in_config: 259 | LogUtil.logger.log(40,'{}:DER configuration with ID:{} is defined to be used with model type {} but is being used with model {}!'.format(self.name,self.parameter_ID,DER_model_type_in_config,self.DER_model_type)) 260 | except: 261 | LogUtil.exception_handler() 262 | 263 | def check_basic_specs(self): 264 | """Check basic specs in DER config.""" 265 | try: 266 | if self.DER_model_type in templates.DER_design_template: 267 | 268 | n_phases = self.DER_config['basic_specs']['n_phases'] 269 | if not n_phases == templates.DER_design_template[self.DER_model_type]['basic_specs']['n_phases']: 270 | LogUtil.logger.log(40,'{}:DER configuration with ID:{} has {} phases which is invalid for {} DER model!'.format(self.name,self.parameter_ID,n_phases,self.DER_model_type)) 271 | 272 | if not n_phases == len(templates.DER_design_template[self.DER_model_type]['basic_specs']['phases']): 273 | LogUtil.logger.log(40,'{}:DER configuration with ID:{} has {} phases buf following phases were found {}!'.format(self.name,self.parameter_ID,n_phases,len(templates.DER_design_template[self.DER_model_type]['basic_specs']['phases']))) 274 | 275 | n_ODE = self.DER_config['basic_specs']['n_ODE'] 276 | if not n_ODE == templates.DER_design_template[self.DER_model_type]['basic_specs']['n_ODE']: 277 | LogUtil.logger.log(40,'{}:DER configuration with ID:{} has {} ODE equations which is invalid for {} DER model!'.format(self.name,self.parameter_ID,n_ODE,self.DER_model_type)) 278 | 279 | if not n_ODE == len(templates.DER_design_template[self.DER_model_type]['initial_states']): 280 | LogUtil.logger.log(40,'{}:DER configuration with ID:{} needs {} states, but only {} states were found for {} DER model!'. 281 | format(self.name,self.parameter_ID,n_ODE,len(templates.DER_design_template[self.DER_model_type]['initial_states']),self.DER_model_type)) 282 | 283 | else: 284 | LogUtil.logger.log(40,'{}:{} is an invalid DER model class'.format(self.name,self.DER_model_type)) 285 | except: 286 | LogUtil.exception_handler() 287 | 288 | class PVModule(object): 289 | """ 290 | Class for describing PV module. 291 | 292 | Attributes: 293 | Iph (float):Photocurrent from a single cell. 294 | Ipv (float): PV module current. 295 | 296 | """ 297 | 298 | #Select either NN or polyfit model for MPP 299 | USE_POLYNOMIAL_MPP = True 300 | _MPP_fit_points = 50 301 | _Tactual_min = 273.15 #0 degree celsius 302 | _S_min = 10.0 303 | _Tactual_max = _Tactual_min + 35.0 #35 degree celsius 304 | _S_max = 100.0 305 | 306 | Iscr = 8.03 #Cell short-circuit current at reference temperature and radiation 307 | Kv = 0.0017 #Short-circuit current temperature co-efficient 308 | T0 = 273.15 + 25.0 #Cell reference temperature in Kelvin 309 | Irs = 1.2e-7 #Cell reverse saturation current 310 | q = 1.602e-19 #Charge of an electron 311 | k = 1.38e-23 #Boltzmann's constant 312 | A = 1.92 #p-n junction ideality factor 313 | 314 | """ 315 | module_parameters = {'1':{'Np':2,'Ns':500,'Vdcmpp0':250.0,'Vdcmpp_min': 225.0,'Vdcmpp_max': 300.0}, 316 | '10':{'Np':2,'Ns':1000,'Vdcmpp0':750.0,'Vdcmpp_min': 650.0,'Vdcmpp_max': 800.0}, 317 | '50':{'Np':11,'Ns':735,'Vdcmpp0':550.0,'Vdcmpp_min': 520.0,'Vdcmpp_max': 650.0}, 318 | '250':{'Np':45,'Ns':1000,'Vdcmpp0':750.0,'Vdcmpp_min': 750.0,'Vdcmpp_max': 1000.0}} 319 | 320 | module_parameters_list = module_parameters.keys() 321 | """ 322 | def __init__(self,Sinsol): 323 | """Creates an instance of `PV_Module`. 324 | Args: 325 | Sinsol (float): Solar insolation in percentage. 326 | """ 327 | try: 328 | super(PVModule,self).__init__() 329 | self.initialize_module_parameters() 330 | self.Sinsol = Sinsol #PV module initial conditions 331 | self.Tactual = defaults.Tactual 332 | 333 | #Fit polynomial 334 | if self.MPPT_ENABLE and self.USE_POLYNOMIAL_MPP: 335 | self.fit_MPP_poly() 336 | self.Iph = self.Iph_calc() 337 | except: 338 | raise 339 | 340 | @property 341 | def Vdcmpp(self): 342 | """Voltage at maximum power point for given insolation and temperature""" 343 | try: 344 | #sol, = fsolve(lambda x: 2.5 - np.sqrt(x), 8) 345 | self.Iph = self.Iph_calc() #This function uses solar insolation 346 | 347 | return fsolve(lambda Vdc0:-((self.Np*self.Irs*(scipy.exp((self.q*Vdc0)/(self.k*self.Tactual*self.A*self.Ns))))*(self.q/(self.k*self.Tactual*self.A*self.Ns))*Vdc0)-((self.Np*self.Irs*(scipy.exp((self.q*Vdc0)/(self.k*self.Tactual*self.A*self.Ns))-1)))\ 348 | +(self.Np*self.Iph),self.Vdcmpp0)[0] #This is a time consuming operation 349 | except: 350 | LogUtil.exception_handler() 351 | 352 | def Iph_calc(self): 353 | """Photocurrent from a single cell for given insolation and temperature.""" 354 | try: 355 | return (self.Iscr+(self.Kv*(self.Tactual-self.T0)))*(self.Sinsol/100.0) 356 | except: 357 | LogUtil.exception_handler() 358 | 359 | def Ppv_calc(self,Vdc_actual): 360 | """PV panel power output from solar insolation. 361 | 362 | Args: 363 | Vdc_actual (float): DC link voltage in volts 364 | 365 | Returns: 366 | float: Power output from PV module in p.u. 367 | """ 368 | try: 369 | self.Iph = self.Iph_calc() 370 | self.Ipv = (self.Np*self.Iph)-(self.Np*self.Irs*(math.exp((self.q*Vdc_actual)/(self.k*self.Tactual*self.A*self.Ns))-1)) #Faster with Pure Python functions 371 | 372 | return max(0,(self.Ipv*Vdc_actual))/BaseValues.Sbase 373 | #return utility_functions.Ppv_calc(self.Iph,self.Np,self.Ns,Vdc_actual,self.Tactual,Grid.Sbase) 374 | except: 375 | LogUtil.exception_handler() 376 | 377 | def fit_MPP_poly(self): 378 | """Method to fit MPP to a polynomial function.""" 379 | try: 380 | self.Tactual = 298.15 #Use constant temperature 381 | self._MPP_fit_points = 10 382 | Srange = np.linspace(self._S_min,self._S_max,self._MPP_fit_points+1) 383 | Vdcmpp_list = [] 384 | Ppvmpp_list =[] 385 | Sinsol_list= [] 386 | LogUtil.logger.log(20,'Calculating {} values for MPP polynomial fit!'.format(len(Srange))) 387 | for S in Srange: 388 | self.Sinsol = S 389 | Vdcmpp = self.Vdcmpp 390 | Ppvmpp = self.Ppv_calc(self.Vdcmpp)*BaseValues.Sbase 391 | 392 | Sinsol_list.append(S) 393 | Vdcmpp_list.append(Vdcmpp) 394 | Ppvmpp_list.append(Ppvmpp) 395 | 396 | x = np.array(Sinsol_list) 397 | y = np.array(Vdcmpp_list) 398 | self.z = np.polyfit(x, y, 3) 399 | LogUtil.logger.log(20,'Found polynomial for MPP :{:.4f}x^3 + {:.4f}x^2 +{:.4f}x^1 + {:.4f}!'.format(self.z[0],self.z[1],self.z[2], self.z[3])) 400 | except: 401 | LogUtil.exception_handler() 402 | 403 | def initialize_module_parameters(self): 404 | """Initialize PV module parameters.""" 405 | try: 406 | if self.parameter_ID in self.module_parameters: 407 | 408 | self.Np = self.module_parameters[self.parameter_ID]['Np'] 409 | self.Ns = self.module_parameters[self.parameter_ID]['Ns'] 410 | self.Vdcmpp0 = self.module_parameters[self.parameter_ID]['Vdcmpp0'] 411 | self.Vdcmpp_min = self.module_parameters[self.parameter_ID]['Vdcmpp_min'] 412 | self.Vdcmpp_max = self.module_parameters[self.parameter_ID]['Vdcmpp_max'] 413 | else: 414 | LogUtil.logger.log(40,'PV module parameters not available for parameter ID {} '.format(self.parameter_ID)) 415 | except: 416 | LogUtil.exception_handler() 417 | 418 | 419 | -------------------------------------------------------------------------------- /pvder/DER_components_single_phase.py: -------------------------------------------------------------------------------- 1 | """Single phase PV-DER code.""" 2 | 3 | from __future__ import division 4 | 5 | import six 6 | import pdb 7 | import warnings 8 | 9 | import numpy as np 10 | import math 11 | import cmath 12 | import scipy 13 | from scipy.optimize import fsolve, minimize 14 | 15 | from pvder.DER_components import SolarPVDER,PVModule 16 | 17 | from pvder import utility_functions 18 | from pvder import defaults,templates 19 | from pvder.logutil import LogUtil 20 | 21 | 22 | class SolarPVDERSinglePhase(PVModule,SolarPVDER): 23 | """ 24 | Class for describing a Solar Photo-voltaic Distributed Energy Resource consisting of panel, converters, and 25 | control systems. 26 | 27 | Attributes: 28 | count (int): Number of instances of `SolarPVDERSinglePhase`. 29 | n_ODE (int): Number of ODE's. 30 | """ 31 | 32 | count = 0 33 | 34 | def __init__(self,events,configFile,derId,**kwargs): 35 | """Creates an instance of `SolarPV_DER_SinglePhase`. 36 | Args: 37 | events (SimulationEvents): An instance of `SimulationEvents`. 38 | gridModel (Grid): An instance of `Grid`(only need to be suppled for stand alone simulation). 39 | powerRating (float): A scalar specifying the rated power (VA) of the DER. 40 | VrmsRating (float): A scalar specifying the rated RMS L-G voltage (V) of the DER. 41 | ia0,xa0,ua0 (complex): Initial value of inverter states in p.u. of the DER instance. 42 | xDC0,xQ0,xPLL0,wte0 (float): Initial value of inverter states in the DER instance. 43 | gridVoltatePhaseA,gridVoltatePhaseA,gridVoltatePhaseA (float): Initial voltage phasor (V) at PCC - LV side from external program (only need to be suppled if model is not stand alone). 44 | standAlone (bool): Specify if the DER instance is a stand alone simulation or part of a larger simulation. 45 | steadyStateInitialization (bool): Specify whether states in the DER instance will be initialized to steady state values. 46 | allowUnbalancedM (bool): Allow duty cycles to take on unbalanced values during initialization (default: False). 47 | derConfig (dict): Configuration parameters that may be supplied from an external program. 48 | identifier (str): An identifier that can be used to name the instance (default: None). 49 | 50 | Raises: 51 | ValueError: If parameters corresponding to `Sinverter_rated` are not available. 52 | ValueError: If rated DC link voltage is not sufficient. 53 | """ 54 | try: 55 | SolarPVDERSinglePhase.count = SolarPVDERSinglePhase.count+1 #Increment count to keep track of number of PV-DER model instances 56 | DER_arguments = self.setup_DER(events,configFile,derId,**kwargs) 57 | super().__init__(self.DER_config['basic_options']['Sinsol']) #Initialize PV module class (base class) 58 | 59 | self.initialize_DER(DER_arguments) 60 | self.creation_message() 61 | except: 62 | LogUtil.exception_handler() 63 | 64 | @property#Decorator used for auto updating 65 | def y0(self): 66 | """List of initial states""" 67 | try: 68 | return[self.ia.real, self.ia.imag, self.xa.real, self.xa.imag, self.ua.real,self.ua.imag,\ 69 | self.Vdc,self.xDC,self.xQ,self.xPLL,self.wte] 70 | except: 71 | LogUtil.exception_handler() 72 | 73 | #Apparent power output at inverter terminal 74 | def S_calc(self): 75 | """Inverter apparent power output""" 76 | try: 77 | return (1/2)*(self.vta*self.ia.conjugate())*1.0 78 | #return utility_functions.S_calc(self.vta,self.vtb,self.vtc,self.ia,self.ib,self.ic) 79 | except: 80 | LogUtil.exception_handler() 81 | 82 | #Apparent power output at PCC - LV side 83 | def S_PCC_calc(self): 84 | """Power output at PCC LV side""" 85 | try: 86 | return (1/2)*(self.va*self.ia.conjugate()) 87 | #return utility_functions.S_calc(self.va,self.vb,self.vc,self.ia,self.ib,self.ic) 88 | except: 89 | LogUtil.exception_handler() 90 | 91 | def S_load1_calc(self): 92 | """Power absorbed by load at PCC LV side.""" 93 | try: 94 | return (1/2)*(self.va*(-(self.va/self.Zload1)).conjugate()) 95 | except: 96 | LogUtil.exception_handler() 97 | 98 | def S_G_calc(self): 99 | """Power absorbed/produced by grid voltage source.""" 100 | try: 101 | return (1/2)*(-(self.ia-(self.va/self.Zload1))/self.a).conjugate()*self.grid_model.vag 102 | except: 103 | LogUtil.exception_handler() 104 | 105 | #@property 106 | def Vtrms_calc(self): 107 | """Inverter terminal voltage -RMS""" 108 | try: 109 | return utility_functions.Urms_calc(self.vta,self.vta,self.vta) 110 | except: 111 | LogUtil.exception_handler() 112 | 113 | def Vrms_calc(self): 114 | """PCC LV side voltage - RMS""" 115 | try: 116 | return utility_functions.Urms_calc(self.va,self.va,self.va) 117 | except: 118 | LogUtil.exception_handler() 119 | 120 | def Irms_calc(self): 121 | """Inverter current - RMS""" 122 | try: 123 | return utility_functions.Urms_calc(self.ia,self.ia,self.ia) 124 | except: 125 | LogUtil.exception_handler() 126 | 127 | def Vabrms_calc(self): 128 | """PCC LV side voltage - line to lineRMS""" 129 | try: 130 | return abs(self.va-self.vb)/math.sqrt(2) 131 | except: 132 | LogUtil.exception_handler() 133 | 134 | def update_inverter_states(self,ia,xa,ua,Vdc,xDC,xQ,xPLL,wte): 135 | """Update inverter states 136 | Args: 137 | ia (complex): Inverter phase a current. 138 | xa (complex): Inverter controller state. 139 | ua (complex): Inverter controller state. 140 | Vdc (float): DC link voltage. 141 | """ 142 | try: 143 | self.ia = ia 144 | self.xa = xa 145 | self.ua = ua 146 | 147 | self.Vdc = Vdc 148 | self.xDC = xDC 149 | self.xQ = xQ 150 | 151 | self.xPLL = xPLL 152 | self.wte = wte 153 | except: 154 | LogUtil.exception_handler() 155 | 156 | def update_voltages(self): 157 | """Update voltages.""" 158 | try: 159 | #Update inverter terminal voltage 160 | self.vta = self.vta_calc() 161 | 162 | #Update PCC LV side voltage 163 | self.va = self.va_calc() 164 | except: 165 | LogUtil.exception_handler() 166 | 167 | def update_RMS(self): 168 | """Update RMS voltages.""" 169 | try: 170 | self.Vtrms = self.Vtrms_calc() 171 | self.Vrms_min = self.Vrms = self.Vrms_calc() 172 | 173 | self.Irms = self.Irms_calc() 174 | 175 | #Update RMS values 176 | if self.DO_EXTRA_CALCULATIONS: 177 | pass 178 | except: 179 | LogUtil.exception_handler() 180 | 181 | def update_power(self): 182 | """Update RMS voltages.""" 183 | try: 184 | #Update power output 185 | self.S = self.S_calc() 186 | self.S_PCC = self.S_PCC_calc() 187 | 188 | if self.standAlone:#Update load current and grid voltage source power only in stand alone mode 189 | self.iaload1 = self.iphload1_calc(self.va) 190 | self.S_G = self.S_G_calc() 191 | self.S_load1 = self.S_load1_calc() 192 | except: 193 | LogUtil.exception_handler() 194 | 195 | def update_iref(self,t): 196 | """Update reference reference current.""" 197 | try: 198 | #Get current controller setpoint 199 | if self.current_gradient_limiter: 200 | self.ia_ref = self.get_ramp_limited_iref(t,self.ia_ref_calc()) 201 | else: 202 | self.ia_ref = self.ia_ref_calc() 203 | except: 204 | LogUtil.exception_handler() 205 | 206 | def update_inverter_frequency(self,t): 207 | """Update inverter PLL frequency. 208 | Args: 209 | t (float): Simulation time in seconds. 210 | """ 211 | try: 212 | #Update grid frequency 213 | self.wgrid_measured = self.wgrid_calc(t) 214 | 215 | #Convert PCC LV side voltage from phasor to alpha-beta domain 216 | self.valpha = utility_functions.phasor_to_time_1phase(self.va,w=self.wgrid_measured,t=t) 217 | self.vbeta =utility_functions.phasor_to_time_1phase(self.va*pow(math.e,-1j*(math.pi/2)),w=self.wgrid_measured,t=t) 218 | 219 | #Convert from alpha-beta domain to d-q domain using Parks transformation 220 | self.vd,self.vq = utility_functions.alpha_beta_to_d_q(self.valpha,self.vbeta,self.wte) 221 | 222 | #Calculate inverter frequency from PLL equation 223 | self.we = self.we_calc() 224 | self.winv = self.we 225 | except: 226 | LogUtil.exception_handler() 227 | 228 | def ODE_model(self,y,t): 229 | """System of ODE's defining the dynamic DER model. 230 | Args: 231 | y (list of float): Initial conditions for the states.. 232 | t (float): Simulation time in seconds. 233 | 234 | Returns: 235 | result (list of float): Derivates for the system of ODE's. 236 | """ 237 | try: 238 | iaR, iaI, xaR, xaI, uaR, uaI,\ 239 | Vdc, xDC, xQ, xPLL, wte = y # unpack current values of y 240 | 241 | self.update_inverter_states(iaR + 1j*iaI, xaR + 1j*xaI,uaR + 1j*uaI,\ 242 | Vdc,xDC,xQ,\ 243 | xPLL,wte) 244 | 245 | self.update_Ppv(t) 246 | self.update_Zload1(t) 247 | 248 | self.update_voltages() 249 | self.update_power() 250 | self.update_RMS() 251 | 252 | self.update_Qref(t) 253 | self.update_Vdc_ref(t) 254 | self.update_iref(t) 255 | 256 | self.update_inverter_frequency(t) 257 | 258 | self.update_ridethrough_flags(t) 259 | self.disconnect_or_reconnect(t) 260 | 261 | #Phase a inverter output current 262 | diaR = (1/self.Lf)*(-self.Rf*self.ia.real - self.va.real + self.vta.real) + (self.winv/self.wbase)*self.ia.imag 263 | diaI = (1/self.Lf)*(-self.Rf*self.ia.imag - self.va.imag + self.vta.imag) - (self.winv/self.wbase)*self.ia.real 264 | 265 | #Current controller dynamics 266 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 267 | if np.sign(self.Ki_GCC*self.ua.real) == np.sign(self.xa.real): 268 | dxaR = 0.0 269 | else: 270 | dxaR = self.Ki_GCC*self.ua.real 271 | if np.sign(self.Ki_GCC*self.ua.imag) == np.sign(self.xa.imag): 272 | dxaI = 0.0 273 | else: 274 | dxaI = self.Ki_GCC*self.ua.imag 275 | #six.print_(dxaR+1j*dxaI,np.sign(self.Ki_GCC*self.ua)) 276 | else: 277 | dxaR = self.Ki_GCC*self.ua.real 278 | dxaI = self.Ki_GCC*self.ua.imag 279 | 280 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 281 | if np.sign( (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real)) == np.sign(self.ua.real): 282 | duaR = 0.0 283 | else: 284 | duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 285 | 286 | if np.sign((self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag)) == np.sign(self.ua.imag): 287 | duaI = 0.0 288 | else: 289 | duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 290 | 291 | else: 292 | duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 293 | duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 294 | 295 | #DC link voltage dynamics 296 | dVdc = (self.Ppv - self.S.real)/(self.Vdc*self.C) 297 | 298 | if abs(self.xDC + self.Kp_DC*(self.Vdc_ref - self.Vdc) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 299 | if np.sign(self.Ki_DC*(self.Vdc_ref - self.Vdc)) == np.sign(self.xDC): 300 | dxDC = 0.0 301 | else: 302 | dxDC = self.Ki_DC*(self.Vdc_ref - self.Vdc) 303 | else: 304 | dxDC = self.Ki_DC*(self.Vdc_ref - self.Vdc) 305 | 306 | # Reactive power controller dynamics 307 | if abs(self.xDC + self.Kp_DC*(self.Vdc_ref - self.Vdc) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 308 | 309 | if np.sign(-self.Ki_Q*(self.Q_ref - self.S_PCC.imag)) == np.sign(self.xQ): 310 | dxQ = 0.0 311 | else: 312 | dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 313 | else: 314 | dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 315 | 316 | #SRF-PLL dynamics 317 | dxPLL = self.Ki_PLL*(self.vd) 318 | 319 | #Frequency integration to get angle 320 | dwte = self.we 321 | 322 | result = [ diaR,# list of dy/dt=f functions 323 | diaI, 324 | dxaR, 325 | dxaI, 326 | duaR, 327 | duaI, 328 | dVdc, 329 | dxDC, 330 | dxQ, 331 | dxPLL, 332 | dwte] 333 | 334 | return np.array(result) 335 | except: 336 | LogUtil.exception_handler() 337 | 338 | def jac_ODE_model(self,y,t): 339 | """Jacobian for the system of ODE's. 340 | Args: 341 | y (list of float): Initial conditions for the states.. 342 | t (float): Simulation time in seconds. 343 | 344 | Returns: 345 | result (array of float): An array containing the elements of the Jacobian. 346 | """ 347 | try: 348 | iaR, iaI, xaR, xaI, uaR, uaI,\ 349 | Vdc, xDC, xQ, xPLL, wte = y # unpack current values of y 350 | 351 | self.update_inverter_states(iaR + 1j*iaI, xaR + 1j*xaI,uaR + 1j*uaI,\ 352 | Vdc,xDC,xQ,\ 353 | xPLL,wte) 354 | 355 | J = self.J 356 | varInd = self.varInd 357 | self.update_Ppv(t) 358 | #self.update_Zload1(t) 359 | 360 | self.update_voltages() 361 | self.update_power() 362 | self.update_RMS() 363 | 364 | self.update_Qref(t) 365 | self.update_Vdc_ref(t) 366 | self.update_iref(t) 367 | 368 | #d-q transformation 369 | self.update_inverter_frequency(t) 370 | 371 | self.update_ridethrough_flags(t) 372 | self.disconnect_or_reconnect(t) 373 | #Phase a inverter output current 374 | 375 | ra,theta_a = cmath.polar(self.va) 376 | 377 | theta_a = self.wgrid_measured*t + theta_a - math.pi/2 378 | 379 | J[varInd['iaR'],varInd['iaR']] = -self.Rf/self.Lf 380 | J[varInd['iaR'],varInd['iaI']] = (self.xPLL+self.Kp_PLL*self.vd+2*math.pi*60)/self.wbase 381 | J[varInd['iaR'],varInd['xaR']] = self.Vdc/(2*self.Lf) 382 | J[varInd['iaR'],varInd['uaR']] = (self.Vdc*self.Kp_GCC)/(2*self.Lf) 383 | J[varInd['iaR'],varInd['Vdc']] = (self.xa.real+self.ua.real*self.Kp_GCC)/(2*self.Lf) 384 | J[varInd['iaR'],varInd['xPLL']] = self.ia.imag/self.wbase 385 | J[varInd['iaR'],varInd['wte']] = ((self.Kp_PLL*self.ia.imag*ra)/self.wbase)*(-math.cos(theta_a)*math.sin(self.wte) 386 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 387 | 388 | J[varInd['iaI'],varInd['iaR']]= -(self.xPLL+self.Kp_PLL*self.vd+2*math.pi*60)/self.wbase 389 | J[varInd['iaI'],varInd['iaI']]= -self.Rf/self.Lf 390 | J[varInd['iaI'],varInd['xaI']]= self.Vdc/(2*self.Lf) 391 | J[varInd['iaI'],varInd['uaI']]= (self.Vdc*self.Kp_GCC)/(2*self.Lf) 392 | J[varInd['iaI'],varInd['Vdc']]= (self.xa.imag+self.ua.imag*self.Kp_GCC)/(2*self.Lf) 393 | J[varInd['iaI'],varInd['xPLL']]= -self.ia.real/self.wbase 394 | J[varInd['iaI'],varInd['wte']] = ((self.Kp_PLL*self.ia.real*ra)/self.wbase)*(-math.cos(theta_a)*math.sin(self.wte) 395 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 396 | 397 | #Current controller dynamics 398 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 399 | if np.sign(self.Ki_GCC*self.ua.real) == np.sign(self.xa.real): 400 | J[varInd['xaR'],varInd['uaR']]=0.0 401 | else: 402 | J[varInd['xaR'],varInd['uaR']]=self.Ki_GCC 403 | if np.sign(self.Ki_GCC*self.ua.imag) == np.sign(self.xa.imag): 404 | J[varInd['xaI'],varInd['uaI']]=0.0 405 | else: 406 | J[varInd['xaI'],varInd['uaI']]=self.Ki_GCC 407 | 408 | else: 409 | J[varInd['xaR'],varInd['uaR']]=self.Ki_GCC 410 | J[varInd['xaI'],varInd['uaI']]=self.Ki_GCC 411 | 412 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 413 | if np.sign( (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real)) == np.sign(self.ua.real): 414 | J[varInd['uaR'],varInd['iaR']]= 0.0 415 | J[varInd['uaR'],varInd['uaR']]= 0.0 416 | J[varInd['uaR'],varInd['Vdc']]= 0.0 417 | J[varInd['uaR'],varInd['xDC']]= 0.0 418 | else: 419 | #duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 420 | J[varInd['uaR'],varInd['iaR']]= -self.wp 421 | J[varInd['uaR'],varInd['uaR']]= -self.wp 422 | J[varInd['uaR'],varInd['Vdc']]= -self.wp*self.Kp_DC 423 | J[varInd['uaR'],varInd['xDC']]= self.wp 424 | 425 | if np.sign((self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag)) == np.sign(self.ua.imag): 426 | #duaI = 0.0 427 | J[varInd['uaI'],varInd['iaR']]= 0.0 428 | J[varInd['uaI'],varInd['iaI']]= 0.0 429 | J[varInd['uaI'],varInd['uaI']]= 0.0 430 | J[varInd['uaI'],varInd['xQ']]= 0.0 431 | 432 | else: 433 | #duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 434 | J[varInd['uaI'],varInd['iaR']]= (self.Kp_Q*self.wp*self.va.imag/2) 435 | J[varInd['uaI'],varInd['iaI']]= -self.wp - (self.Kp_Q*self.wp*self.va.real/2) 436 | J[varInd['uaI'],varInd['uaI']]= -self.wp 437 | J[varInd['uaI'],varInd['xQ']]= self.wp 438 | 439 | else: 440 | #duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 441 | #duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 442 | J[varInd['uaR'],varInd['iaR']]= -self.wp 443 | J[varInd['uaR'],varInd['uaR']]= -self.wp 444 | J[varInd['uaR'],varInd['Vdc']]= -self.wp*self.Kp_DC 445 | J[varInd['uaR'],varInd['xDC']]= self.wp 446 | 447 | J[varInd['uaI'],varInd['iaR']]= (self.Kp_Q*self.wp*self.va.imag/2) 448 | J[varInd['uaI'],varInd['iaI']]= -self.wp - (self.Kp_Q*self.wp*self.va.real/2) 449 | J[varInd['uaI'],varInd['uaI']]= -self.wp 450 | J[varInd['uaI'],varInd['xQ']]= self.wp 451 | 452 | #DC link voltage dynamics 453 | dVdc = (self.Ppv - self.S.real)/(self.Vdc*self.C) 454 | J[varInd['Vdc'],varInd['iaR']]= -(self.xa.real+self.Kp_GCC*self.ua.real)/(4*self.C) 455 | J[varInd['Vdc'],varInd['iaI']]= -(self.xa.imag+self.Kp_GCC*self.ua.imag)/(4*self.C) 456 | J[varInd['Vdc'],varInd['xaR']]= -self.ia.real/(4*self.C) 457 | J[varInd['Vdc'],varInd['xaI']]= -self.ia.imag/(4*self.C) 458 | J[varInd['Vdc'],varInd['uaR']]= -(self.Kp_GCC*self.ia.real)/(4*self.C) 459 | J[varInd['Vdc'],varInd['uaI']]= -(self.Kp_GCC*self.ia.imag)/(4*self.C) 460 | J[varInd['Vdc'],varInd['Vdc']]= (-(self.q*self.Np*self.Irs*(self.Vdcbase**2))/(self.C*self.k*self.A*self.Ns*self.Tactual*self.Sbase))*math.exp((self.q*self.Vdc*self.Vdcbase)/(self.k*self.A*self.Ns*self.Tactual)) 461 | #DC link voltage controller dynamics 462 | if abs(self.xDC + self.Kp_DC*(self.Vdc_ref - self.Vdc) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 463 | if np.sign(self.Ki_DC*(self.Vdc_ref - self.Vdc)) == np.sign(self.xDC): 464 | #dxDC = 0.0 465 | J[varInd['xDC'],varInd['Vdc']]= 0.0 466 | else: 467 | #dxDC = self.Ki_DC*(self.Vdc_ref - self.Vdc) 468 | J[varInd['xDC'],varInd['Vdc']]=-self.Ki_DC 469 | else: 470 | #dxDC = self.Ki_DC*(self.Vdc_ref - self.Vdc) 471 | J[varInd['xDC'],varInd['Vdc']]=-self.Ki_DC 472 | 473 | # Reactive power controller dynamics 474 | if abs(self.xDC + self.Kp_DC*(self.Vdc_ref - self.Vdc) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 475 | 476 | if np.sign(-self.Ki_Q*(self.Q_ref - self.S_PCC.imag)) == np.sign(self.xQ): 477 | #dxQ = 0.0 478 | J[varInd['xQ'],varInd['iaR']]= 0.0 479 | J[varInd['xQ'],varInd['iaI']]= 0.0 480 | 481 | else: 482 | #dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 483 | J[varInd['xQ'],varInd['iaR']]= (self.Ki_Q*self.va.imag/2) 484 | J[varInd['xQ'],varInd['iaI']]= -(self.Ki_Q*self.va.real/2) 485 | 486 | else: 487 | #dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 488 | J[varInd['xQ'],varInd['iaR']]= (self.Ki_Q*self.va.imag/2) 489 | J[varInd['xQ'],varInd['iaI']]= -(self.Ki_Q*self.va.real/2) 490 | 491 | #SRF-PLL dynamics 492 | #dxPLL = self.Ki_PLL*(self.vd) 493 | J[varInd['xPLL'],varInd['wte']] = (self.Ki_PLL*ra)*(-math.cos(theta_a)*math.sin(self.wte) 494 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 495 | 496 | #Frequency integration to get angle 497 | #dwte = self.we 498 | J[varInd['wte'],varInd['xPLL']]= 1 499 | J[varInd['wte'],varInd['wte']] = (self.Kp_PLL*ra)*(-math.cos(theta_a)*math.sin(self.wte) 500 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 501 | 502 | return J 503 | except: 504 | LogUtil.exception_handler() 505 | 506 | 507 | -------------------------------------------------------------------------------- /pvder/DER_components_single_phase_constant_Vdc.py: -------------------------------------------------------------------------------- 1 | """Single phase constant Vdc PV-DER code.""" 2 | 3 | from __future__ import division 4 | import numpy as np 5 | import math 6 | import cmath 7 | import scipy 8 | import six 9 | import pdb 10 | import warnings 11 | 12 | from pvder.DER_components import SolarPVDER,PVModule 13 | from pvder.grid_components import BaseValues 14 | 15 | from pvder import utility_functions 16 | from pvder import defaults,templates 17 | from pvder.logutil import LogUtil 18 | 19 | 20 | class SolarPVDERSinglePhaseConstantVdc(PVModule,SolarPVDER): 21 | """ 22 | Class for describing a Solar Photo-voltaic Distributed Energy Resource consisting of panel, converters, and 23 | control systems. 24 | 25 | Attributes: 26 | count (int): Number of instances of `SolarPVDERSinglePhaseConstantVdc`. 27 | n_ODE (int): Number of ODE's. 28 | 29 | """ 30 | count = 0 31 | 32 | def __init__(self,events,configFile,derId,**kwargs): 33 | """Creates an instance of `SolarPV_DER_SinglePhase`. 34 | Args: 35 | events (SimulationEvents): An instance of `SimulationEvents`. 36 | gridModel (Grid): An instance of `Grid`(only need to be suppled for stand alone simulation). 37 | powerRating (float): A scalar specifying the rated power (VA) of the DER. 38 | VrmsRating (float): A scalar specifying the rated RMS L-G voltage (V) of the DER. 39 | ia0,xa0,ua0 (complex): Initial value of inverter states in p.u. of the DER instance. 40 | xP0,xQ0,xPLL0,wte0 (float): Initial value of inverter states in the DER instance. 41 | gridVoltatePhaseA: Initial voltage phasor (V) at PCC - LV side from external program (only need to be suppled if model is not stand alone). 42 | standAlone (bool): Specify if the DER instance is a stand alone simulation or part of a larger simulation. 43 | steadyStateInitialization (bool): Specify whether states in the DER instance will be initialized to steady state values. 44 | allowUnbalancedM (bool): Allow duty cycles to take on unbalanced values during initialization (default: False). 45 | derConfig (dict): Configuration parameters that may be supplied from an external program. 46 | identifier (str): An identifier that can be used to name the instance (default: None). 47 | 48 | Raises: 49 | ValueError: If parameters corresponding to `Sinverter_rated` are not available. 50 | ValueError: If rated DC link voltage is not sufficient. 51 | """ 52 | try: 53 | SolarPVDERSinglePhaseConstantVdc.count = SolarPVDERSinglePhaseConstantVdc.count+1 #Increment count to keep track of number of PV-DER model instances 54 | DER_arguments = self.setup_DER(events,configFile,derId,**kwargs) 55 | super().__init__(self.DER_config['basic_options']['Sinsol'])#Initialize PV module class (base class) 56 | 57 | self.initialize_DER(DER_arguments) 58 | self.creation_message() 59 | except: 60 | LogUtil.exception_handler() 61 | 62 | @property#Decorator used for auto updating 63 | def y0(self): 64 | """List of initial states""" 65 | try: 66 | return[self.ia.real, self.ia.imag, self.xa.real, self.xa.imag, self.ua.real,self.ua.imag, 67 | self.xP,self.xQ,self.xPLL,self.wte] 68 | except: 69 | LogUtil.exception_handler() 70 | 71 | #Apparent power output at inverter terminal 72 | def S_calc(self): 73 | """Inverter apparent power output""" 74 | try: 75 | return (1/2)*(self.vta*self.ia.conjugate())*1.0 76 | except: 77 | LogUtil.exception_handler() 78 | 79 | #Apparent power output at PCC - LV side 80 | def S_PCC_calc(self): 81 | """Power output at PCC LV side""" 82 | try: 83 | return (1/2)*(self.va*self.ia.conjugate()) 84 | #return utility_functions.S_calc(self.va,self.vb,self.vc,self.ia,self.ib,self.ic) 85 | except: 86 | LogUtil.exception_handler() 87 | 88 | def S_load1_calc(self): 89 | """Power absorbed by load at PCC LV side.""" 90 | 91 | return (1/2)*(self.va*(-(self.va/self.Zload1)).conjugate()) 92 | 93 | def S_G_calc(self): 94 | """Power absorbed/produced by grid voltage source.""" 95 | try: 96 | return (1/2)*(-(self.ia-(self.va/self.Zload1))/self.a).conjugate()*self.grid_model.vag 97 | except: 98 | LogUtil.exception_handler() 99 | 100 | #@property 101 | def Vtrms_calc(self): 102 | """Inverter terminal voltage -RMS""" 103 | try: 104 | return utility_functions.Urms_calc(self.vta,self.vta,self.vta) 105 | except: 106 | LogUtil.exception_handler() 107 | 108 | def Vrms_calc(self): 109 | """PCC LV side voltage - RMS""" 110 | try: 111 | return utility_functions.Urms_calc(self.va,self.va,self.va) 112 | except: 113 | LogUtil.exception_handler() 114 | 115 | def Irms_calc(self): 116 | """Inverter current - RMS""" 117 | 118 | return utility_functions.Urms_calc(self.ia,self.ia,self.ia) 119 | 120 | def Vabrms_calc(self): 121 | """PCC LV side voltage - line to lineRMS""" 122 | try: 123 | return abs(self.va-self.vb)/math.sqrt(2) 124 | except: 125 | LogUtil.exception_handler() 126 | 127 | def update_inverter_states(self,ia,xa,ua,xP,xQ,xPLL,wte): 128 | """Update inverter states 129 | Args: 130 | ia (complex): Inverter phase a current. 131 | xa (complex): Inverter controller state. 132 | ua (complex): Inverter controller state. 133 | Vdc (float): DC link voltage. 134 | """ 135 | try: 136 | self.ia = ia 137 | self.xa = xa 138 | self.ua = ua 139 | 140 | self.xP = xP 141 | self.xQ = xQ 142 | 143 | self.xPLL = xPLL 144 | self.wte = wte 145 | except: 146 | LogUtil.exception_handler() 147 | 148 | def update_voltages(self): 149 | """Update voltages.""" 150 | try: 151 | self.vta = self.vta_calc() #Update inverter terminal voltage 152 | self.va = self.va_calc() #Update PCC LV side voltage 153 | except: 154 | LogUtil.exception_handler() 155 | 156 | def update_RMS(self): 157 | """Update RMS voltages.""" 158 | try: 159 | self.Vtrms = self.Vtrms_calc() 160 | self.Vrms_min = self.Vrms = self.Vrms_calc() 161 | self.Irms = self.Irms_calc() 162 | 163 | #Update RMS values 164 | if self.DO_EXTRA_CALCULATIONS: 165 | pass 166 | except: 167 | LogUtil.exception_handler() 168 | 169 | def update_power(self): 170 | """Update RMS voltages.""" 171 | try: 172 | #Update power output 173 | self.S = self.S_calc() 174 | self.S_PCC = self.S_PCC_calc() 175 | 176 | if self.standAlone:#Update load current and grid voltage source power only in stand alone mode 177 | self.iaload1 = self.iphload1_calc(self.va) 178 | 179 | self.S_G = self.S_G_calc() 180 | self.S_load1 = self.S_load1_calc() 181 | except: 182 | LogUtil.exception_handler() 183 | 184 | def update_Pref(self): 185 | """Update active power reference""" 186 | try: 187 | if not self.use_Pref: 188 | self.Pref = self.Ppv 189 | else: 190 | raise ValueError('{}:User active power reference not implemented!') 191 | except: 192 | LogUtil.exception_handler() 193 | 194 | def update_iref(self,t): 195 | """Update current reference""" 196 | try: 197 | if self.current_gradient_limiter: 198 | self.ia_ref = self.get_ramp_limited_iref(t,self.ia_ref_activepower_control()) 199 | else: 200 | self.ia_ref = self.ia_ref_activepower_control() #Get current controller setpoint 201 | except: 202 | LogUtil.exception_handler() 203 | 204 | def update_inverter_frequency(self,t): 205 | """Update inverter PLL frequency. 206 | Args: 207 | t (float): Simulation time in seconds. 208 | """ 209 | try: 210 | self.wgrid_measured = self.wgrid_calc(t) #Update grid frequency 211 | 212 | #Convert PCC LV side voltage from phasor to alpha-beta domain 213 | self.valpha = utility_functions.phasor_to_time_1phase(self.va,w=self.wgrid_measured,t=t) 214 | self.vbeta =utility_functions.phasor_to_time_1phase(self.va*pow(math.e,-1j*(math.pi/2)),w=self.wgrid_measured,t=t) 215 | 216 | #Convert from alpha-beta domain to d-q domain using Parks transformation 217 | self.vd,self.vq = utility_functions.alpha_beta_to_d_q(self.valpha,self.vbeta,self.wte) 218 | 219 | #Calculate inverter frequency from PLL equation 220 | self.we = self.we_calc() 221 | self.winv = self.we 222 | except: 223 | LogUtil.exception_handler() 224 | 225 | 226 | def ODE_model(self,y,t): 227 | """System of ODE's defining the dynamic DER model. 228 | Args: 229 | y (list of float): Initial conditions for the states.. 230 | t (float): Simulation time in seconds. 231 | 232 | Returns: 233 | result (list of float): Derivates for the system of ODE's. 234 | """ 235 | try: 236 | iaR, iaI, xaR, xaI, uaR, uaI, xP, xQ, xPLL, wte = y # unpack current values of y 237 | 238 | self.update_inverter_states(iaR + 1j*iaI, xaR + 1j*xaI,uaR + 1j*uaI, 239 | xP,xQ, 240 | xPLL,wte) 241 | 242 | self.update_Ppv(t) 243 | self.update_Zload1(t) 244 | 245 | self.update_voltages() 246 | self.update_power() 247 | self.update_RMS() 248 | 249 | self.update_Qref(t) 250 | self.update_iref(t) 251 | 252 | self.update_inverter_frequency(t) 253 | 254 | self.update_ridethrough_flags(t) 255 | self.disconnect_or_reconnect(t) 256 | 257 | #Phase a inverter output current 258 | diaR = (1/self.Lf)*(-self.Rf*self.ia.real - self.va.real + self.vta.real) + (self.winv/self.wbase)*self.ia.imag 259 | diaI = (1/self.Lf)*(-self.Rf*self.ia.imag - self.va.imag + self.vta.imag) - (self.winv/self.wbase)*self.ia.real 260 | 261 | #Current controller dynamics 262 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 263 | if np.sign(self.Ki_GCC*self.ua.real) == np.sign(self.xa.real): 264 | dxaR = 0.0 265 | else: 266 | dxaR = self.Ki_GCC*self.ua.real 267 | if np.sign(self.Ki_GCC*self.ua.imag) == np.sign(self.xa.imag): 268 | dxaI = 0.0 269 | else: 270 | dxaI = self.Ki_GCC*self.ua.imag 271 | #six.print_(dxaR+1j*dxaI,np.sign(self.Ki_GCC*self.ua)) 272 | else: 273 | dxaR = self.Ki_GCC*self.ua.real 274 | dxaI = self.Ki_GCC*self.ua.imag 275 | 276 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 277 | if np.sign( (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real)) == np.sign(self.ua.real): 278 | duaR = 0.0 279 | else: 280 | duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 281 | 282 | if np.sign((self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag)) == np.sign(self.ua.imag): 283 | duaI = 0.0 284 | else: 285 | duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 286 | 287 | else: 288 | duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 289 | duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 290 | 291 | #DC link voltage dynamics 292 | dVdc = 0.0 293 | 294 | if abs(self.xP + self.Kp_P*(self.Ppv -self.S.real) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 295 | if np.sign(self.Ki_P*(self.Ppv -self.S.real)) == np.sign(self.xP): 296 | dxP = 0.0 297 | else: 298 | dxP = self.Ki_P*(self.Ppv -self.S.real) 299 | else: 300 | dxP = self.Ki_P*(self.Ppv -self.S.real) 301 | 302 | # Reactive power controller dynamics 303 | if abs(self.xP + self.Kp_P*(self.Ppv -self.S.real) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 304 | 305 | if np.sign(-self.Ki_Q*(self.Q_ref - self.S_PCC.imag)) == np.sign(self.xQ): 306 | dxQ = 0.0 307 | else: 308 | dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 309 | else: 310 | dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 311 | 312 | #SRF-PLL dynamics 313 | dxPLL = self.Ki_PLL*(self.vd) 314 | 315 | #Frequency integration to get angle 316 | dwte = self.we 317 | 318 | result = [ diaR,# list of dy/dt=f functions 319 | diaI, 320 | dxaR, 321 | dxaI, 322 | duaR, 323 | duaI, 324 | dxP, 325 | dxQ, 326 | dxPLL, 327 | dwte] 328 | 329 | return np.array(result) 330 | except: 331 | LogUtil.exception_handler() 332 | 333 | 334 | def jac_ODE_model(self,y,t): 335 | """Jacobian for the system of ODE's. 336 | Args: 337 | y (list of float): Initial conditions for the states.. 338 | t (float): Simulation time in seconds. 339 | Returns: 340 | result (array of float): An array containing the elements of the Jacobian. 341 | """ 342 | try: 343 | iaR, iaI, xaR, xaI, uaR, uaI,xP, xQ, xPLL, wte = y # unpack current values of y 344 | 345 | #self.update_inverter_states(iaR,iaI,xaR,xaI,uaR,uaI, 346 | # xP,xQ, 347 | # xPLL,wte) 348 | self.update_inverter_states(iaR + 1j*iaI, xaR + 1j*xaI,uaR + 1j*uaI, 349 | xP,xQ, 350 | xPLL,wte) 351 | J = self.J 352 | varInd = self.varInd 353 | self.update_Ppv(t) 354 | #self.update_Zload1(t) 355 | 356 | self.update_voltages() 357 | self.update_power() 358 | self.update_RMS() 359 | 360 | self.update_Qref(t) 361 | #self.update_Vdc_ref(t) 362 | self.update_iref(t) 363 | 364 | #d-q transformation 365 | self.update_inverter_frequency(t) 366 | 367 | self.update_ridethrough_flags(t) 368 | self.disconnect_or_reconnect(t) 369 | #Phase a inverter output current 370 | 371 | ra,theta_a = cmath.polar(self.va) 372 | 373 | theta_a = self.wgrid_measured*t + theta_a - math.pi/2 374 | 375 | J[varInd['iaR'],varInd['iaR']] = -self.Rf/self.Lf 376 | J[varInd['iaR'],varInd['iaI']] = (self.xPLL+self.Kp_PLL*self.vd+2*math.pi*60)/self.wbase 377 | J[varInd['iaR'],varInd['xaR']] = self.Vdc/(2*self.Lf) 378 | J[varInd['iaR'],varInd['uaR']] = (self.Vdc*self.Kp_GCC)/(2*self.Lf) 379 | J[varInd['iaR'],varInd['xPLL']] = self.ia.imag/self.wbase 380 | J[varInd['iaR'],varInd['wte']] = ((self.Kp_PLL*self.ia.imag*ra)/self.wbase)*(-math.cos(theta_a)*math.sin(self.wte) 381 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 382 | 383 | J[varInd['iaI'],varInd['iaR']]= -(self.xPLL+self.Kp_PLL*self.vd+2*math.pi*60)/self.wbase 384 | J[varInd['iaI'],varInd['iaI']]= -self.Rf/self.Lf 385 | J[varInd['iaI'],varInd['xaI']]= self.Vdc/(2*self.Lf) 386 | J[varInd['iaI'],varInd['uaI']]= (self.Vdc*self.Kp_GCC)/(2*self.Lf) 387 | J[varInd['iaI'],varInd['xPLL']]= -self.ia.real/self.wbase 388 | J[varInd['iaI'],varInd['wte']] = ((self.Kp_PLL*self.ia.real*ra)/self.wbase)*(-math.cos(theta_a)*math.sin(self.wte) 389 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 390 | 391 | #Current controller dynamics 392 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 393 | if np.sign(self.Ki_GCC*self.ua.real) == np.sign(self.xa.real): 394 | J[varInd['xaR'],varInd['uaR']]=0.0 395 | else: 396 | J[varInd['xaR'],varInd['uaR']]=self.Ki_GCC 397 | if np.sign(self.Ki_GCC*self.ua.imag) == np.sign(self.xa.imag): 398 | J[varInd['xaI'],varInd['uaI']]=0.0 399 | else: 400 | J[varInd['xaI'],varInd['uaI']]=self.Ki_GCC 401 | 402 | else: 403 | J[varInd['xaR'],varInd['uaR']]=self.Ki_GCC 404 | J[varInd['xaI'],varInd['uaI']]=self.Ki_GCC 405 | 406 | if abs(self.Kp_GCC*self.ua + self.xa)>self.m_limit: 407 | if np.sign( (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real)) == np.sign(self.ua.real): 408 | J[varInd['uaR'],varInd['iaR']]= 0.0 409 | J[varInd['uaR'],varInd['uaR']]= 0.0 410 | J[varInd['uaR'],varInd['xP']]= 0.0 411 | else: 412 | #duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 413 | J[varInd['uaR'],varInd['iaR']]= -self.wp 414 | J[varInd['uaR'],varInd['uaR']]= -self.wp 415 | J[varInd['uaR'],varInd['xP']]= self.wp 416 | 417 | if np.sign((self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag)) == np.sign(self.ua.imag): 418 | #duaI = 0.0 419 | J[varInd['uaI'],varInd['iaR']]= 0.0 420 | J[varInd['uaI'],varInd['iaI']]= 0.0 421 | J[varInd['uaI'],varInd['uaI']]= 0.0 422 | J[varInd['uaI'],varInd['xQ']]= 0.0 423 | 424 | else: 425 | #duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 426 | J[varInd['uaI'],varInd['iaR']]= (self.Kp_Q*self.wp*self.va.imag/2) 427 | J[varInd['uaI'],varInd['iaI']]= -self.wp - (self.Kp_Q*self.wp*self.va.real/2) 428 | J[varInd['uaI'],varInd['uaI']]= -self.wp 429 | J[varInd['uaI'],varInd['xQ']]= self.wp 430 | 431 | else: 432 | #duaR = (self.wp)*(-self.ua.real +self.ia_ref.real - self.ia.real) 433 | #duaI = (self.wp)*(-self.ua.imag +self.ia_ref.imag - self.ia.imag) 434 | J[varInd['uaR'],varInd['iaR']]= -self.wp 435 | J[varInd['uaR'],varInd['uaR']]= -self.wp 436 | #J[varInd['uaR'],varInd['Vdc']]= -self.wp*self.Kp_DC 437 | J[varInd['uaR'],varInd['xP']]= self.wp 438 | 439 | J[varInd['uaI'],varInd['iaR']]= (self.Kp_Q*self.wp*self.va.imag/2) 440 | J[varInd['uaI'],varInd['iaI']]= -self.wp - (self.Kp_Q*self.wp*self.va.real/2) 441 | J[varInd['uaI'],varInd['uaI']]= -self.wp 442 | J[varInd['uaI'],varInd['xQ']]= self.wp 443 | 444 | #Active power controller dynamics 445 | if abs(self.xP + self.Kp_P*(self.Ppv -self.S.real) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 446 | if np.sign(self.Ki_P*(self.Vdc_ref - self.Vdc)) == np.sign(self.xP): 447 | #dxP = 0.0 448 | J[varInd['xP'],varInd['iaR']]= 0.0 449 | J[varInd['xP'],varInd['iaI']]= 0.0 450 | else: 451 | #dxP = self.Ki_P*(self.Ppv -self.S.real) 452 | J[varInd['xP'],varInd['iaR']]= (self.Ki_P*self.va.imag/2) 453 | J[varInd['xP'],varInd['iaI']]= -(self.Ki_P*self.va.real/2) 454 | else: 455 | #dxP = self.Ki_P*(self.Ppv -self.S.real) 456 | J[varInd['xP'],varInd['iaR']]= (self.Ki_P*self.va.imag/2) 457 | J[varInd['xP'],varInd['iaI']]= -(self.Ki_P*self.va.real/2) 458 | 459 | # Reactive power controller dynamics 460 | if abs(self.xP + self.Kp_P*(self.Ppv -self.S.real) + 1j*(self.xQ- self.Kp_Q*(self.Q_ref - self.S_PCC.imag)))>self.iref_limit: 461 | if np.sign(-self.Ki_Q*(self.Q_ref - self.S_PCC.imag)) == np.sign(self.xQ): 462 | #dxQ = 0.0 463 | J[varInd['xQ'],varInd['iaR']]= 0.0 464 | J[varInd['xQ'],varInd['iaI']]= 0.0 465 | 466 | else: 467 | #dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 468 | J[varInd['xQ'],varInd['iaR']]= (self.Ki_Q*self.va.imag/2) 469 | J[varInd['xQ'],varInd['iaI']]= -(self.Ki_Q*self.va.real/2) 470 | 471 | else: 472 | #dxQ = -self.Ki_Q*(self.Q_ref - self.S_PCC.imag) 473 | J[varInd['xQ'],varInd['iaR']]= (self.Ki_Q*self.va.imag/2) 474 | J[varInd['xQ'],varInd['iaI']]= -(self.Ki_Q*self.va.real/2) 475 | 476 | #SRF-PLL dynamics 477 | #dxPLL = self.Ki_PLL*(self.vd) 478 | J[varInd['xPLL'],varInd['wte']] = (self.Ki_PLL*ra)*(-math.cos(theta_a)*math.sin(self.wte) 479 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 480 | 481 | #Frequency integration to get angle 482 | #dwte = self.we 483 | J[varInd['wte'],varInd['xPLL']]= 1 484 | J[varInd['wte'],varInd['wte']] = (self.Kp_PLL*ra)*(-math.cos(theta_a)*math.sin(self.wte) 485 | + math.cos(theta_a-math.pi/2)*math.cos(self.wte)) 486 | 487 | return J 488 | except: 489 | LogUtil.exception_handler() 490 | 491 | 492 | -------------------------------------------------------------------------------- /pvder/DER_wrapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu May 14 13:10:31 2020 4 | 5 | @author: splathottam 6 | """ 7 | 8 | from pvder.DER_components import SolarPVDER 9 | from pvder.DER_components_three_phase import SolarPVDERThreePhase 10 | from pvder.DER_components_three_phase_constant_Vdc import SolarPVDERThreePhaseConstantVdc 11 | from pvder.DER_components_three_phase_no_Vrms_filter import SolarPVDERThreePhaseNoVrmsFilter 12 | from pvder.DER_components_three_phase_balanced import SolarPVDERThreePhaseBalanced 13 | from pvder.DER_components_single_phase import SolarPVDERSinglePhase 14 | from pvder.DER_components_single_phase_constant_Vdc import SolarPVDERSinglePhaseConstantVdc 15 | from pvder import defaults,templates,specifications 16 | from pvder import utility_functions 17 | from pvder.logutil import LogUtil 18 | 19 | 20 | class DERModel(SolarPVDER): 21 | """ 22 | Class providing a wrapper to all the DER models. 23 | """ 24 | 25 | #def __init__(self,modelType,events,configFile,**kwargs): 26 | def __init__(self,events,configFile,derId,createDERModel=True,**kwargs): 27 | """Creates an instance of `SolarPV_DER_SinglePhase`. 28 | 29 | Args: 30 | modelType (str): Name of the DER model type. 31 | events (SimulationEvents): An instance of `SimulationEvents`. 32 | gridModel (Grid): An instance of `Grid`(only need to be suppled for stand alone simulation). 33 | gridVoltatePhaseA,gridVoltatePhaseA,gridVoltatePhaseA (float): Initial voltage phasor (V) at PCC - LV side from external program (only need to be suppled if model is not stand alone). 34 | standAlone (bool): Specify if the DER instance is a stand alone simulation or part of a larger simulation. 35 | steadyStateInitialization (bool): Specify whether states in the DER instance will be initialized to steady state values. 36 | allowUnbalancedM (bool): Allow duty cycles to take on unbalanced values during initialization (default: False). 37 | derConfig (dict): Configuration parameters that may be supplied from an external program. 38 | identifier (str): An identifier that can be used to name the instance (default: None). 39 | 40 | Raises: 41 | ValueError: If parameters corresponding to `Sinverter_rated` are not available. 42 | ValueError: If rated DC link voltage is not sufficient. 43 | 44 | """ 45 | try: 46 | """ 47 | if modelType == 'ThreePhaseUnbalanced': 48 | self.DER_model = SolarPVDERThreePhase(events,configFile,**kwargs) 49 | elif modelType == 'ThreePhaseUnbalancedNoVrmsFilter': 50 | self.DER_model = SolarPVDERThreePhaseNoVrmsFilter(events,configFile,**kwargs) 51 | elif modelType == 'ThreePhaseUnbalancedConstantVdc': 52 | self.DER_model = SolarPVDERThreePhaseConstantVdc(events,configFile,**kwargs) 53 | elif modelType == 'ThreePhaseBalanced': 54 | self.DER_model = SolarPVDERThreePhaseBalanced(events,configFile,**kwargs) 55 | elif modelType == 'SinglePhase': 56 | self.DER_model = SolarPVDERSinglePhase(events,configFile,**kwargs) 57 | elif modelType == 'SinglePhaseConstantVdc': 58 | self.DER_model = SolarPVDERSinglePhaseConstantVdc(events,configFile,**kwargs) 59 | elif modelType == 'ThreePhaseUnbalancedNumba': 60 | from pvder.DER_components_three_phase_numba import SolarPVDERThreePhaseNumba 61 | self.DER_model = SolarPVDERThreePhaseNumba(events,configFile,**kwargs) 62 | else: 63 | raise ValueError('{} is not a valid model type! - Valid model types:{}'.format(modelType,templates.model_types)) 64 | """ 65 | if createDERModel: 66 | DER_config = self.get_config(configFile,derId) 67 | modelType = DER_config["basic_specs"]["model_type"] #self.get_DER_model_type(DER_config,DER_parent_config) 68 | self.create_DER_model(events,modelType,configFile,derId,**kwargs) 69 | 70 | except: 71 | LogUtil.exception_handler() 72 | 73 | def get_config(self,configFile,derID): 74 | """Return a config file. 75 | 76 | Args: 77 | configFile (str): The inverter power rating in kVA. 78 | derId (str): User specified parameter ID (can be None). 79 | 80 | Returns: 81 | dict: Config file 82 | """ 83 | try: 84 | DER_config,config_dict = self.get_DER_config(configFile,derID) 85 | DER_parent_ID = self.get_DER_parent_id(DER_config) 86 | DER_parent_config = self.get_DER_parent_config(configFile,DER_parent_ID) 87 | #DER_config, DER_parent_config = self.get_completed_DER_config(DER_config,DER_parent_config) 88 | DER_config = self.update_DER_config_specs(DER_config,DER_parent_config) 89 | return DER_config 90 | except: 91 | LogUtil.exception_handler() 92 | 93 | def get_DER_id(self,**kwargs): 94 | """Create a parameter ID from inverter rated power output. 95 | 96 | Args: 97 | powerRating (float): The inverter power rating in kVA. 98 | derId (str): User specified parameter ID (can be None). 99 | 100 | Returns: 101 | str: Parameter id 102 | """ 103 | try: 104 | assert 'derId' in kwargs, 'DER parameter ID was not provided' # nor DER power rating was provided!' # or 'powerRating' in kwargs, 105 | #if 'derId' in kwargs: 106 | DER_id = kwargs['derId'] 107 | #elif 'powerRating' in kwargs: 108 | # DER_id = str(int(kwargs['powerRating']/1e3)) 109 | return DER_id 110 | except: 111 | LogUtil.exception_handler() 112 | 113 | def create_DER_model(self,events,modelType,configFile,derID,**kwargs): 114 | """Create DER model object.""" 115 | try: 116 | if modelType == 'SolarPVDERThreePhase': #'ThreePhaseUnbalanced': 117 | self.DER_model = SolarPVDERThreePhase(events,configFile,derID,**kwargs) 118 | elif modelType == 'SolarPVDERThreePhaseNoVrmsFilter': #'ThreePhaseUnbalancedNoVrmsFilter': 119 | self.DER_model = SolarPVDERThreePhaseNoVrmsFilter(events,configFile,derID,config_dict,**kwargs) 120 | elif modelType == 'SolarPVDERThreePhaseConstantVdc': #'ThreePhaseUnbalancedConstantVdc': 121 | self.DER_model = SolarPVDERThreePhaseConstantVdc(events,configFile,derID,config_dict,**kwargs) 122 | elif modelType == 'SolarPVDERThreePhaseBalanced': #'ThreePhaseBalanced': 123 | self.DER_model = SolarPVDERThreePhaseBalanced(events,configFile,derID,**kwargs) 124 | elif modelType == 'SolarPVDERSinglePhase': #'SinglePhase': 125 | self.DER_model = SolarPVDERSinglePhase(events,configFile,derID,**kwargs) 126 | elif modelType == 'SolarPVDERSinglePhaseConstantVdc': #SinglePhaseConstantVdc': 127 | self.DER_model = SolarPVDERSinglePhaseConstantVdc(events,configFile,derID,**kwargs) 128 | elif modelType == 'SolarPVDERThreePhaseNumba': #'ThreePhaseUnbalancedNumba': 129 | from pvder.DER_components_three_phase_numba import SolarPVDERThreePhaseNumba 130 | self.DER_model = SolarPVDERThreePhaseNumba(events,configFile,derID,**kwargs) 131 | else: 132 | raise ValueError('{} is not a valid model type! - Valid model types:{}'.format(modelType,list(templates.DER_design_template.keys()))) #templates.model_types 133 | except: 134 | LogUtil.exception_handler() 135 | 136 | -------------------------------------------------------------------------------- /pvder/__init__.py: -------------------------------------------------------------------------------- 1 | from pvder._version import __version__ -------------------------------------------------------------------------------- /pvder/_version.py: -------------------------------------------------------------------------------- 1 | __version__= '0.6.0' 2 | -------------------------------------------------------------------------------- /pvder/config_default.json: -------------------------------------------------------------------------------- 1 | { 2 | "50":{"basic_specs":{"n_phases":3,"Sinsol":1.0,"model_type":"SolarPVDERThreePhase"}, 3 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 4 | 5 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0}, 6 | 7 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6, 8 | "C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 9 | 10 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0, 11 | "Kp_DC":-2.0,"Ki_DC":-10.0, 12 | "Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 13 | 14 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 15 | "initial_values":{"iaR0":0,"iaI0":0.0,"xaR0":0.0,"xaI0":0.0,"uaR0":0.0,"uaI0":0.0, 16 | "xDC0":0.0,"xQ0":0.0,"xPLL0":0.0,"wte0":6.28} 17 | }, 18 | "250":{"basic_specs":{"n_phases":3,"Sinsol":1.0,"model_type":"SolarPVDERThreePhase"}, 19 | "module_parameters":{"Np":45,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 750.0,"Vdcmpp_max": 1000.0}, 20 | 21 | "inverter_ratings":{"Srated":250e3,"Vdcrated":750.0,"Ioverload":1.3,"Vrmsrated":230.0}, 22 | 23 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":300.0e-6, 24 | "C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 25 | 26 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0, 27 | "Kp_DC":-2.0,"Ki_DC":-10.0, 28 | "Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 29 | 30 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 31 | "initial_values":{"iaR0":0,"iaI0":0.0,"xaR0":0.0,"xaI0":0.0,"uaR0":0.0,"uaI0":0.0, 32 | "xDC0":0.0,"xQ0":0.0,"xPLL0":0.0,"wte0":6.28} 33 | }, 34 | 35 | "10":{"basic_specs":{"n_phases":1,"Sinsol":1.0}, 36 | "module_parameters":{"Np":2,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 650.0,"Vdcmpp_max": 800.0}, 37 | 38 | "inverter_ratings":{"Srated":10e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0}, 39 | 40 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6, 41 | "C_actual":30.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 42 | 43 | "controller_gains":{"Kp_GCC":12000.0,"Ki_GCC":4000.0, 44 | "Kp_DC":-4.0,"Ki_DC":-20.0, 45 | "Kp_Q":0.4,"Ki_Q":20.0,"wp": 20e4}, 46 | 47 | "steadystate_values":{"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01}, 48 | "initial_values":{"iaR0":0,"iaI0":0.0,"xaR0":0.0,"xaI0":0.0,"uaR0":0.0,"uaI0":0.0, 49 | "xDC0":0.0,"xQ0":0.0,"xPLL0":0.0,"wte0":6.28} 50 | 51 | }, 52 | "10_constantVdc":{"basic_specs":{"n_phases":1,"Sinsol":1.0}, 53 | "module_parameters":{"Np":2,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 650.0,"Vdcmpp_max": 800.0}, 54 | 55 | "inverter_ratings":{"Srated":10e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0}, 56 | 57 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6, 58 | "C_actual":30.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 59 | 60 | "controller_gains":{"Kp_GCC":12000.0,"Ki_GCC":4000.0, 61 | "Kp_DC":5000.0,"Ki_DC":500.0, 62 | "Kp_Q":0.4,"Ki_Q":20.0,"wp": 20e4}, 63 | 64 | "steadystate_values":{"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01}, 65 | "initial_values":{"iaR0":0,"iaI0":0.0,"xaR0":0.0,"xaI0":0.0,"uaR0":0.0,"uaI0":0.0, 66 | "xDC0":0.0,"xQ0":0.0,"xPLL0":0.0,"wte0":6.28} 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /pvder/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Mar 29 16:42:13 2021 4 | 5 | @author: splathottam 6 | """ 7 | 8 | Iscr = 8.03 #Cell short-circuit current at reference temperature and radiation 9 | Kv = 0.0017 #Short-circuit current temperature co-efficient 10 | T0 = 273.15 + 25.0 #Cell reference temperature in Kelvin 11 | Irs = 1.2e-7 #Cell reverse saturation current 12 | q = 1.602e-19 #Charge of an electron 13 | k = 1.38e-23 #Boltzmann's constant 14 | A = 1.92 #p-n junction ideality factor 15 | -------------------------------------------------------------------------------- /pvder/defaults.py: -------------------------------------------------------------------------------- 1 | """Store configuration options.""" 2 | 3 | 4 | #Results options 5 | 6 | # Changing these adjusts the size and layout of the plot 7 | FIGURE_WIDTH = 8 8 | FIGURE_HEIGHT = 8 9 | FIGURE_DPI = 1200 10 | 11 | 12 | #Model options 13 | Ioverload = 1.3 #Inverter overload 14 | 15 | #Events 16 | Tactual = 298.15 #PV module temperature in K 17 | Zload1_actual = 10e6+0j #Load at PCC 18 | 19 | #Smart inverter feature options 20 | Vthreshold_high_limit = 1.5 #percentage 21 | tthreshold_high_limit = 100.0 #seconds 22 | 23 | tdisconnect_low_limit = 1/120.0 #seconds #Minimum time required to initiate DER disconnection (output cessation) 24 | treconnect_low_limit = 0.1 #seconds #Minimum time required to initiate DER reconnection (output restoration) 25 | 26 | #DC link voltage control 27 | DEFAULT_del_Vdc_ref = 2.0 28 | DEFAULT_del_t_Vdc_ref = 0.5 29 | 30 | #Frequency estimation 31 | use_frequency_estimate = False 32 | 33 | #Solver options 34 | DEFAULT_DELTA_T = 0.001 #Simulation time step 35 | max_steps = 1000 #Max steps to be used by solver before producing error 36 | 37 | #Steady state solver options 38 | STEADYSTATE_SOLVER = 'SLSQP' 39 | 40 | #Default DER parameters 41 | """ 42 | DEFAULT_Z1_actual= 0.0019 + 1j*0.0561 43 | DEFAULT_basic_specs = {"Sinsol0":1.0} 44 | DEFAULT_module_parameters = {'Np':2,'Ns':1000,'Vdcmpp0':750.0,'Vdcmpp_min': 650.0,'Vdcmpp_max': 800.0} 45 | DEFAULT_inverter_ratings = {"Vdcrated":550.0,"Ioverload":DEFAULT_Ioverload,"Vrmsrated":177.0} 46 | DEFAULT_circuit_parameters = {"Rf_actual":0.002,"Lf_actual":25.0e-6, 47 | "C_actual":300.0e-6,"R1_actual":DEFAULT_Z1_actual.real,"X1_actual":DEFAULT_Z1_actual.imag} 48 | DEFAULT_controller_gains ={"Kp_GCC":12000.0,"Ki_GCC":4000.0, 49 | "Kp_DC":-4.0,"Ki_DC":-20.0, 50 | "Kp_Q":0.4,"Ki_Q":20.0,"wp": 20e4} 51 | DEFAULT_steadystate_values = {"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01} 52 | DEFAULT_initial_values = {"iaR0":0,"iaI0":0.0,"xaR0":0.0,"xaI0":0.0,"uaR0":0.0,"uaI0":0.0, 53 | "xDC0":0.0,"xQ0":0.0,"xPLL0":0.0,"wte0":6.28} 54 | 55 | #Default VRT config 56 | """ 57 | RT_config = {'LVRT':{'0':{'V_threshold':0.5, 58 | 't_threshold':1.0, 59 | 'mode':'mandatory_operation', 60 | 't_start':0.0, 61 | 'threshold_breach':False}, 62 | '1':{'V_threshold':0.7, 63 | 't_threshold':3.5, 64 | 'mode':'momentary_cessation', #'momentary_cessation' 65 | 't_start':0.0, 66 | 'threshold_breach':False}, 67 | '2':{'V_threshold':0.88, 68 | 't_threshold':5.0, 69 | 'mode':'mandatory_operation', 70 | 't_start':0.0, 71 | 'threshold_breach':False}, 72 | }, 73 | 'HVRT':{'0':{'V_threshold':1.12, 74 | 't_threshold':0.5, 75 | 'mode':'mandatory_operation', 76 | 't_start':0.0, 77 | 'threshold_breach':False}, 78 | '1':{'V_threshold':1.06, 79 | 't_threshold':1.0, 80 | 'mode':'mandatory_operation', 81 | 't_start':0.0, 82 | 'threshold_breach':False}, 83 | }, 84 | 'OUTPUT_CESSATION_DELAY':0.01, 85 | 'OUTPUT_RESTORE_DELAY':1.75, 86 | 'RESTORE_Vdc':False} 87 | 88 | #Logging 89 | logConfig={ 90 | "logLevel": 20, 91 | "logFilePath": "logs/pvder.log", 92 | "mode": "w", 93 | "loggerName": "pvder_logger" 94 | } 95 | 96 | 97 | -------------------------------------------------------------------------------- /pvder/exceptionutil.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import linecache 5 | import pdb 6 | import json 7 | from dateutil import parser 8 | 9 | 10 | #=================================================================================================== 11 | #=================================================================================================== 12 | #=================================================================================================== 13 | class ExceptionUtil(object): 14 | """ 15 | Sample usage: 16 | ============= 17 | from exceptionutil import ExceptionUtil 18 | 19 | class Foo(ExceptionUtil): 20 | def __init__(self): 21 | self.create_logger("sampleLogger",logFilePath='path/to/myLog.log',logLevel=logging.DEBUG, 22 | mode='w') 23 | return None 24 | 25 | def bar(self,a,b): 26 | try: 27 | code 28 | self.logger.log(level=10,msg='') 29 | some more code 30 | except: 31 | self.exception_handler() 32 | """ 33 | 34 | #=================================================================================================== 35 | def __init__(self): 36 | super(ExceptionUtil,self).__init__() 37 | self._defaultFormatterStr='%(asctime)s::%(name)s::%(filename)s::%(funcName)s::'+\ 38 | '%(levelname)s::%(message)s::%(threadName)s::%(process)d' 39 | self._defaultFormatterStrSep='::' 40 | self._formatterStr=None 41 | self._formatterStrSep=None 42 | return None 43 | 44 | #=================================================================================================== 45 | def create_logger(self,loggerName,logFilePath,logLevel,formatterStr=None,formatterStrSep=None,mode='a'): 46 | try: 47 | # create logger 48 | self.logger = logging.getLogger(loggerName) 49 | self._logFilePath=logFilePath 50 | 51 | # create formatter and add it to the handlers 52 | if not formatterStr: 53 | formatterStr=self._defaultFormatterStr 54 | formatterStrSep=self._defaultFormatterStrSep 55 | 56 | self._formatterStr=formatterStr 57 | self._formatterStrSep=formatterStrSep 58 | self.set_logger(logLevel,mode) 59 | 60 | except: 61 | raise 62 | 63 | #=================================================================================================== 64 | def set_logger(self,logLevel,mode): 65 | try: 66 | self.logger.setLevel(logLevel) 67 | # create file handler which logs messages 68 | fh = logging.FileHandler(self._logFilePath,mode=mode) 69 | 70 | formatter = logging.Formatter(self._formatterStr) 71 | fh.setFormatter(formatter) 72 | fh.setLevel(logLevel) 73 | 74 | self.logger.addHandler(fh) 75 | except: 76 | raise 77 | 78 | #=================================================================================================== 79 | def _get_exception(self): 80 | try: 81 | res={} 82 | exc_type, exc_obj, tb = sys.exc_info() 83 | f = tb.tb_frame 84 | res['lineno'] = tb.tb_lineno 85 | res['filename'] = f.f_code.co_filename 86 | res['funcName']=f.f_code.co_name 87 | linecache.checkcache(res['filename']) 88 | res['line'] = linecache.getline(res['filename'], res['lineno'], f.f_globals).strip() 89 | res['reason']='{}'.format(exc_obj) 90 | return res 91 | except: 92 | raise 93 | 94 | #=================================================================================================== 95 | def exception_handler(self,additionalInfo=None): 96 | try: 97 | err=self._get_exception() 98 | if additionalInfo: 99 | err['additionalInfo']=additionalInfo 100 | self.logger.error(json.dumps(err)) 101 | raise 102 | except: 103 | raise 104 | 105 | #=================================================================================================== 106 | def log(self,level=None,msg=None): 107 | try: 108 | if not level: 109 | level=logging.ERROR 110 | if level==logging.ERROR:# will also raise 111 | self.exception_handler(msg) 112 | else: 113 | self.logger.log(level,msg) 114 | except: 115 | raise 116 | 117 | #=================================================================================================== 118 | def get_logs(self,filterResult=None,logFilePath=None,formatterStr=None,formatterStrSep=None): 119 | """ 120 | Sample call: 121 | ============ 122 | self.get_logs(logFilePath='foo.log') 123 | """ 124 | try: 125 | if not formatterStr and self._formatterStr: 126 | formatterStr=self._formatterStr 127 | elif not formatterStr and not self._formatterStr: 128 | formatterStr=self._defaultFormatterStr 129 | 130 | if not formatterStrSep and self._formatterStrSep: 131 | formatterStrSep=self._formatterStrSep 132 | elif not formatterStrSep and not self._formatterStrSep: 133 | formatterStrSep=self._defaultFormatterStrSep 134 | 135 | if not logFilePath: 136 | logFilePath=self._logFilePath 137 | f=open(logFilePath); 138 | logs=f.read(); f.close() 139 | 140 | res=[]; addThisItem=True 141 | header=[entry for entry in formatterStr.split(formatterStrSep)] 142 | for thisLine in logs.splitlines(): 143 | thisLog={} 144 | for item,val in zip(header,thisLine.split(formatterStrSep)): 145 | thisLog[item.strip()]=val.strip() 146 | if '%(asctime)s' in thisLog:# convert to datetime object 147 | thisLog['%(asctime)s']=parser.parse(thisLog['%(asctime)s']) 148 | if '%(levelname)s' in thisLog and thisLog['%(levelname)s']=='ERROR': 149 | thisLog['%(message)s']=json.loads(thisLog['%(message)s']) 150 | if '%(filename)s' in thisLog: 151 | thisLog['%(filename)s']=os.path.basename(thisLog['%(message)s']['filename']) 152 | if '%(funcName)s' in thisLog: 153 | thisLog['%(funcName)s']=os.path.basename(thisLog['%(message)s']['funcName']) 154 | 155 | if filterResult: 156 | if isinstance(filterResult,dict): 157 | addThisItem=True 158 | for entry in filterResult: 159 | if not entry in thisLog or thisLog[entry]!=filterResult[entry]: 160 | addThisItem=False 161 | break 162 | if addThisItem: 163 | res.append(thisLog) 164 | 165 | return res 166 | except: 167 | raise 168 | 169 | 170 | -------------------------------------------------------------------------------- /pvder/grid_components.py: -------------------------------------------------------------------------------- 1 | """Grid model and shared attributes.""" 2 | 3 | from __future__ import division 4 | import numpy as np 5 | import math 6 | import cmath 7 | import six 8 | from pvder import utility_functions 9 | 10 | class BaseValues(): 11 | """Class to store base values.""" 12 | 13 | Vbase = 500.0 #L-G peak" 14 | Sbase = 50e3 #VA base 15 | wbase = 2*math.pi*60.0 16 | Vdcbase = Vbase #DC side base value is same as AC side base value 17 | Ibase = Sbase/Vbase 18 | Zbase = (Vbase**2)/Sbase 19 | 20 | Lbase = Zbase/wbase 21 | Cbase = 1/(Zbase*wbase) 22 | 23 | class Grid(BaseValues): 24 | """ Class for grid""" 25 | 26 | grid_count = 0 #Count for grid objects 27 | 28 | n_ODE = 0 #Number of ODE's 29 | 30 | Vgridrated = 20415.0 # L-G peak to peak equivalent to 25000 V L-L RMS 31 | _t_voltage_previous = 0.0 32 | _t_frequency_previous = 0.0 33 | 34 | def __init__(self,events,unbalance_ratio_b=1.0,unbalance_ratio_c=1.0,Z2_actual = 1.61 + 1j*5.54): 35 | """Creates an instance of `GridSimulation`. 36 | 37 | Args: 38 | events: An instance of `SimulationEvents`. 39 | unbalance_ratio_b,unbalance_ratio_c: Scalar specifying difference in Phase B and Phase C voltage magnitude compared to phase A. 40 | Z2_actual: Complex scalar specifying the impedance of the feeder connecting the DER with the voltage source. 41 | """ 42 | 43 | #Increment count 44 | Grid.grid_count = Grid.grid_count+1 45 | #Events object 46 | self.events = events 47 | 48 | #Object name 49 | self.name = 'grid_'+str(Grid.grid_count) 50 | 51 | #Voltage unbalance 52 | self.unbalance_ratio_b = unbalance_ratio_b 53 | self.unbalance_ratio_c = unbalance_ratio_c 54 | 55 | #Grid impedance 56 | self.Z2_actual = Z2_actual 57 | self.R2_actual = self.Z2_actual.real 58 | self.L2_actual = self.Z2_actual.imag/(2*math.pi*60.0) 59 | 60 | #Converting to per unit 61 | self.R2 = self.R2_actual/self.Zbase #Transmission line resistance 62 | self.L2 = self.L2_actual/self.Lbase #Transmission line resistance 63 | 64 | self.Z2 = self.Z2_actual/self.Zbase #Transmission line impedance 65 | 66 | self.transmission_name = 'transmission_'+str(Grid.grid_count) 67 | #Grid voltage/frequency events 68 | self.Vagrid,self.wgrid = events.grid_events(t=0.0) #Grid voltage and frequency set-point 69 | self.Vagrid = self.Vagrid*(self.Vgridrated/self.Vbase) 70 | self.Vbgrid = utility_functions.Ub_calc(self.Vagrid*self.unbalance_ratio_b) 71 | self.Vcgrid = utility_functions.Uc_calc(self.Vagrid*self.unbalance_ratio_c) 72 | #Actual Grid voltage 73 | self.vag = self.Vagrid 74 | self.vbg = utility_functions.Ub_calc(self.vag*self.unbalance_ratio_b) 75 | self.vcg = utility_functions.Uc_calc(self.vag*self.unbalance_ratio_c) 76 | self.Vgrms = self.Vgrms_calc() 77 | 78 | @property 79 | def y0(self): 80 | """Grid states""" 81 | 82 | return [self.vag.real,self.vag.imag,\ 83 | self.vbg.real,self.vbg.imag,\ 84 | self.vcg.real,self.vcg.imag] 85 | 86 | def Vgrms_calc(self): 87 | """Grid side terminal voltage - RMS""" 88 | 89 | return utility_functions.Urms_calc(self.vag,self.vbg,self.vcg) 90 | 91 | def steady_state_model(self,t): 92 | """Grid voltage change.""" 93 | 94 | Vagrid_new,wgrid_new = self.events.grid_events(t) 95 | Vagrid_new = Vagrid_new*(self.Vgridrated/self.Vbase) 96 | 97 | if abs(self.Vagrid- Vagrid_new) > 0.0 and t >= self._t_voltage_previous: 98 | utility_functions.print_to_terminal("{}:Grid voltage changed from {:.3f} V to {:.3f} V at {:.3f} s".format(self.name,self.Vagrid,Vagrid_new,t)) 99 | 100 | self.Vagrid = Vagrid_new 101 | self.Vbgrid = utility_functions.Ub_calc(self.Vagrid*self.unbalance_ratio_b) 102 | self.Vcgrid = utility_functions.Uc_calc(self.Vagrid*self.unbalance_ratio_c) 103 | 104 | self._t_voltage_previous = t 105 | 106 | if abs(self.wgrid- wgrid_new) > 0.0 and t >= self._t_frequency_previous: 107 | utility_functions.print_to_terminal("{}:Grid frequency changed from {:.3f} Hz to {:.3f} Hz at {:.3f} s".format(self.name,self.wgrid/(2.0*math.pi),wgrid_new/(2.0*math.pi),t)) 108 | 109 | self.wgrid = wgrid_new 110 | self._t_frequency_previous = t 111 | 112 | self.vag = self.Vagrid 113 | self.vbg = self.Vbgrid 114 | self.vcg = self.Vcgrid 115 | -------------------------------------------------------------------------------- /pvder/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /pvder/logutil.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from pvder.defaults import logConfig 5 | from pvder.exceptionutil import ExceptionUtil 6 | 7 | 8 | LogUtil=ExceptionUtil() 9 | baseDir=os.path.dirname(os.path.abspath(__file__)) 10 | logConfig['logFilePath']=os.path.join(baseDir,logConfig['logFilePath']) 11 | LogUtil.create_logger(**logConfig) 12 | -------------------------------------------------------------------------------- /pvder/properties.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Jun 11 12:43:26 2020 4 | 5 | @author: splathottam 6 | """ 7 | 8 | import six 9 | 10 | if six.PY3: 11 | string_type = (str) 12 | elif six.PY2: 13 | string_type = (str,unicode) 14 | 15 | parameter_properties = {"Rf":{'base':'impendance','type':(int,float)}, 16 | "Lf":{'base':'inductance','type':(int,float)}, 17 | "C":{'base':'capacitance','type':(int,float)}, 18 | "R1":{'base':'impendance','type':(int,float)}, 19 | "X1":{'base':'impendance','type':(int,float)}, 20 | "Zf":{'base':'impendance','type':(complex)}, 21 | 22 | "Rf_actual":{'base':'impendance','type':(int,float)}, 23 | "Lf_actual":{'base':'impendance','type':(int,float)}, 24 | "C_actual":{'base':'impendance','type':(int,float)}, 25 | "R1_actual":{'base':'impendance','type':(int,float)}, 26 | "X1_actual":{'base':'impendance','type':(int,float)}, 27 | 28 | "Np":{'type':(int,float)}, 29 | "Ns":{'type':(int,float)}, 30 | "Vdcmpp0":{'base':'voltage','type':(int,float)}, 31 | "Vdcmpp_min":{'base':'voltage','type':(int,float)}, 32 | "Vdcmpp_max":{'base':'voltage','type':(int,float)}, 33 | 34 | "Srated":{'base':'power','type':(int,float)}, 35 | "Vdcrated":{'base':'voltage','type':(int,float)}, 36 | "Ioverload":{'type':(int,float)}, 37 | "Vrmsrated":{'base':'voltage','type':(int,float)}, 38 | "Iramp_max_gradient_real":{'type':(int,float)}, 39 | "Iramp_max_gradient_imag":{'type':(int,float)}, 40 | 41 | "Kp_GCC":{'type':(int,float)}, 42 | "Ki_GCC":{'type':(int,float)}, 43 | "Kp_DC":{'type':(int,float)}, 44 | "Ki_DC":{'type':(int,float)}, 45 | "Kp_P":{'type':(int,float)}, 46 | "Ki_P":{'type':(int,float)}, 47 | "Kp_Q":{'type':(int,float)}, 48 | "Ki_Q":{'type':(int,float)}, 49 | "wp": {'type':(int,float)}, 50 | "Tfilter_Vrms": {'type':(int,float)}, 51 | 52 | "maR0":{'type':(int,float)}, 53 | "maI0":{'type':(int,float)}, 54 | "iaR0":{'type':(int,float)}, 55 | "iaI0":{'type':(int,float)}, 56 | 57 | "iaR":{'type':(int,float)}, 58 | "iaI":{'type':(int,float)}, 59 | "ibR":{'type':(int,float)}, 60 | "ibI":{'type':(int,float)}, 61 | "icR":{'type':(int,float)}, 62 | "icI":{'type':(int,float)}, 63 | 64 | "xaR":{'type':(int,float)}, 65 | "xaI":{'type':(int,float)}, 66 | "xbR":{'type':(int,float)}, 67 | "xbI":{'type':(int,float)}, 68 | "xcR":{'type':(int,float)}, 69 | "xcI":{'type':(int,float)}, 70 | 71 | "uaR":{'type':(int,float)}, 72 | "uaI":{'type':(int,float)}, 73 | "ubR":{'type':(int,float)}, 74 | "ubI":{'type':(int,float)}, 75 | "ucR":{'type':(int,float)}, 76 | "ucI":{'type':(int,float)}, 77 | 78 | "Vdc":{'type':(int,float)}, 79 | "xDC":{'type':(int,float)}, 80 | "xP":{'type':(int,float)}, 81 | "xQ":{'type':(int,float)}, 82 | "xPLL":{'type':(int,float)}, 83 | "wte":{'type':(int,float)}, 84 | 85 | "n_phases":{'type':(int)}, 86 | "n_ODE":{'type':(int)}, 87 | "model_type":{'type':string_type}, 88 | "phases":{'type':(tuple)}, 89 | "unbalanced":{'type':(bool)}, 90 | 91 | 't_stable':{'type':(int,float)}, 92 | 'm_steady_state':{'type':(float)}, 93 | "Sinsol":{'type':(int,float)}, 94 | 'use_Pref':{'type':(bool)}, 95 | 'current_gradient_limiter':{'type':(bool)}, 96 | "Vrms_measurement_type":{'type':(string_type)} 97 | 98 | } 99 | 100 | phase_properties = {"i":{'base':'current','type':(complex,float),'description':'Current'}, 101 | "x":{'base':'','type':(complex,float),'description':'Controller state'}, 102 | "u":{'base':'','type':(complex,float),'description':'Controller state'} 103 | } 104 | 105 | state_properties = {"iaR":{'physical_type':'real'}, 106 | "iaI":{'physical_type':'imag'}, 107 | "ibR":{'physical_type':'real'}, 108 | "ibI":{'physical_type':'imag'}, 109 | "icR":{'physical_type':'real'}, 110 | "icI":{'physical_type':'imag'}, 111 | 112 | "xaR":{'physical_type':'real'}, 113 | "xaI":{'physical_type':'imag'}, 114 | "xbR":{'physical_type':'real'}, 115 | "xbI":{'physical_type':'imag'}, 116 | "xcR":{'physical_type':'real'}, 117 | "xcI":{'physical_type':'imag'}, 118 | 119 | "uaR":{'physical_type':'real'}, 120 | "uaI":{'physical_type':'imag'}, 121 | "ubR":{'physical_type':'real'}, 122 | "ubI":{'physical_type':'imag'}, 123 | "ucR":{'physical_type':'real'}, 124 | "ucI":{'physical_type':'imag'}, 125 | 126 | "Vdc":{'physical_type':'real'}, 127 | 128 | "xDC":{'physical_type':'real'}, 129 | "xP":{'physical_type':'real'}, 130 | "xQ":{'physical_type':'real'}, 131 | 132 | "xPLL":{'physical_type':'real'}, 133 | "wte":{'physical_type':'real'} 134 | } 135 | 136 | controller_properties = {"current_controller":{"gains":["Kp_GCC","Ki_GCC","wp"],"description":"Current controller"}, 137 | "dc_link_voltage_controller": {"gains":["Kp_DC","Ki_DC"],"description":"DC link voltage controller"}, 138 | "active_power_controller": {"gains":["Kp_P","Ki_P"],"description":"Active power controller"}, 139 | "reactive_power_controller": {"gains":["Kp_Q","Ki_Q"],"description":"Reactive power controller"}, 140 | "pll_controller": {"gains":["Kp_PLL","Ki_PLL"],"description":"PLL controller"} 141 | } -------------------------------------------------------------------------------- /pvder/specifications.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Apr 6 12:20:55 2020 4 | 5 | @author: splathottam 6 | """ 7 | 8 | import six 9 | from pvder.grid_components import Grid 10 | 11 | steadystate_solver_spec = {'SLSQP':{'ftol': 1e-10, 'disp': True, 'maxiter':10000}, 12 | 'nelder-mead':{'xtol': 1e-8, 'disp': True, 'maxiter':10000}} 13 | 14 | if six.PY3: 15 | string_type = (str) 16 | elif six.PY2: 17 | string_type = (str,unicode) 18 | 19 | DER_argument_spec = {'derId':{'default_value':None,'type':string_type}, 20 | 'Srated':{'default_value':None,'type':(int,float)}, 21 | 'powerRating':{'default_value':None,'type':(int,float)}, 22 | 'Vrmsrated':{'default_value':None,'type':(int,float)}, 23 | 'VrmsRating':{'default_value':None,'type':(int,float)}, 24 | 'Vdcrated':{'default_value':None,'type':(int,float)}, 25 | 'gridModel':{'default_value':None,'type':Grid}, 26 | 'verbosity':{'default_value':'INFO','type':(string_type,int)}, 27 | 'identifier':{'default_value':'','type':string_type}, 28 | 'derConfig':{'default_value':{},'type':dict}, 29 | 'gridVoltagePhaseA':{'default_value':None,'type':complex}, 30 | 'gridVoltagePhaseB':{'default_value':None,'type':complex}, 31 | 'gridVoltagePhaseC':{'default_value':None,'type':complex}, 32 | 'gridFrequency':{'default_value':None,'type':(int,float)}, 33 | 'standAlone':{'default_value':True,'type':bool}, 34 | 'steadyStateInitialization':{'default_value':True,'type':bool}, 35 | 'allowUnbalancedM':{'default_value':False,'type':bool}, 36 | 'ia0':{'default_value':None,'type':complex}, 37 | 'xa0':{'default_value':None,'type':complex}, 38 | 'ua0':{'default_value':None,'type':complex}, 39 | 'xDC0':{'default_value':None,'type':float}, 40 | 'xP0':{'default_value':None,'type':float}, 41 | 'xQ0':{'default_value':None,'type':float}} 42 | 43 | logging_levels = ['DEBUG','INFO','WARNING','ERROR','CRITICAL'] 44 | logging_levels_integer = [10,20,30,40,50] 45 | 46 | logging_levels_dict = {'DEBUG':10,'INFO':20,'WARNING':30,'ERROR':40,'CRITICAL':50} 47 | 48 | -------------------------------------------------------------------------------- /pvder/templates.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Mar 30 10:19:16 2020 4 | 5 | @author: splathottam 6 | """ 7 | 8 | single_phase_models = ["SolarPVDERSinglePhase","SolarPVDERSinglePhaseConstantVdc"] 9 | three_phase_models = ["SolarPVDERThreePhase","SolarPVDERThreePhaseNoVrmsFilter","SolarPVDERThreePhaseConstantVdc","SolarPVDERThreePhaseBalanced","SolarPVDERThreePhaseNumba"] 10 | 11 | constant_Vdc_models = ["SolarPVDERSinglePhaseConstantVdc","SolarPVDERThreePhaseConstantVdc"] 12 | 13 | model_types = ['SinglePhase','SinglePhaseConstantVdc', 14 | 'ThreePhaseUnbalanced','ThreePhaseUnbalancedConstantVdc','ThreePhaseBalanced','ThreePhaseUnbalancedNumba','ThreePhaseUnbalancedNoVrmsFilter'] 15 | 16 | DER_design_template = {"SolarPVDERSinglePhase": 17 | {"parent_config":"", 18 | "basic_specs":{'phases':('a'),'n_phases':1,'n_ODE':11,"model_type":"SolarPVDERSinglePhase"}, 19 | "basic_options":{'t_stable':0.5,'m_steady_state':0.96,"Sinsol":100.0,"current_gradient_limiter":False,"Vrms_measurement_type":"minimum"}, 20 | "module_parameters":{"Np":2,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 650.0,"Vdcmpp_max": 800.0}, 21 | "inverter_ratings":{"Srated":10e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 22 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"C_actual":30.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 23 | "controller_gains":{"Kp_GCC":12000.0,"Ki_GCC":4000.0,"Kp_DC":-4.0,"Ki_DC":-20.0,"Kp_Q":0.4,"Ki_Q":20.0,"wp": 20e4}, 24 | "steadystate_values":{"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01}, 25 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 26 | "Vdc":550.0, 27 | "xDC":0.0,"xQ":0.0, 28 | "xPLL":0.0,"wte":6.28} 29 | 30 | }, 31 | 32 | "SolarPVDERSinglePhaseConstantVdc": 33 | {"parent_config":"", 34 | "basic_specs":{'phases':('a'),'n_phases':1,'n_ODE':10,"model_type":"SolarPVDERSinglePhaseConstantVdc"}, 35 | "basic_options":{'t_stable':0.5,'m_steady_state':0.96,"Sinsol":100.0,'use_Pref':False,"current_gradient_limiter":False,"Vrms_measurement_type":"minimum"}, 36 | "module_parameters":{"Np":2,"Ns":1000,"Vdcmpp0":750.0,"Vdcmpp_min": 650.0,"Vdcmpp_max": 800.0}, 37 | "inverter_ratings":{"Srated":10e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 38 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 39 | "controller_gains":{"Kp_GCC":12000.0,"Ki_GCC":4000.0,"Kp_P":5000.0,"Ki_P":500.0,"Kp_Q":0.4,"Ki_Q":20.0,"wp": 20e4}, 40 | "steadystate_values":{"maR0":0.7,"maI0":0.0,"iaR0":0.5,"iaI0":0.01}, 41 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 42 | "xP":0.0,"xQ":0.0, 43 | "xPLL":0.0,"wte":6.28} 44 | }, 45 | 46 | "SolarPVDERThreePhase": 47 | {"parent_config":"", 48 | "basic_specs":{'phases':('a','b','c'),'n_phases':3,'n_ODE':24,'unbalanced':True,"model_type":"SolarPVDERThreePhase"}, 49 | "basic_options":{'t_stable':0.5,'m_steady_state':0.96,"Sinsol":100.0,"current_gradient_limiter":False,"Vrms_measurement_type":"filtered"},#average,minimum,filtered 50 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 51 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 52 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 53 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0,"Kp_DC":-2.0,"Ki_DC":-10.0,"Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4,"Tfilter_Vrms":0.1}, 54 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 55 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 56 | "ibR":0,"ibI":0.0,"xbR":0.0,"xbI":0.0,"ubR":0.0,"ubI":0.0, 57 | "icR":0,"icI":0.0,"xcR":0.0,"xcI":0.0,"ucR":0.0,"ucI":0.0, 58 | "Vdc":550.0,"xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28} 59 | 60 | }, 61 | 62 | "SolarPVDERThreePhaseNoVrmsFilter": 63 | {"parent_config":"", 64 | "basic_specs":{'phases':('a','b','c'),'n_phases':3,'n_ODE':23,'unbalanced':True,"model_type":"SolarPVDERThreePhaseNoVrmsFilter"}, 65 | "basic_options":{'t_stable':0.5,'m_steady_state':0.96,"Sinsol":100.0,"current_gradient_limiter":False,"Vrms_measurement_type":"minimum"}, 66 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 67 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 68 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 69 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0,"Kp_DC":-2.0,"Ki_DC":-10.0,"Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 70 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 71 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 72 | "ibR":0,"ibI":0.0,"xbR":0.0,"xbI":0.0,"ubR":0.0,"ubI":0.0, 73 | "icR":0,"icI":0.0,"xcR":0.0,"xcI":0.0,"ucR":0.0,"ucI":0.0, 74 | "Vdc":550.0,"xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28} 75 | }, 76 | 77 | "SolarPVDERThreePhaseNumba": 78 | {"parent_config":"", 79 | "basic_specs":{'phases':('a','b','c'),'n_phases':3,'n_ODE':23,'unbalanced':True,"model_type":"SolarPVDERThreePhaseNumba"}, 80 | "basic_options":{'t_stable':0.5,'m_steady_state':0.96,"Sinsol":100.0,"current_gradient_limiter":False,"Vrms_measurement_type":"minimum"}, 81 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 82 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 83 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 84 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0,"Kp_DC":-2.0,"Ki_DC":-10.0,"Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 85 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 86 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 87 | "ibR":0,"ibI":0.0,"xbR":0.0,"xbI":0.0,"ubR":0.0,"ubI":0.0, 88 | "icR":0,"icI":0.0,"xcR":0.0,"xcI":0.0,"ucR":0.0,"ucI":0.0, 89 | "Vdc":550.0,"xDC":0.0,"xQ":0.0,"xPLL":0.0,"wte":6.28} 90 | 91 | }, 92 | 93 | 94 | "SolarPVDERThreePhaseBalanced": 95 | {"parent_config":"", 96 | "basic_specs":{'phases':('a','b','c'),'n_phases':3,'n_ODE':11,'unbalanced':False,"model_type":"SolarPVDERThreePhaseBalanced"}, 97 | "basic_options":{'t_stable':0.5,'m_steady_state':0.96,"Sinsol":100.0,"current_gradient_limiter":False,"Vrms_measurement_type":"minimum"}, 98 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 99 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 100 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"C_actual":300.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 101 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0,"Kp_DC":-2.0,"Ki_DC":-10.0,"Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 102 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 103 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 104 | "Vdc":550.0, 105 | "xDC":0.0,"xQ":0.0, 106 | "xPLL":0.0,"wte":6.28} 107 | }, 108 | 109 | "SolarPVDERThreePhaseConstantVdc": 110 | {"parent_config":"", 111 | "basic_specs":{'phases':('a','b','c'),'n_phases':3,'n_ODE':22,'unbalanced':True,"model_type":"SolarPVDERThreePhaseConstantVdc"}, 112 | "basic_options":{'t_stable':0.5,'m_steady_state':0.96,"Sinsol":100.0,"current_gradient_limiter":False,"Vrms_measurement_type":"minimum"}, 113 | "module_parameters":{"Np":11,"Ns":735,"Vdcmpp0":550.0,"Vdcmpp_min": 525.0,"Vdcmpp_max": 650.0}, 114 | "inverter_ratings":{"Srated":50e3,"Vdcrated":550.0,"Ioverload":1.3,"Vrmsrated":177.0,"Iramp_max_gradient_real":1.0,"Iramp_max_gradient_imag":1.0}, 115 | "circuit_parameters":{"Rf_actual":0.002,"Lf_actual":25.0e-6,"R1_actual":0.0019,"X1_actual":0.0561}, 116 | "controller_gains":{"Kp_GCC":6000.0,"Ki_GCC":2000.0,"Kp_P":500.0,"Ki_P":50.0,"Kp_Q":0.2,"Ki_Q":10.0,"wp": 20e4}, 117 | "steadystate_values":{"maR0":0.89,"maI0":0.0,"iaR0":1.0,"iaI0":0.001}, 118 | "initial_states":{"iaR":0,"iaI":0.0,"xaR":0.0,"xaI":0.0,"uaR":0.0,"uaI":0.0, 119 | "ibR":0,"ibI":0.0,"xbR":0.0,"xbI":0.0,"ubR":0.0,"ubI":0.0, 120 | "icR":0,"icI":0.0,"xcR":0.0,"xcI":0.0,"ucR":0.0,"ucI":0.0, 121 | "xP":0.0,"xQ":0.0, 122 | "xPLL":0.0,"wte":6.28} 123 | } 124 | } 125 | 126 | 127 | VRT_config_template = {'LVRT':{'config_id':'', 128 | 'config':{'1':{'V_threshold':0.88, 129 | 't_threshold':5.0, 130 | 't_min_ridethrough':2, 131 | 'mode':'mandatory_operation', 132 | 't_start':0.0, 133 | 'threshold_breach':False}, 134 | '2':{'V_threshold':0.7, 135 | 't_threshold':3.5, 136 | 't_min_ridethrough':1, 137 | 'mode':'mandatory_operation', #'momentary_cessation' 138 | 't_start':0.0, 139 | 'threshold_breach':False}, 140 | '3':{'V_threshold':0.5, 141 | 't_threshold':1.0, 142 | 't_min_ridethrough':0.5, 143 | 'mode':'momentary_cessation', 144 | 't_start':0.0, 145 | 'threshold_breach':False}, 146 | } 147 | }, 148 | 'HVRT':{'config_id':'', 149 | 'config':{'1':{'V_threshold':1.06, 150 | 't_threshold':1.0, 151 | 't_min_ridethrough':0.5, 152 | 'mode':'mandatory_operation', 153 | 't_start':0.0, 154 | 'threshold_breach':False}, 155 | '2':{'V_threshold':1.12, 156 | 't_threshold':0.5, 157 | 't_min_ridethrough':0.5, 158 | 'mode':'mandatory_operation', 159 | 't_start':0.0, 160 | 'threshold_breach':False}, 161 | } 162 | }, 163 | 'VRT_delays':{'config_id':'', 164 | 'config':{'output_cessation_delay':0.01, 165 | 'output_restore_delay':1.75, 166 | 'restore_Vdc':False} 167 | }} 168 | 169 | FRT_config_template = {'LFRT':{'parent_config':'', 170 | 'config':{'1':{'F_LF':57.0, 171 | 't_LF_limit':1/60, 172 | 't_LFstart':0.0}, 173 | '2':{'F_LF':58.8, 174 | 't_LF_limit':299.0, 175 | 't_LFstart':0.0} 176 | }}, 177 | 'HFRT':{'parent_config':'', 178 | 'config':{'1':{'F_HF':61.2, 179 | 't_HF_limit':299.0, 180 | 't_HFstart':0.0}, 181 | '2':{'F_HF':62.0, 182 | 't_HF_limit':1/60, 183 | 't_HFstart':0.0} 184 | }}, 185 | 'FRT_delays':{'parent_config':'', 186 | 'config':{'FRT_INSTANTANEOUS_TRIP':False}} 187 | } 188 | 189 | #Voltage and frequency ride through settings from IEEE 1557-2018 Category III (Table 16, page 48) 190 | #V1 to V2 - zone 2,V1 < - zone 1 SolarPVDER_ThreePhaseConstantVdc 191 | 192 | RT_config_template_old = {'LVRT':{'0':['V_threshold','t_threshold','mode','t_start','threshold_breach']}, 193 | 'HVRT':{'0':['V_threshold','t_threshold','mode','t_start','threshold_breach']}, 194 | 'OUTPUT_CESSATION_DELAY':'','OUTPUT_RESTORE_DELAY':'','RESTORE_Vdc':'', 195 | 'F_LF1':'','F_LF2':'','t_LF1_limit':'','t_LF2_limit':'', 196 | 'F_HF1':'','F_HF2':'', 197 | 't_HF1_limit':'','t_HF2_limit':'', 198 | 'FRT_INSTANTANEOUS_TRIP':'' 199 | } 200 | -------------------------------------------------------------------------------- /pvder/templates_experimental.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Apr 15 13:04:45 2020 4 | 5 | @author: splathottam 6 | """ 7 | 8 | 9 | template={ "phase_quantities":{"ia":complex(0.0,0.0),"ib":complex(0.0,0.0),"ic":complex(0.0,0.0), 10 | "xa":complex(0.0,0.0),"xb":complex(0.0,0.0),"xc":complex(0.0,0.0), 11 | "ua":complex(0.0,0.0),"ub":complex(0.0,0.0),"uc":complex(0.0,0.0)}} -------------------------------------------------------------------------------- /pvder/utilities_experimental.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Apr 13 15:28:24 2020 4 | 5 | @author: splathottam 6 | """ 7 | 8 | from graphviz import Digraph 9 | 10 | from pvder.logutil import LogUtil 11 | 12 | 13 | class ModelUtilities(): 14 | """Class for model wide utilities.""" 15 | 16 | def __init__(self,PV_model,simulation,grid_model=None): 17 | try: 18 | self.PV_model = PV_model 19 | self.simulation = simulation 20 | if self.PV_model.standAlone and grid_model is not None: 21 | self.grid_model = grid_model 22 | elif self.PV_model.standAlone and grid_model is None: 23 | raise ValueError('`Grid` instance need to provided in stand alone mode for creating `GridSimulation` instance!') 24 | except: 25 | LogUtil.exception_handler() 26 | 27 | 28 | def draw_model(self,display_value_type='per_unit'): 29 | """Draw and render the model using graphs.""" 30 | try: 31 | assert display_value_type in ['per_unit','actual'],'Display can only be in per unit or actual values' 32 | self.display_value_type =display_value_type 33 | if self.display_value_type == 'per_unit': 34 | _C = self.PV_model.C 35 | _Lf = self.PV_model.Lf 36 | _Rf = self.PV_model.Rf 37 | _Z1 = self.PV_model.Z1 38 | _Z2 = self.grid_model.Z2 39 | else: 40 | _C = self.PV_model.C_actual 41 | _Lf = self.PV_model.Lf_actual 42 | _Rf = self.PV_model.Rf_actual 43 | _Z1 = self.PV_model.Z1_actual 44 | _Z2 = self.grid_model.Z2_actual 45 | 46 | dot = Digraph(comment='PV_DER and grid_model.') 47 | dot.node('Base','Vbase={},Sbase={},Zbase={:.4f},Lbase={:.4f},Cbase={:.4f}'.format(self.grid_model.Vbase,self.grid_model.Sbase,self.grid_model.Zbase,self.grid_model.Lbase,self.grid_model.Cbase),shape='rectangle') 48 | dot.node('Value_type','{}'.format(self.display_value_type)) 49 | dot.node('DER','{}\nC={:.5f},Lf={:.5f},Rf= {:.4f}'.format(self.PV_model.name,_C,_Lf,_Rf),shape='box') 50 | dot.node('Transformer','{}\nZl = {:.5f}'.format(self.PV_model.transformer_name,_Z1),shape='rectangle') 51 | dot.node('Transmission_Line','{}\nZt = {:.5f}'.format(self.grid_model.transmission_name,_Z2),shape='rectangle') 52 | dot.node('Grid',self.grid_model.name) 53 | 54 | dot.edge('DER', 'Transformer') 55 | dot.edge('Transformer', 'Transmission_Line') 56 | dot.edge('Transmission_Line', 'Grid') 57 | dot.render('model_graphs/model.gv', view=True) 58 | except: 59 | LogUtil.exception_handler() 60 | 61 | 62 | -------------------------------------------------------------------------------- /pvder/utility_classes.py: -------------------------------------------------------------------------------- 1 | """Classes with commonly used methods.""" 2 | 3 | from __future__ import division 4 | 5 | import sys 6 | import time 7 | import six 8 | import pprint 9 | import pdb 10 | import os 11 | 12 | from pvder import templates,specifications 13 | from pvder.defaults import logConfig 14 | from pvder.logutil import LogUtil 15 | 16 | 17 | class Utilities(object): 18 | """ Utility class for common methods.""" 19 | pp = pprint.PrettyPrinter(indent=4) 20 | 21 | def name_instance(self,identifier=''): 22 | """Generate a name for the model instance.""" 23 | 24 | self.ID = self.count #ID is same as current instance count 25 | model_name = type(self).__name__ 26 | 27 | if model_name in templates.DER_design_template: 28 | self.name = model_name + '_' + str(self.ID) #Object name 29 | elif model_name == 'DynamicSimulation': 30 | self.name = 'sim_'+str(self.ID) #Object name 31 | elif model_name == 'SimulationEvents': 32 | self.name = 'events_'+str(self.ID) #Object name 33 | elif model_name == 'SimulationResults': 34 | self.name = 'results_'+str(self.ID) #Object name 35 | else: 36 | raise ValueError('{} is not a valid instance name.'.format(model_name)) 37 | self.name = str(identifier) + '-' +self.name #Add additional identifier to object name if it was provided 38 | 39 | def initialize_logger(self,logging_level): 40 | """Initialize loggers for different classes.""" 41 | self.verbosity = logging_level 42 | 43 | logConfig['logLevel']= specifications.logging_levels_dict[self.verbosity] 44 | LogUtil.set_logger(logConfig['logLevel'],logConfig['mode']) 45 | 46 | # @property 47 | # def verbosity(self): 48 | # return self.__verbosity 49 | 50 | # @verbosity.setter 51 | # def verbosity(self,verbosity): 52 | # """Method to set verbosity of logging on terminal. Different classes may have different levels of verbosity.""" 53 | # self.__verbosity=10 54 | # return self.__verbosity 55 | 56 | 57 | -------------------------------------------------------------------------------- /pvder/utility_functions.py: -------------------------------------------------------------------------------- 1 | """Commonly used calculations on electrical quantities.""" 2 | 3 | from __future__ import division 4 | import numpy as np 5 | import math 6 | import cmath 7 | import sys 8 | import time 9 | import six 10 | import json 11 | import logging 12 | import scipy.io as sio 13 | from pvder.logutil import LogUtil 14 | 15 | 16 | def Urms_time_series(ua,ub,uc): 17 | """Function to calculate rms value of phasor quantities.""" 18 | try: 19 | assert len(ua) == len(ub) == len(uc), " The number of phasor quantities should be equal" 20 | return np.sqrt((np.square(np.abs(ua))+np.square(np.abs(ub))+np.square(np.abs(uc)))/3.0)/math.sqrt(2) 21 | except: 22 | LogUtil.exception_handler() 23 | 24 | 25 | def Uphrms_time_series(uph): 26 | """Function to calculate rms value of single phasor quantity.""" 27 | try: 28 | return np.abs(uph)/math.sqrt(2) 29 | except: 30 | LogUtil.exception_handler() 31 | 32 | 33 | def Uabsolute_time_series(u): 34 | """Function to calculate rms value of phasor quantities.""" 35 | try: 36 | return np.abs(u) 37 | except: 38 | LogUtil.exception_handler() 39 | 40 | 41 | def Uunbalance_calc(ua,ub,uc): 42 | """Calculate voltage/current unbalance.""" 43 | try: 44 | uavg = (ua + ub + uc)/3 45 | return (max(ua,ub,uc) - min(ua,ub,uc))/uavg 46 | except: 47 | LogUtil.exception_handler() 48 | 49 | 50 | #@jit(nopython=True) 51 | def Ppv_calc(Iph,Np,Ns,Vdc_actual,Tactual,Sbase): 52 | """Function to calculate PV module power output.""" 53 | try: 54 | Irs = 1.2e-7 #Cell reverse saturation current 55 | q = 1.602e-19 #Charge of an electron 56 | k = 1.38e-23 #Boltzmann's constant 57 | A = 1.92 #p-n junction ideality factor 58 | Ipv = (Np*Iph)-(Np*Irs*(math.exp((q*Vdc_actual)/(k*Tactual*A*Ns))-1)) #Faster with Pure Python functions 59 | return max(0,(Ipv*Vdc_actual))/Sbase 60 | except: 61 | LogUtil.exception_handler() 62 | 63 | 64 | #@jit(nopython=True) 65 | def S_calc(va,vb,vc,ia,ib,ic): 66 | """Function to calculate apparent power.""" 67 | try: 68 | return (1/2)*(va*ia.conjugate() + vb*ib.conjugate() + vc*ic.conjugate())*1.0 69 | except: 70 | LogUtil.exception_handler() 71 | 72 | 73 | #Average duty cycle - Phase A 74 | #@jit(nopython=True) 75 | def m_calc(Kp_GCC,u,x): 76 | """Duty cycle for a single phase.""" 77 | try: 78 | return Kp_GCC*u + x #PI controller equation 79 | except: 80 | LogUtil.exception_handler() 81 | 82 | 83 | #@jit(nopython=True) 84 | def Urms_calc(ua,ub,uc): 85 | """Function to calculate rms value of scalar phasor quantities.""" 86 | try: 87 | return math.sqrt((pow(abs(ua),2)+pow(abs(ub),2)+pow(abs(uc),2))/3.0)/math.sqrt(2) #Pure python implementation is faster 88 | except: 89 | LogUtil.exception_handler() 90 | 91 | 92 | def Urms_min_calc(ua,ub,uc): 93 | """Function to calculate minimum of rms value of scalar phasor quantities.""" 94 | try: 95 | return min(abs(ua),abs(ub),abs(uc))/math.sqrt(2) #Pure python implementation is faster 96 | except: 97 | LogUtil.exception_handler() 98 | 99 | 100 | def Urms_calc_1phase(ua): 101 | """Function to calculate rms value of scalar phasor quantities for single phase.""" 102 | try: 103 | return abs(ua)/math.sqrt(2) #Pure python implementation is faster 104 | except: 105 | LogUtil.exception_handler() 106 | 107 | 108 | #@jit(nopython=True) 109 | def Ub_calc(Ua): 110 | """Convert phase A quantity to Phase B.""" 111 | try: 112 | return Ua*np.power(np.e,1j*(-(2/3)*np.pi)) 113 | #return Ua*pow(math.e,1j*(-(2/3)*math.pi)) #Shift by -120 degrees 114 | #@jit(nopython=True) 115 | except: 116 | LogUtil.exception_handler() 117 | 118 | 119 | def Uc_calc(Ua): 120 | """Convert phase A quantity to Phase C.""" 121 | try: 122 | return Ua*np.power(np.e,1j*((2/3)*np.pi)) 123 | #return Ua*pow(math.e,1j*((2/3)*math.pi)) #Shift by -120 degrees 124 | except: 125 | LogUtil.exception_handler() 126 | 127 | 128 | def relative_phase_calc(Uph1,Uph2,DEGREES=False): 129 | """Calculate relative phase between phasors between 0 to 2pi or 0 to 360 degrees.""" 130 | try: 131 | if DEGREES: 132 | del_phase = math.degrees(cmath.phase(Uph1)-cmath.phase(Uph2)) % 360 133 | else: 134 | del_phase = math.radians(math.degrees(cmath.phase(Uph1)-cmath.phase(Uph2)) % 360) 135 | return del_phase 136 | except: 137 | LogUtil.exception_handler() 138 | 139 | 140 | #@jit(nopython=True) 141 | def phasor_to_time(upha = 1+1j*0.0,uphb = -0.5-1j*0.867,uphc = -0.5+1j*0.867,w=2.0*math.pi*60.0,t=0.0): 142 | """Convert a,b,c quantities from phasor domain to time domain.""" 143 | try: 144 | ra,pha = cmath.polar(upha) 145 | rb,phb = cmath.polar(uphb) 146 | rc,phc = cmath.polar(uphc) 147 | #ua = (ra*np.exp(1j*(w*t+pha-(math.pi/2)))).real 148 | #ub = (rb*np.exp(1j*(w*t+phb-(math.pi/2)))).real 149 | #uc = (rc*np.exp(1j*(w*t+phc-(math.pi/2)))).real 150 | ua = ra*pow(math.e,1j*(w*t+pha-(math.pi/2))).real 151 | ub = rb*pow(math.e,1j*(w*t+phb-(math.pi/2))).real 152 | uc = rc*pow(math.e,1j*(w*t+phc-(math.pi/2))).real 153 | return ua,ub,uc 154 | except: 155 | LogUtil.exception_handler() 156 | 157 | 158 | def phasor_to_time_1phase(uph,w,t): 159 | """Convert a,b,c quantities (time series) from phasor domain to time domain.""" 160 | try: 161 | r,ph = cmath.polar(uph) 162 | return r*pow(math.e,1j*(w*t+ph-(math.pi/2))).real 163 | except: 164 | LogUtil.exception_handler() 165 | 166 | 167 | #@jit(nopython=True) 168 | def abc_to_dq0(ua,ub,uc,wt=2*math.pi): 169 | """Convert to d-q.""" 170 | try: 171 | Us = (2/3)*(ua + ub*pow(math.e,1j*((2/3)*math.pi)) + uc*pow(math.e,1j*(-(2/3)*math.pi)))*pow(math.e,1j*(-wt)) 172 | ud = Us.real 173 | uq = Us.imag 174 | u0 = (1/3)*(ua+ub+uc) 175 | return ud,uq,u0 176 | except: 177 | LogUtil.exception_handler() 178 | 179 | 180 | def dq0_to_abc(ud,uq,u0,wt=2*math.pi): 181 | """Convert to abc.""" 182 | try: 183 | ua = ud*math.cos(wt) - uq*math.sin(wt) + u0 184 | ub = ud*math.cos(wt-(2/3)*math.pi) - uq*math.sin(wt-(2/3)*math.pi) + u0 185 | uc = ud*math.cos(wt+(2/3)*math.pi) - uq*math.sin(wt+(2/3)*math.pi) + u0 186 | return ua,ub,uc 187 | except: 188 | LogUtil.exception_handler() 189 | 190 | 191 | def alpha_beta_to_d_q(ualpha,ubeta,wt): 192 | """Convert alpha-beta to d-q.""" 193 | try: 194 | Us = (ualpha + 1j*ubeta)*pow(math.e,-1j*(wt)) 195 | #Us = (ualpha + 1j*ubeta)*pow(math.e,-1j*(wt-(math.pi/2))) 196 | 197 | ud = Us.real 198 | uq = Us.imag 199 | 200 | #print(ud,uq) 201 | #ud = ualpha*math.sin(wt) - ubeta*math.cos(wt) 202 | #uq = ualpha*math.cos(wt) + ubeta*math.sin(wt) 203 | 204 | #ud = ualpha*math.cos(wt) + ubeta*math.sin(wt) 205 | #uq = -ualpha*math.sin(wt) + ubeta*math.cos(wt) 206 | 207 | return ud,uq 208 | except: 209 | LogUtil.exception_handler() 210 | 211 | 212 | def phasor_to_symmetrical(upha,uphb,uphc): 213 | """Convert to zero sequence.""" 214 | try: 215 | a = pow(math.e,1j*((2/3)*math.pi)) 216 | aa = pow(math.e,1j*((4/3)*math.pi)) 217 | u0 = (1/3)*(upha + uphb + uphc) 218 | u1 = (1/3)*(upha + a*uphb + (aa)*uphc) #Positive sequence 219 | u2 = (1/3)*(upha + (aa)*uphb + a*uphc) #Negative sequence 220 | 221 | return u0,u1,u2 222 | except: 223 | LogUtil.exception_handler() 224 | 225 | 226 | def phasor_to_zero_sequence(upha,uphb,uphc): 227 | """Convert to zero sequence.""" 228 | try: 229 | u0,u1,u2 = phasor_to_symmetrical(upha,uphb,uphc) 230 | return u0,u0,u0 231 | except: 232 | LogUtil.exception_handler() 233 | 234 | 235 | def phasor_to_positive_sequence(upha,uphb,uphc): 236 | """Convert to positive sequence.""" 237 | try: 238 | u0,u1,u2 = phasor_to_symmetrical(upha,uphb,uphc) 239 | return u1,u1*pow(math.e,1j*((4/3)*math.pi)),u1*pow(math.e,1j*((2/3)*math.pi)) 240 | except: 241 | LogUtil.exception_handler() 242 | 243 | 244 | def phasor_to_negative_sequence(upha,uphb,uphc): 245 | """Convert to negative sequence.""" 246 | try: 247 | u0,u1,u2 = phasor_to_symmetrical(upha,uphb,uphc) 248 | return u2,u2*pow(math.e,1j*((4/3)*math.pi)),u2*pow(math.e,1j*((2/3)*math.pi)) 249 | except: 250 | LogUtil.exception_handler() 251 | 252 | 253 | def Vinv_terminal_time_series(m_t,Vdc_t): 254 | """Function to generate time series inverter terminal voltage.""" 255 | try: 256 | assert len(m_t) == len(Vdc_t) != None 257 | return m_t*(Vdc_t/2) 258 | except: 259 | LogUtil.exception_handler() 260 | 261 | def m_time_series(u_t,x_t,Kp_GCC): 262 | """Function to generate time series inverter terminal voltage.""" 263 | try: 264 | assert len(u_t) == len(x_t) != None 265 | return Kp_GCC*u_t + x_t 266 | except: 267 | LogUtil.exception_handler() 268 | 269 | 270 | def extract_matlab_file(file_name,series_label): 271 | """Program to extract contents of .mat file having structure with time format.""" 272 | try: 273 | matlab_file = sio.loadmat(file_name) 274 | content_list = [] 275 | sim_time = matlab_file[series_label][0,0][0] 276 | n_labels = len(matlab_file[series_label][0,0][1][0]) 277 | for i in range(n_labels): 278 | content_list.append(matlab_file[series_label][0,0][1][0,i][0]) 279 | return sim_time,content_list 280 | except: 281 | LogUtil.exception_handler() 282 | 283 | 284 | def limit_complex(z,low_limit=-1.0,high_limit=1.0): 285 | """Check if complex number is within limits.""" 286 | try: 287 | assert low_limit < high_limit,'low limit should be higher than high limit' 288 | r,phi = cmath.polar(z) 289 | r = min(max(r,low_limit),high_limit) 290 | 291 | #z_real = min(max(z.real,low_limit),high_limit) 292 | #z_imag = min(max(z.imag,low_limit),high_limit) 293 | #z_imag = z.imag 294 | #return z_real + z_imag*1j 295 | return cmath.rect(r, phi) 296 | except: 297 | LogUtil.exception_handler() 298 | 299 | 300 | def limit_complex_time_series(z,low_limit=-1.0,high_limit=1.0): 301 | """Check if complex number is within limits.""" 302 | try: 303 | assert low_limit < high_limit,'low limit should be higher than high limit' 304 | #z_real = np.minimum(np.maximum(z.real,np.full(len(z),-1)),np.full(len(z),1)) 305 | #z_imag = np.minimum(np.maximum(z.imag,np.full(len(z),-1)),np.full(len(z),1)) 306 | r = np.absolute(z) 307 | phi = np.angle(z) 308 | r = np.minimum(np.maximum(r,np.full(len(r),low_limit)),np.full(len(r),high_limit)) 309 | 310 | #z_imag = z.imag 311 | return r*np.cos(phi) + r*np.sin(phi)*1j 312 | except: 313 | LogUtil.exception_handler() 314 | 315 | 316 | def print_to_terminal(text_string='Printing to terminal!'): 317 | """Print to terminal.""" 318 | try: 319 | sys.__stdout__.write(text_string+'\n') 320 | sys.__stdout__.flush() #Flush buffer to terminal 321 | except: 322 | LogUtil.exception_handler() 323 | 324 | 325 | def print_dictionary_keys(dictionary,dictionary_name): 326 | """Print dictionary.""" 327 | try: 328 | print(dictionary_name,':',','.join(dictionary)) 329 | except: 330 | LogUtil.exception_handler() 331 | 332 | 333 | def read_json(file_name): 334 | """Load json file and return dictionary.""" 335 | try: 336 | if six.PY3: 337 | string_type = (str) 338 | elif six.PY2: 339 | string_type = (str,unicode) 340 | assert isinstance(file_name,string_type),'File name should be a string!' 341 | assert file_name[-4:]=='json','File should be a JSON file!' 342 | with open(file_name, "r") as config_file: 343 | #logger.debug('Reading configuration file:{}'.format(config_file.name)) 344 | confDict = json.load(config_file) 345 | 346 | return confDict 347 | except: 348 | LogUtil.exception_handler() 349 | 350 | 351 | def get_logger(logging_level): 352 | """Get logger.""" 353 | try: 354 | logger=logging.getLogger() #Creating an object 355 | logger.setLevel(eval('logging.'+logging_level)) #Setting the threshold of logger to DEBUG 356 | 357 | return logger 358 | except: 359 | LogUtil.exception_handler() 360 | 361 | def find_first_order_time_constant(t_t,y_t,t_event): 362 | """Find first_order time constant from time series data""" 363 | try: 364 | for t,y in zip(t_t,y_t): 365 | if y<(y_t[0]-(y_t[0]-y_t[-1])*.63): 366 | print("Time constant:{:.3f},Value at time constant:{:.3f},Expected value at time constant:{:.3f}".format(t-t_event,y,(y_t[0]-(y_t[0]-y_t[-1])*.63))) 367 | break 368 | except: 369 | LogUtil.exception_handler() 370 | -------------------------------------------------------------------------------- /pvder/utility_functions_numba.py: -------------------------------------------------------------------------------- 1 | """Commonly used calculations on electrical quantities.""" 2 | 3 | from __future__ import division 4 | import numpy as np 5 | import math 6 | import cmath 7 | import sys 8 | import time 9 | import six 10 | import json 11 | import logging 12 | import os 13 | import glob 14 | import scipy.io as sio 15 | 16 | from pvder.logutil import LogUtil 17 | import numba 18 | from numba import float32, float64, complex64,complex128 19 | 20 | debug_flag = False 21 | cache_flag = True 22 | 23 | def check_numba_cache(cache_folder): 24 | """Check Numba cache folder""" 25 | 26 | files = glob.glob(os.path.join(cache_folder,'*.nbi')) + glob.glob(os.path.join(cache_folder,'*.nbc')) 27 | print("Numba cache folder contains {} *.nbi,*.nbc files".format(len(files))) 28 | 29 | def clear_numba_cache(cache_folder): 30 | """Clear Numba cache folder""" 31 | 32 | files = glob.glob(os.path.join(cache_folder,'*.nbi')) + glob.glob(os.path.join(cache_folder,'*.nbc')) 33 | print("Clearing {} *.nbi,*.nbc files from Cache".format(len(files))) 34 | for f in files: 35 | os.remove(f) 36 | files = glob.glob(os.path.join(cache_folder,'*.nbi')) + glob.glob(os.path.join(cache_folder,'*.nbc')) 37 | 38 | if len(files) >0: 39 | print("Following files could not be cleared:{}".format(files)) 40 | 41 | @numba.njit(cache=cache_flag) 42 | def Uunbalance_calc(ua,ub,uc): 43 | """Calculate voltage/current unbalance.""" 44 | uavg = (ua + ub + uc)/3 45 | return (max(ua,ub,uc) - min(ua,ub,uc))/uavg 46 | 47 | @numba.njit(cache=cache_flag) 48 | def Ppv_calc(Iph,Np,Ns,Vdc_actual,Tactual,Sbase): 49 | """Function to calculate PV module power output.""" 50 | Irs = 1.2e-7 #Cell reverse saturation current 51 | q = 1.602e-19 #Charge of an electron 52 | k = 1.38e-23 #Boltzmann's constant 53 | A = 1.92 #p-n junction ideality factor 54 | Ipv = (Np*Iph)-(Np*Irs*(math.exp((q*Vdc_actual)/(k*Tactual*A*Ns))-1)) #Faster with Pure Python functions 55 | return max(0,(Ipv*Vdc_actual))/Sbase 56 | 57 | @numba.njit(cache=cache_flag) 58 | def we_calc(xPLL,vd,Kp_PLL): 59 | """Calculate inverter frequency from PLL.""" 60 | return (Kp_PLL*(vd) + xPLL + 2*math.pi*60.0)#/wbase 61 | 62 | @numba.njit(cache=cache_flag) 63 | def S_calc(va,vb,vc,ia,ib,ic): 64 | """Function to calculate apparent power.""" 65 | return (1/2)*(va*ia.conjugate() + vb*ib.conjugate() + vc*ic.conjugate())*1.0 66 | 67 | @numba.njit(cache=cache_flag) 68 | def S_load_calc(va,vb,vc,Zload): 69 | """Power absorbed by load at PCC LV side.""" 70 | return (1/2)*(va*(-(va/Zload)).conjugate() + vb*(-(vb/Zload)).conjugate() + vc*(-(vc/Zload)).conjugate()) 71 | 72 | @numba.njit(cache=cache_flag) 73 | def S_G_calc(va,vb,vc,vag,vbg,vcg,ia,ib,ic,Zload,a): 74 | """Power absorbed/produced by grid voltage source.""" 75 | return (1/2)*((-(ia-(va/Zload))/a).conjugate()*vag+(-(ib-(vb/Zload))/a).conjugate()*vbg+(-(ic-(vc/Zload))/a).conjugate()*vcg) 76 | 77 | #Average duty cycle - Phase A 78 | @numba.njit(cache=cache_flag) #complex128(float32, complex128 ,complex128), 79 | def m_calc(Kp_GCC,u,x): 80 | """Duty cycle for a single phase.""" 81 | return Kp_GCC*u + x #PI controller equation 82 | 83 | @numba.njit(cache=cache_flag) #float32(complex128,complex128,complex128), 84 | def ia_ref_Vdc_Q_control_calc(xDC,xQ,Vdc,S,Vdc_ref,Q_ref,Kp_DC,Kp_Q): 85 | """Phase A current reference""" 86 | return xDC + Kp_DC*(Vdc_ref - Vdc) + 1j*(xQ - Kp_Q*(Q_ref - S.imag)) #PI controller equation 87 | 88 | @numba.njit(cache=cache_flag) #float32(complex128,complex128,complex128), 89 | def i_load_calc(v,Zload): 90 | """Current counsumed by load connected at PCC LV side - Phase A/B/C.""" 91 | return v/Zload 92 | 93 | @numba.njit(cache=cache_flag) #complex128(complex128,float32), 94 | def vt_calc(m,Vdc): 95 | """Inverter terminal voltage - Phase A/B/C""" 96 | return m*(Vdc/2) 97 | 98 | @numba.njit(cache=cache_flag) #float32(complex128,complex128,complex128), 99 | def Vabrms_calc(va,vb): 100 | """Inverter terminal voltage - line to line RMS""" 101 | return abs(va-vb)/math.sqrt(2) 102 | 103 | @numba.njit(cache=cache_flag) #float32(complex128,complex128,complex128), 104 | def Urms_calc(ua,ub,uc): 105 | """Function to calculate rms value of scalar phasor quantities.""" 106 | return math.sqrt((pow(abs(ua),2)+pow(abs(ub),2)+pow(abs(uc),2))/3.0)/math.sqrt(2) #Pure python implementation is faster 107 | 108 | @numba.njit(cache=cache_flag) 109 | def Urms_min_calc(ua,ub,uc): 110 | """Function to calculate minimum of rms value of scalar phasor quantities.""" 111 | return min(abs(ua),abs(ub),abs(uc))/math.sqrt(2) #Pure python implementation is faster 112 | 113 | @numba.njit(cache=cache_flag) 114 | def Urms_calc_1phase(ua): 115 | """Function to calculate rms value of scalar phasor quantities for single phase.""" 116 | return abs(ua)/math.sqrt(2) #Pure python implementation is faster 117 | 118 | @numba.njit(cache=cache_flag) 119 | def Ub_calc(Ua): 120 | """Convert phase A quantity to Phase B.""" 121 | return Ua*np.power(np.e,1j*(-(2/3)*np.pi)) 122 | #return Ua*pow(math.e,1j*(-(2/3)*math.pi)) #Shift by -120 degrees 123 | 124 | @numba.njit(cache=cache_flag) 125 | def Uc_calc(Ua): 126 | """Convert phase A quantity to Phase C.""" 127 | return Ua*np.power(np.e,1j*((2/3)*np.pi)) 128 | #return Ua*pow(math.e,1j*((2/3)*math.pi)) #Shift by -120 degrees 129 | 130 | @numba.njit(cache=cache_flag) 131 | def relative_phase_calc(Uph1,Uph2,DEGREES=False): 132 | """Calculate relative phase between phasors between 0 to 2pi or 0 to 360 degrees.""" 133 | if DEGREES: 134 | del_phase = math.degrees(cmath.phase(Uph1)-cmath.phase(Uph2)) % 360 135 | else: 136 | del_phase = math.radians(math.degrees(cmath.phase(Uph1)-cmath.phase(Uph2)) % 360) 137 | return del_phase 138 | 139 | 140 | @numba.njit(cache=cache_flag) 141 | def phasor_to_time(upha = 1+1j*0.0,uphb = -0.5-1j*0.867,uphc = -0.5+1j*0.867,w=2.0*math.pi*60.0,t=0.0): 142 | """Convert a,b,c quantities from phasor domain to time domain.""" 143 | ra,pha = cmath.polar(upha) 144 | rb,phb = cmath.polar(uphb) 145 | rc,phc = cmath.polar(uphc) 146 | #ua = (ra*np.exp(1j*(w*t+pha-(math.pi/2)))).real 147 | #ub = (rb*np.exp(1j*(w*t+phb-(math.pi/2)))).real 148 | #uc = (rc*np.exp(1j*(w*t+phc-(math.pi/2)))).real 149 | ua = ra*pow(math.e,1j*(w*t+pha-(math.pi/2))).real 150 | ub = rb*pow(math.e,1j*(w*t+phb-(math.pi/2))).real 151 | uc = rc*pow(math.e,1j*(w*t+phc-(math.pi/2))).real 152 | return ua,ub,uc 153 | 154 | @numba.njit(cache=cache_flag) 155 | def phasor_to_time_1phase(uph,w,t): 156 | """Convert a,b,c quantities (time series) from phasor domain to time domain.""" 157 | r,ph = cmath.polar(uph) 158 | return r*pow(math.e,1j*(w*t+ph-(math.pi/2))).real 159 | 160 | @numba.njit(cache=cache_flag) 161 | def abc_to_dq0(ua,ub,uc,wt=2*math.pi): 162 | """Convert to d-q.""" 163 | Us = (2/3)*(ua + ub*pow(math.e,1j*((2/3)*math.pi)) + uc*pow(math.e,1j*(-(2/3)*math.pi)))*pow(math.e,1j*(-wt)) 164 | ud = Us.real 165 | uq = Us.imag 166 | u0 = (1/3)*(ua+ub+uc) 167 | return ud,uq,u0 168 | 169 | @numba.njit(cache=cache_flag) 170 | def dq0_to_abc(ud,uq,u0,wt=2*math.pi): 171 | """Convert to abc.""" 172 | ua = ud*math.cos(wt) - uq*math.sin(wt) + u0 173 | ub = ud*math.cos(wt-(2/3)*math.pi) - uq*math.sin(wt-(2/3)*math.pi) + u0 174 | uc = ud*math.cos(wt+(2/3)*math.pi) - uq*math.sin(wt+(2/3)*math.pi) + u0 175 | return ua,ub,uc 176 | 177 | @numba.njit(cache=cache_flag) 178 | def alpha_beta_to_d_q(ualpha,ubeta,wt): 179 | """Convert alpha-beta to d-q.""" 180 | Us = (ualpha + 1j*ubeta)*pow(math.e,-1j*(wt)) 181 | #Us = (ualpha + 1j*ubeta)*pow(math.e,-1j*(wt-(math.pi/2))) 182 | 183 | ud = Us.real 184 | uq = Us.imag 185 | 186 | #ud = ualpha*math.sin(wt) - ubeta*math.cos(wt) 187 | #uq = ualpha*math.cos(wt) + ubeta*math.sin(wt) 188 | 189 | #ud = ualpha*math.cos(wt) + ubeta*math.sin(wt) 190 | #uq = -ualpha*math.sin(wt) + ubeta*math.cos(wt) 191 | 192 | return ud,uq 193 | 194 | @numba.njit(cache=cache_flag) 195 | def phasor_to_symmetrical(upha,uphb,uphc): 196 | """Convert to zero sequence.""" 197 | 198 | a = pow(math.e,1j*((2/3)*math.pi)) 199 | aa = pow(math.e,1j*((4/3)*math.pi)) 200 | u0 = (1/3)*(upha + uphb + uphc) 201 | u1 = (1/3)*(upha + a*uphb + (aa)*uphc) #Positive sequence 202 | u2 = (1/3)*(upha + (aa)*uphb + a*uphc) #Negative sequence 203 | 204 | return u0,u1,u2 205 | 206 | @numba.njit(cache=cache_flag) 207 | def phasor_to_zero_sequence(upha,uphb,uphc): 208 | """Convert to zero sequence.""" 209 | u0,u1,u2 = phasor_to_symmetrical(upha,uphb,uphc) 210 | return u0,u0,u0 211 | 212 | @numba.njit(cache=cache_flag) 213 | def phasor_to_positive_sequence(upha,uphb,uphc): 214 | """Convert to positive sequence.""" 215 | u0,u1,u2 = phasor_to_symmetrical(upha,uphb,uphc) 216 | return u1,u1*pow(math.e,1j*((4/3)*math.pi)),u1*pow(math.e,1j*((2/3)*math.pi)) 217 | 218 | @numba.njit(cache=cache_flag) 219 | def phasor_to_negative_sequence(upha,uphb,uphc): 220 | """Convert to negative sequence.""" 221 | u0,u1,u2 = phasor_to_symmetrical(upha,uphb,uphc) 222 | return u2,u2*pow(math.e,1j*((4/3)*math.pi)),u2*pow(math.e,1j*((2/3)*math.pi)) 223 | 224 | @numba.njit(cache=cache_flag) 225 | def limit_complex(z,low_limit=-1.0,high_limit=1.0): 226 | """Check if complex number is within limits.""" 227 | #assert low_limit < high_limit,'low limit should be higher than high limit' 228 | r,phi = cmath.polar(z) 229 | r = min(max(r,low_limit),high_limit) 230 | 231 | #z_real = min(max(z.real,low_limit),high_limit) 232 | #z_imag = min(max(z.imag,low_limit),high_limit) 233 | #z_imag = z.imag 234 | #return z_real + z_imag*1j 235 | return cmath.rect(r, phi) 236 | 237 | 238 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scipy>=1.0.0 2 | numpy>=1.15.1 3 | matplotlib>=2.2.2 4 | xlsxwriter>=1.1.5 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | # The text of the README file 5 | f=open(os.path.join(os.path.dirname(os.path.abspath(__file__)),'README.md')) 6 | README=f.read() 7 | f.close() 8 | 9 | setup(name='pvder', 10 | version=open("pvder/_version.py").readlines()[-1].split()[-1].strip("\"'"), 11 | packages=['pvder',], 12 | include_package_data=True, 13 | description='Utility for simulating PV-DER', 14 | long_description=README, 15 | long_description_content_type="text/markdown", 16 | url ='https://github.com/tdcosim/SolarPV-DER-simulation-tool', 17 | author = 'Siby Jose Plathottam', 18 | author_email='sibyjackgrove@gmail.com', 19 | license= 'LICENSE.txt', 20 | classifiers=[ 21 | 'License :: OSI Approved :: BSD License', 22 | 'Intended Audience :: Science/Research', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.11', 25 | ], 26 | install_requires=['scipy>=1.0.0','numpy>=1.15.1','matplotlib>=2.0.2'],#And any other dependencies required 27 | extras_require={"docs": ['sphinx-rtd-theme','nbsphinx','nbsphinx-link'], 28 | "numba":['numba>=0.53.0']} 29 | ) 30 | -------------------------------------------------------------------------------- /tests/test_PVDER_DynamicSimulation.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import sys 3 | import os 4 | import argparse 5 | import logging 6 | import unittest 7 | 8 | import math 9 | 10 | import matplotlib.pyplot as plt 11 | 12 | from pvder.DER_components_three_phase import SolarPVDERThreePhase 13 | from pvder.grid_components import Grid 14 | from pvder.dynamic_simulation import DynamicSimulation 15 | from pvder.simulation_events import SimulationEvents 16 | from pvder.simulation_utilities import SimulationResults 17 | 18 | from unittest_utilities import show_DER_status, plot_DER_trajectories 19 | config_file = r'..\config_der.json' 20 | 21 | def suite(): 22 | """Define a test suite.""" 23 | 24 | all_tests = ['test_init','test_run_simulation'] 25 | 26 | avoid_tests = [] 27 | 28 | tests = list(set(all_tests) - set(avoid_tests)) 29 | print('Following unittest scenarios will be run:{}'.format(tests)) 30 | suite = unittest.TestSuite() 31 | 32 | for test in tests: 33 | suite.addTest(TestPVDER(test)) 34 | 35 | return suite 36 | 37 | class TestPVDER(unittest.TestCase): 38 | 39 | Vnominal = 1.0 40 | 41 | scaler = 0.835 42 | Va = (.50+0j)*Grid.Vbase*scaler 43 | Vb = (-.25-.43301270j)*Grid.Vbase*scaler 44 | Vc = (-.25+.43301270j)*Grid.Vbase*scaler 45 | 46 | power_rating = 50.0e3 47 | Vrms = abs(Va)/math.sqrt(2) 48 | 49 | wgrid = 2*math.pi*60.0 50 | events = SimulationEvents() 51 | 52 | flag_arguments = {'standAlone': False, 53 | 'steadyStateInitialization':True, 54 | 'verbosity':'DEBUG'} 55 | ratings_arguments ={'powerRating':power_rating, 56 | 'VrmsRating':Vrms} 57 | voltage_arguments = {'gridVoltagePhaseA': Va, 58 | 'gridVoltagePhaseB':Vb, 59 | 'gridVoltagePhaseC':Vc, 60 | 'gridFrequency':wgrid} 61 | 62 | def test_init(self): 63 | """Test Dynamic Simulation initialization.""" 64 | events = SimulationEvents() 65 | kwargs={} 66 | kwargs.update(self.flag_arguments) 67 | kwargs.update(self.ratings_arguments) 68 | kwargs.update(self.voltage_arguments) 69 | PVDER = SolarPVDERThreePhase(events = events,configFile=config_file,**kwargs) 70 | 71 | sim = DynamicSimulation(PV_model=PVDER,events = events,jacFlag = True,verbosity = 'DEBUG',solverType='odeint') 72 | self.assertIsInstance(sim, DynamicSimulation) 73 | self.assertTrue(sim.jacFlag) 74 | 75 | def test_run_simulation(self): 76 | """Test run simulation method.""" 77 | 78 | events = SimulationEvents() 79 | kwargs={} 80 | kwargs.update(self.flag_arguments) 81 | kwargs.update(self.ratings_arguments) 82 | kwargs.update(self.voltage_arguments) 83 | PVDER = SolarPVDERThreePhase(events = events,configFile=config_file,**kwargs) 84 | 85 | sim = DynamicSimulation(PV_model=PVDER,events = events, 86 | jacFlag = True,verbosity = 'DEBUG',solverType='odeint') 87 | sim.tStop = 10.0 88 | sim.tInc = 1/120. 89 | sim.run_simulation() 90 | 91 | self.assertEqual(sim.t[-1],sim.tStop) 92 | self.assertTrue(sim.SOLVER_CONVERGENCE) 93 | 94 | 95 | if __name__ == '__main__': 96 | #unittest.main() 97 | logging.debug('test') 98 | runner = unittest.TextTestRunner() 99 | runner.run(suite()) 100 | -------------------------------------------------------------------------------- /tests/test_PVDER_HVRT.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import sys 3 | import os 4 | import math 5 | import argparse 6 | 7 | import matplotlib.pyplot as plt 8 | import unittest 9 | 10 | from pvder.DER_components_single_phase import SolarPVDERSinglePhase 11 | from pvder.DER_components_three_phase import SolarPVDERThreePhase 12 | from pvder.grid_components import Grid 13 | from pvder.dynamic_simulation import DynamicSimulation 14 | from pvder.simulation_events import SimulationEvents 15 | from pvder.simulation_utilities import SimulationResults 16 | 17 | from unittest_utilities import show_DER_status, plot_DER_trajectories 18 | 19 | #working_folder = os.path.dirname(os.path.dirname(os.path.dirname(os.getcwd()))) 20 | #module_folder = os.path.dirname(os.path.abspath(__file__)) 21 | 22 | #sys.path.append(module_folder) 23 | #os.environ['PATH']+=';'+module_folder 24 | config_file = r'..\config_der.json' 25 | 26 | def suite(): 27 | """Define a test suite.""" 28 | avoid_tests = ['HVRT2','HVRT3'] 29 | tests = test_options.options 30 | tests = list(set(tests) - set(avoid_tests)) 31 | print('Following unittest scenarios will be run:{}'.format(tests)) 32 | suite = unittest.TestSuite() 33 | 34 | for test in tests: 35 | suite.addTest(TestPVDER('test_PVDER_'+ test)) 36 | 37 | return suite 38 | 39 | class TestPVDER(unittest.TestCase): 40 | 41 | Vnominal = 1.0 42 | default_setup_scenario = 'default' 43 | default_test_scenario = 'default' 44 | 45 | setup_scenarios ={'default':{'powerRating':50e3,'SinglePhase':False,'steadyStateInitialization':True,'n_DER':1}, 46 | 'case1':{'SinglePhase':True}, 47 | 'case2':{'SteadyState':False}} 48 | 49 | test_scenarios = {'default':{'HVRT_ENABLE':True,'tEnd':5.0,'Vnominal':1.0,'Vspike':1.1}, 50 | 'HVRT1':{'HVRT_ENABLE':True,'tEnd':10.0,'tspike_start':2.0,'tspike_duration':0.5,'Vspike':1.1}, 51 | 'HVRT2':{'HVRT_ENABLE':True,'tEnd':10.0,'tspike_start':2.0,'tspike_duration':4.0,'Vspike':1.1}, 52 | 'HVRT3':{'HVRT_ENABLE':True,'tEnd':10.0,'tspike_start':2.0,'tspike_duration':4.0,'Vspike':1.1}} 53 | 54 | 55 | 56 | def test_PVDER_default(self): 57 | """Test PV-DER without HVRT.""" 58 | 59 | self.setup_simulation(scenario='default') 60 | 61 | n_time_steps = self.run_simulation(scenario='default') 62 | 63 | self.loop_and_check(n_time_steps) 64 | 65 | def test_PVDER_HVRT1(self): 66 | """Test PV-DER with HVRT.""" 67 | 68 | self.setup_simulation(scenario='default') 69 | 70 | n_time_steps = self.run_simulation(scenario='HVRT1') 71 | 72 | self.loop_and_check(n_time_steps) 73 | 74 | def test_PVDER_HVRT2(self): 75 | """Test PV-DER with HVRT.""" 76 | 77 | self.setup_simulation(scenario='default') 78 | 79 | n_time_steps = self.run_simulation(scenario='HVRT2') 80 | 81 | self.loop_and_check(n_time_steps) 82 | 83 | def test_PVDER_HVRT3(self): 84 | """Test PV-DER with HVRT.""" 85 | 86 | self.setup_simulation(scenario='default') 87 | 88 | n_time_steps = self.run_simulation(scenario='HVRT3') 89 | 90 | self.loop_and_check(n_time_steps) 91 | 92 | def setup_simulation(self,scenario='default'): 93 | """Setup a simulation.""" 94 | 95 | self.events_list = [] 96 | self.grid_list = [] 97 | self.DER_model_list =[] 98 | self.sim_list = [] 99 | self.results_list = [] 100 | 101 | self.n_instances = self.return_settings(scenario=scenario,parameter='n_DER',settings_type='setup') 102 | 103 | flag_arguments = {'standAlone': True, 104 | 'steadyStateInitialization':self.return_settings(scenario=scenario,parameter='steadyStateInitialization',settings_type='setup'), 105 | 'verbosity':'DEBUG'} 106 | ratings_arguments ={'powerRating': self.return_settings(scenario=scenario,parameter='powerRating',settings_type='setup')} 107 | 108 | 109 | SinglePhase = self.return_settings(scenario=scenario,parameter='SinglePhase',settings_type='setup') 110 | 111 | for i in range(self.n_instances): 112 | 113 | self.events_list.append(SimulationEvents()) 114 | self.grid_list.append(Grid(events=self.events_list[-1])) 115 | 116 | kwargs={"gridModel":self.grid_list[-1],"identifier":scenario} 117 | kwargs.update(flag_arguments) 118 | kwargs.update(ratings_arguments) 119 | 120 | if SinglePhase: 121 | self.DER_model_list.append(SolarPVDERSinglePhase(events=self.events_list[-1],\ 122 | configFile=config_file,**kwargs)) 123 | else: 124 | self.DER_model_list.append(SolarPVDERThreePhase(events=self.events_list[-1],\ 125 | configFile=config_file,**kwargs)) 126 | 127 | self.sim_list.append(DynamicSimulation(gridModel=self.grid_list[-1],PV_model=self.DER_model_list[-1],events = self.events_list[-1],LOOP_MODE=False,COLLECT_SOLUTION=True)) 128 | self.sim_list[-1].jacFlag = False #Provide analytical Jacobian to ODE solver 129 | self.sim_list[-1].DEBUG_SOLVER = False #Check whether solver is failing to converge at any step 130 | 131 | self.results_list.append(SimulationResults(simulation = self.sim_list[-1],PER_UNIT=True)) 132 | 133 | def run_simulation(self,scenario): 134 | """Test PV-DER with HVRT.""" 135 | 136 | dt=1/120 137 | 138 | for i in range(self.n_instances): 139 | self.specify_scenario(self.events_list[i],self.DER_model_list[i],self.sim_list[i],scenario=scenario) 140 | n_time_steps = int(self.sim_list[i].tStop/dt) 141 | self.sim_list[i].run_simulation() 142 | 143 | return n_time_steps 144 | 145 | def specify_scenario(self,events,DER_model,sim,scenario='HVRT1'): 146 | """Specify scenario for unit test.""" 147 | 148 | Vnominal = self.return_settings(scenario=scenario,parameter='Vnominal',settings_type='test') 149 | Vspike = self.return_settings(scenario=scenario,parameter='Vspike',settings_type='test') 150 | 151 | tspike_start = self.return_settings(scenario=scenario,parameter='tspike_start',settings_type='test') 152 | tspike_duration = self.return_settings(scenario=scenario,parameter='tspike_duration',settings_type='test') 153 | 154 | DER_model.HVRT_ENABLE = self.return_settings(scenario=scenario,parameter='HVRT_ENABLE',settings_type='test') 155 | 156 | sim.tStop = self.return_settings(scenario=scenario,parameter='tEnd',settings_type='test') 157 | sim.name = scenario+'-'+sim.name 158 | if tspike_start is not None: 159 | events.add_grid_event(tspike_start,Vspike) 160 | 161 | if tspike_duration is not None: 162 | events.add_grid_event(tspike_start+tspike_duration,Vnominal) 163 | 164 | def return_settings(self,scenario,parameter,settings_type): 165 | """Check and repalce with correct parameter from scenario.""" 166 | 167 | if settings_type == 'test': 168 | if parameter in self.test_scenarios[scenario]: 169 | _variable = self.test_scenarios[scenario][parameter] 170 | else: 171 | if parameter in self.test_scenarios[self.default_test_scenario]: 172 | _variable = self.test_scenarios[self.default_test_scenario][parameter] 173 | else: 174 | _variable = None 175 | 176 | elif settings_type == 'setup': 177 | if parameter in self.setup_scenarios[scenario]: 178 | _variable = self.setup_scenarios[scenario][parameter] 179 | else: 180 | if parameter in self.setup_scenarios[self.default_setup_scenario]: 181 | _variable = self.setup_scenarios[self.default_setup_scenario][parameter] 182 | else: 183 | _variable = None 184 | 185 | return _variable 186 | 187 | def loop_and_check(self,n_time_steps): 188 | """Loop trhough DER instances and check.""" 189 | 190 | for i in range(self.n_instances): 191 | pvder_object = self.DER_model_list[i] 192 | sim_object = self.sim_list[i] 193 | results_object = self.results_list[i] 194 | 195 | for convergence_failure in sim_object.convergence_failure_list: 196 | print('Failure event:{}'.format(convergence_failure)) 197 | 198 | show_DER_status(pvder_object) 199 | self.check_DER_state(pvder_object) 200 | plot_DER_trajectories(results_object) 201 | self.check_HVRT_status(pvder_object) 202 | 203 | #self.assertTrue(len(sim_object.t_t) == len(sim_object.Vdc_t) == n_time_steps+1, msg='{}:The number of states collected should be {} but it is actually {}!'.format(sim_object.name,n_time_steps+1,len(sim_object.t_t))) 204 | 205 | def check_DER_state(self,pvder_object): 206 | """Check whether DER states are nominal.""" 207 | 208 | #Check if DC link voltage within inverter limits. 209 | self.assertTrue(pvder_object.Vdc*pvder_object.Vdcbase >= pvder_object.Vdcmpp_min or pvder_object.Vdc*pvder_object.Vdcbase <= pvder_object.Vdcmpp_max, msg='{}:DC link voltage {:.2f} V exceeded limit!'.format(pvder_object.name,pvder_object.Vdc*pvder_object.Vdcbase)) 210 | 211 | #Check current reference and power output within inverter limits. 212 | self.assertTrue(abs(pvder_object.ia_ref)<= pvder_object.iref_limit, msg='{}:Inverter current exceeded limit by {:.2f} A!'.format(pvder_object.name,(abs(pvder_object.ia_ref) - pvder_object.iref_limit)*pvder_object.Ibase)) 213 | 214 | self.assertTrue(abs(pvder_object.S_PCC)<=pvder_object.Sinverter_nominal, msg='{}:Inverter power output exceeded limit by {:.2f} VA!'.format(pvder_object.name,(abs(pvder_object.S_PCC) -pvder_object.Sinverter_nominal)*pvder_object.Sbase)) 215 | 216 | def check_HVRT_status(self,pvder_object): 217 | """Check whether ride through is working.""" 218 | 219 | 220 | if pvder_object.DER_TRIP: #Check if DER trip flag is True 221 | #Check if HVRT momentary cessation is True is connected 222 | self.assertTrue(pvder_object.HVRT_TRIP, msg='{}: HVRT trip should be true!'.format(pvder_object.name)) 223 | #Check if DER is connected 224 | self.assertFalse(pvder_object.DER_CONNECTED, msg='{}: DER connected despite trip!'.format(pvder_object.name)) 225 | #Check if DER stopped supplying power 226 | self.assertAlmostEqual(abs(pvder_object.S_PCC), 0.0, places=4, msg='{}:Inverter power output is {:.2f} VA despite trip status!'.format(pvder_object.name,pvder_object.S_PCC*pvder_object.Sbase)) 227 | #Check if DC link voltage limits are breached 228 | self.assertTrue(pvder_object.Vdc*pvder_object.Vdcbase >= pvder_object.Vdcmpp_min or pvder_object.Vdc*pvder_object.Vdcbase <= pvder_object.Vdcmpp_max, msg='{}:DC link voltage exceeded limits!'.format(pvder_object.name)) 229 | 230 | elif pvder_object.DER_MOMENTARY_CESSATION: 231 | #Check if HVRT momentary cessation is True is connected 232 | self.assertTrue(pvder_object.HVRT_MOMENTARY_CESSATION, msg='{}: HVRT momentary cessation should be true!'.format(pvder_object.name)) 233 | #Check if DER is connected 234 | self.assertFalse(pvder_object.DER_CONNECTED, msg='{}: DER connected despite momentary cessation!'.format(pvder_object.name)) 235 | 236 | parser = argparse.ArgumentParser(description='Unit tests for HVRT operation in OpenDSS - PVDER simulation.') 237 | 238 | test_options = TestPVDER.test_scenarios.keys() 239 | #test_options.sort() 240 | test_options = sorted(test_options) 241 | 242 | parser.add_argument('-i', '--item', action='store', dest='options', 243 | type=str, nargs='*', default=test_options,choices=test_options, 244 | help="Examples: -i HVRT1 HVRT2, -i HVRT3") 245 | test_options = parser.parse_args() 246 | 247 | if __name__ == '__main__': 248 | #unittest.main() 249 | 250 | runner = unittest.TextTestRunner() 251 | runner.run(suite()) 252 | -------------------------------------------------------------------------------- /tests/test_PVDER_LVRT.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import sys 3 | import os 4 | import math 5 | import argparse 6 | 7 | import matplotlib.pyplot as plt 8 | import unittest 9 | 10 | from pvder.DER_components_single_phase import SolarPVDERSinglePhase 11 | from pvder.DER_components_three_phase import SolarPVDERThreePhase 12 | from pvder.grid_components import Grid 13 | from pvder.dynamic_simulation import DynamicSimulation 14 | from pvder.simulation_events import SimulationEvents 15 | from pvder.simulation_utilities import SimulationResults 16 | from unittest_utilities import show_DER_status, plot_DER_trajectories 17 | 18 | #working_folder = os.path.dirname(os.path.dirname(os.path.dirname(os.getcwd()))) 19 | #module_folder = os.path.dirname(os.path.abspath(__file__)) 20 | 21 | #sys.path.append(module_folder) 22 | #os.environ['PATH']+=';'+module_folder 23 | config_file = r'..\config_der.json' 24 | 25 | def suite(): 26 | """Define a test suite.""" 27 | avoid_tests = ['LVRT2','LVRT3'] 28 | tests = test_options.options #['LVRT1'] #,,'LVRT2','LVRT3' 29 | tests = list(set(tests) - set(avoid_tests)) 30 | print('Following unittest scenarios will be run:{}'.format(tests)) 31 | suite = unittest.TestSuite() 32 | 33 | for test in tests: 34 | suite.addTest(TestPVDER('test_PVDER_'+ test)) 35 | 36 | return suite 37 | 38 | class TestPVDER(unittest.TestCase): 39 | 40 | Vnominal = 1.0 41 | default_setup_scenario = 'default' 42 | default_test_scenario = 'default' 43 | 44 | setup_scenarios ={'default':{'powerRating':50e3,'SinglePhase':False,'steadyStateInitialization':True,'n_DER':1}, 45 | 'case1':{'SinglePhase':True}, 46 | 'case2':{'SteadyState':False}} 47 | 48 | test_scenarios = {'default':{'LVRT_ENABLE':True,'tEnd':5.0,'Vnominal':1.0,'Vfault':0.8}, 49 | 'LVRT1':{'LVRT_ENABLE':True,'tEnd':10.0,'tfault_start':4.0,'tfault_duration':0.5,'Vfault':0.7}, 50 | 'LVRT2':{'LVRT_ENABLE':True,'tEnd':10.0,'tfault_start':4.0,'tfault_duration':2.0,'Vfault':0.7}, 51 | 'LVRT3':{'LVRT_ENABLE':True,'tEnd':10.0,'tfault_start':4.0,'tfault_duration':2.0,'Vfault':0.7}} 52 | 53 | 54 | def test_PVDER_default(self): 55 | """Test PV-DER without LVRT.""" 56 | 57 | self.setup_simulation(scenario='default') 58 | 59 | n_time_steps = self.run_simulation(scenario='default') 60 | 61 | self.loop_and_check(n_time_steps) 62 | 63 | def test_PVDER_LVRT1(self): 64 | """Test PV-DER with LVRT.""" 65 | 66 | self.setup_simulation(scenario='default') 67 | 68 | n_time_steps = self.run_simulation(scenario='LVRT1') 69 | 70 | self.loop_and_check(n_time_steps) 71 | 72 | def test_PVDER_LVRT2(self): 73 | """Test PV-DER with LVRT.""" 74 | 75 | self.setup_simulation(scenario='default') 76 | 77 | n_time_steps = self.run_simulation(scenario='LVRT2') 78 | 79 | self.loop_and_check(n_time_steps) 80 | 81 | def test_PVDER_LVRT3(self): 82 | """Test PV-DER with LVRT.""" 83 | 84 | self.setup_simulation(scenario='default') 85 | 86 | n_time_steps = self.run_simulation(scenario='LVRT3') 87 | 88 | self.loop_and_check(n_time_steps) 89 | 90 | def setup_simulation(self,scenario='default'): 91 | """Setup a simulation.""" 92 | 93 | self.events_list = [] 94 | self.grid_list = [] 95 | self.DER_model_list =[] 96 | self.sim_list = [] 97 | self.results_list = [] 98 | 99 | self.n_instances = self.return_settings(scenario=scenario,parameter='n_DER',settings_type='setup') 100 | 101 | 102 | flag_arguments = {'standAlone': True, 103 | 'steadyStateInitialization':self.return_settings(scenario=scenario,parameter='steadyStateInitialization',settings_type='setup'), 104 | 'verbosity':'DEBUG'} 105 | ratings_arguments ={'powerRating': self.return_settings(scenario=scenario,parameter='powerRating',settings_type='setup')} 106 | 107 | SinglePhase = self.return_settings(scenario=scenario,parameter='SinglePhase',settings_type='setup') 108 | 109 | for i in range(self.n_instances): 110 | 111 | self.events_list.append(SimulationEvents()) 112 | self.grid_list.append(Grid(events=self.events_list[-1])) 113 | 114 | kwargs={"gridModel":self.grid_list[-1],"identifier":scenario} 115 | kwargs.update(flag_arguments) 116 | kwargs.update(ratings_arguments) 117 | if SinglePhase: 118 | self.DER_model_list.append(SolarPVDERSinglePhase(events=self.events_list[-1],configFile=config_file,**kwargs)) 119 | else: 120 | self.DER_model_list.append(SolarPVDERThreePhase(events=self.events_list[-1],configFile=config_file,**kwargs)) 121 | 122 | self.sim_list.append(DynamicSimulation(gridModel=self.grid_list[-1],PV_model=self.DER_model_list[-1],events = self.events_list[-1],LOOP_MODE=False,COLLECT_SOLUTION=True)) 123 | self.sim_list[-1].jacFlag = False #Provide analytical Jacobian to ODE solver 124 | self.sim_list[-1].DEBUG_SOLVER = False #Check whether solver is failing to converge at any step 125 | 126 | self.results_list.append(SimulationResults(simulation = self.sim_list[-1],PER_UNIT=True)) 127 | 128 | def run_simulation(self,scenario): 129 | """Test PV-DER with LVRT.""" 130 | 131 | dt=1/120 132 | 133 | for i in range(self.n_instances): 134 | self.specify_scenario(self.events_list[i],self.DER_model_list[i],self.sim_list[i],scenario=scenario) 135 | n_time_steps = int(self.sim_list[i].tStop/dt) 136 | self.sim_list[i].run_simulation() 137 | 138 | return n_time_steps 139 | 140 | def specify_scenario(self,events,DER_model,sim,scenario='LVRT1'): 141 | """Specify scenario for unit test.""" 142 | 143 | Vnominal = self.return_settings(scenario=scenario,parameter='Vnominal',settings_type='test') 144 | Vfault = self.return_settings(scenario=scenario,parameter='Vfault',settings_type='test') 145 | 146 | tfault_start = self.return_settings(scenario=scenario,parameter='tfault_start',settings_type='test') 147 | tfault_duration = self.return_settings(scenario=scenario,parameter='tfault_duration',settings_type='test') 148 | 149 | DER_model.LVRT_ENABLE = self.return_settings(scenario=scenario,parameter='LVRT_ENABLE',settings_type='test') 150 | 151 | sim.tStop = self.return_settings(scenario=scenario,parameter='tEnd',settings_type='test') 152 | 153 | if tfault_start is not None: 154 | events.add_grid_event(tfault_start,Vfault) 155 | 156 | if tfault_duration is not None: 157 | events.add_grid_event(tfault_start+tfault_duration,Vnominal) 158 | 159 | def return_settings(self,scenario,parameter,settings_type): 160 | """Check and repalce with correct parameter from scenario.""" 161 | 162 | if settings_type == 'test': 163 | if parameter in self.test_scenarios[scenario]: 164 | _variable = self.test_scenarios[scenario][parameter] 165 | else: 166 | if parameter in self.test_scenarios[self.default_test_scenario]: 167 | _variable = self.test_scenarios[self.default_test_scenario][parameter] 168 | else: 169 | _variable = None 170 | 171 | elif settings_type == 'setup': 172 | if parameter in self.setup_scenarios[scenario]: 173 | _variable = self.setup_scenarios[scenario][parameter] 174 | else: 175 | if parameter in self.setup_scenarios[self.default_setup_scenario]: 176 | _variable = self.setup_scenarios[self.default_setup_scenario][parameter] 177 | else: 178 | _variable = None 179 | 180 | return _variable 181 | 182 | def loop_and_check(self,n_time_steps): 183 | """Loop trhough DER instances and check.""" 184 | 185 | for i in range(self.n_instances): 186 | pvder_object = self.DER_model_list[i] 187 | sim_object = self.sim_list[i] 188 | results_object = self.results_list[i] 189 | 190 | for convergence_failure in sim_object.convergence_failure_list: 191 | print('Failure event:{}'.format(convergence_failure)) 192 | 193 | show_DER_status(pvder_object) 194 | self.check_DER_state(pvder_object) 195 | plot_DER_trajectories(results_object) 196 | self.check_LVRT_status(pvder_object) 197 | 198 | #self.assertTrue(len(sim_object.t_t) == len(sim_object.Vdc_t) == n_time_steps+1, msg='{}:The number of states collected should be {} but it is actually {}!'.format(sim_object.name,n_time_steps+1,len(sim_object.t_t))) 199 | 200 | def check_DER_state(self,pvder_object): 201 | """Check whether DER states are nominal.""" 202 | 203 | #Check if DC link voltage within inverter limits. 204 | self.assertTrue(pvder_object.Vdc*pvder_object.Vdcbase >= pvder_object.Vdcmpp_min or pvder_object.Vdc*pvder_object.Vdcbase <= pvder_object.Vdcmpp_max, msg='{}:DC link voltage {:.2f} V exceeded limit!'.format(pvder_object.name,pvder_object.Vdc*pvder_object.Vdcbase)) 205 | 206 | #Check current reference and power output within inverter limits. 207 | self.assertTrue(abs(pvder_object.ia_ref)<= pvder_object.iref_limit, msg='{}:Inverter current exceeded limit by {:.2f} A!'.format(pvder_object.name,(abs(pvder_object.ia_ref) - pvder_object.iref_limit)*pvder_object.Ibase)) 208 | 209 | self.assertTrue(abs(pvder_object.S_PCC)<=pvder_object.Sinverter_nominal, msg='{}:Inverter power output exceeded limit by {:.2f} VA!'.format(pvder_object.name,(abs(pvder_object.S_PCC) -pvder_object.Sinverter_nominal)*pvder_object.Sbase)) 210 | 211 | def check_LVRT_status(self,pvder_object): 212 | """Check whether ride through is working.""" 213 | 214 | if pvder_object.DER_TRIP: #Check if DER trip flag is True 215 | #Check if LVRT momentary cessation is True is connected 216 | self.assertTrue(pvder_object.LVRT_TRIP, msg='{}: LVRT trip should be true!'.format(pvder_object.name)) 217 | #Check if DER is connected 218 | self.assertFalse(pvder_object.DER_CONNECTED, msg='{}: DER connected despite trip!'.format(pvder_object.name)) 219 | #Check if DER stopped supplying power 220 | self.assertAlmostEqual(abs(pvder_object.S_PCC), 0.0, places=4, msg='{}:Inverter power output is {:.2f} VA despite trip status!'.format(pvder_object.name,pvder_object.S_PCC*pvder_object.Sbase)) 221 | #Check if DC link voltage limits are breached 222 | self.assertTrue(pvder_object.Vdc*pvder_object.Vdcbase >= pvder_object.Vdcmpp_min or pvder_object.Vdc*pvder_object.Vdcbase <= pvder_object.Vdcmpp_max, msg='{}:DC link voltage exceeded limits!'.format(pvder_object.name)) 223 | 224 | elif pvder_object.DER_MOMENTARY_CESSATION: 225 | #Check if LVRT momentary cessation is True is connected 226 | self.assertTrue(pvder_object.LVRT_MOMENTARY_CESSATION, msg='{}: LVRT momentary cessation should be true!'.format(pvder_object.name)) 227 | #Check if DER is connected 228 | self.assertFalse(pvder_object.DER_CONNECTED, msg='{}: DER connected despite momentary cessation!'.format(pvder_object.name)) 229 | 230 | parser = argparse.ArgumentParser(description='Unit tests for LVRT operation in OpenDSS - PVDER simulation.') 231 | 232 | test_options = TestPVDER.test_scenarios.keys() 233 | #test_options.sort() 234 | test_options = sorted(test_options) 235 | 236 | parser.add_argument('-i', '--item', action='store', dest='options', 237 | type=str, nargs='*', default=test_options,choices=test_options, 238 | help="Examples: -i LVRT1 LVRT2, -i LVRT3") 239 | test_options = parser.parse_args() 240 | 241 | if __name__ == '__main__': 242 | runner = unittest.TextTestRunner() 243 | runner.run(suite()) 244 | -------------------------------------------------------------------------------- /tests/test_PVDER_SinglePhase.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import sys 3 | import os 4 | import argparse 5 | import unittest 6 | 7 | import math 8 | 9 | import matplotlib.pyplot as plt 10 | 11 | from pvder.DER_components_single_phase import SolarPVDERSinglePhase 12 | from pvder.grid_components import Grid 13 | from pvder.dynamic_simulation import DynamicSimulation 14 | from pvder.simulation_events import SimulationEvents 15 | from pvder.simulation_utilities import SimulationResults 16 | from unittest_utilities import show_DER_status, plot_DER_trajectories 17 | config_file = r'..\config_der.json' 18 | 19 | 20 | def suite(): 21 | """Define a test suite.""" 22 | all_tests = ['test_init','test_parameter_dict','test_jacobian'] 23 | 24 | avoid_tests = ['test_jacobian'] 25 | 26 | tests = list(set(all_tests) - set(avoid_tests)) 27 | print('Following unittest scenarios will be run:{}'.format(tests)) 28 | suite = unittest.TestSuite() 29 | 30 | for test in tests: 31 | suite.addTest(TestPVDER(test)) 32 | 33 | return suite 34 | 35 | class TestPVDER(unittest.TestCase): 36 | 37 | Vnominal = 1.0 38 | 39 | scaler = 0.835 40 | Va = (.50+0j)*Grid.Vbase*scaler 41 | Vb = (-.25-.43301270j)*Grid.Vbase*scaler 42 | Vc = (-.25+.43301270j)*Grid.Vbase*scaler 43 | 44 | power_rating = 10.0e3 45 | Vrms = abs(Va)/math.sqrt(2) 46 | 47 | wgrid = 2*math.pi*60.0 48 | 49 | flag_arguments = {'standAlone': False, 50 | 'steadyStateInitialization':True, 51 | 'verbosity':'DEBUG'} 52 | ratings_arguments ={'powerRating':power_rating, 53 | 'VrmsRating':Vrms} 54 | voltage_arguments = {'gridVoltagePhaseA':Va, 55 | 'gridFrequency':wgrid} 56 | 57 | def test_init(self): 58 | """Test PV-DER three phase mode.""" 59 | 60 | events = SimulationEvents() 61 | kwargs={} 62 | kwargs.update(self.flag_arguments) 63 | kwargs.update(self.ratings_arguments) 64 | kwargs.update(self.voltage_arguments) 65 | PVDER = SolarPVDERSinglePhase(events = events,configFile=config_file,**kwargs) 66 | 67 | self.assertIsInstance(PVDER, SolarPVDERSinglePhase) 68 | self.assertTrue(PVDER.steady_state_initialization) 69 | 70 | def test_parameter_dict(self): 71 | """Test initalization and update of paraemter dictionary.""" 72 | 73 | source_ID = '10' 74 | new_ID ='test_DER' 75 | new_module_parameters = {'Np':5} 76 | new_circuit_parameters = {'C_actual':100.0e-6} 77 | 78 | events = SimulationEvents() 79 | kwargs={} 80 | kwargs.update(self.flag_arguments) 81 | kwargs.update(self.ratings_arguments) 82 | kwargs.update(self.voltage_arguments) 83 | PVDER = SolarPVDERSinglePhase(events = events,configFile=config_file,**kwargs) 84 | 85 | PVDER.initialize_parameter_dict(parameter_ID = new_ID,source_parameter_ID = source_ID) 86 | 87 | self.assertEqual(PVDER.module_parameters[source_ID]['Np'],PVDER.module_parameters[new_ID]['Np']) 88 | self.assertEqual(PVDER.inverter_ratings[source_ID]['Srated'],PVDER.inverter_ratings[new_ID]['Srated']) 89 | self.assertEqual(PVDER.circuit_parameters[source_ID]['Rf_actual'],PVDER.circuit_parameters[new_ID]['Rf_actual']) 90 | 91 | PVDER.update_parameter_dict(parameter_ID = new_ID,parameter_type = 'module_parameters',parameter_dict = new_module_parameters) 92 | PVDER.update_parameter_dict(parameter_ID = new_ID,parameter_type = 'circuit_parameters',parameter_dict = new_circuit_parameters) 93 | 94 | self.assertEqual(PVDER.module_parameters[new_ID]['Np'],new_module_parameters['Np']) 95 | self.assertEqual(PVDER.circuit_parameters[new_ID]['C_actual'],new_circuit_parameters['C_actual']) 96 | 97 | def test_jacobian(self): 98 | """Test PV-DER Jacobian.""" 99 | 100 | events = SimulationEvents() 101 | kwargs={} 102 | kwargs.update(self.flag_arguments) 103 | kwargs.update(self.ratings_arguments) 104 | kwargs.update(self.voltage_arguments) 105 | PVDER = SolarPVDERSinglePhase(events = events,configFile=config_file,**kwargs) 106 | jac_CHECK,Jn,Ja = PVDER.check_jacobian() 107 | self.assertTrue(jac_CHECK,'Analytical and numerical Jacobian should be same.') 108 | self.assertEqual(Jn.shape,(PVDER.n_ODE,PVDER.n_ODE)) 109 | 110 | if __name__ == '__main__': 111 | runner = unittest.TextTestRunner() 112 | runner.run(suite()) 113 | -------------------------------------------------------------------------------- /tests/test_PVDER_ThreePhase.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import sys 3 | import os 4 | import argparse 5 | import logging 6 | import unittest 7 | 8 | import math 9 | import cmath 10 | 11 | import matplotlib.pyplot as plt 12 | 13 | from pvder.DER_components_three_phase import SolarPVDERThreePhase 14 | from pvder.grid_components import Grid 15 | from pvder.dynamic_simulation import DynamicSimulation 16 | from pvder.simulation_events import SimulationEvents 17 | from pvder.simulation_utilities import SimulationResults 18 | from pvder import utility_functions 19 | from unittest_utilities import show_DER_status, plot_DER_trajectories 20 | 21 | config_file = r'..\config_der.json' 22 | 23 | 24 | def suite(): 25 | """Define a test suite.""" 26 | all_tests = ['test_init','test_parameter_dict','test_jacobian','test_steady_state_calc'] 27 | avoid_tests = [] 28 | tests = list(set(all_tests) - set(avoid_tests)) 29 | print('Following unittest scenarios will be run:{}'.format(tests)) 30 | suite = unittest.TestSuite() 31 | for test in tests: 32 | suite.addTest(TestPVDER(test)) 33 | return suite 34 | 35 | class TestPVDER(unittest.TestCase): 36 | 37 | Vnominal = 1.0 38 | 39 | scaler = 0.835 40 | Va = (.50+0j)*Grid.Vbase*scaler 41 | Vb = (-.25-.43301270j)*Grid.Vbase*scaler 42 | Vc = (-.25+.43301270j)*Grid.Vbase*scaler 43 | 44 | power_rating = 50.0e3 45 | Vrms = abs(Va)/math.sqrt(2) 46 | 47 | wgrid = 2*math.pi*60.0 48 | 49 | flag_arguments = {'standAlone': False, 50 | 'steadyStateInitialization':True, 51 | 'verbosity':'DEBUG'} 52 | ratings_arguments ={'powerRating':power_rating, 53 | 'VrmsRating':Vrms} 54 | voltage_arguments = {'gridVoltagePhaseA': Va, 55 | 'gridVoltagePhaseB':Vb, 56 | 'gridVoltagePhaseC':Vc, 57 | 'gridFrequency':wgrid} 58 | kwargs={} 59 | kwargs.update(flag_arguments) 60 | kwargs.update(ratings_arguments) 61 | kwargs.update(voltage_arguments) 62 | 63 | def test_init(self): 64 | """Test PV-DER three phase mode.""" 65 | 66 | events = SimulationEvents() 67 | 68 | PVDER = SolarPVDERThreePhase(events = events,configFile=config_file,**self.kwargs) 69 | 70 | self.assertIsInstance(PVDER, SolarPVDERThreePhase) 71 | self.assertTrue(PVDER.steady_state_initialization) 72 | 73 | def test_parameter_dict(self): 74 | """Test initalization and update of paraemter dictionary.""" 75 | 76 | source_ID = '50' 77 | new_ID ='test_DER' 78 | new_module_parameters = {'Np':5} 79 | new_circuit_parameters = {'C_actual':500.0e-6} 80 | 81 | events = SimulationEvents() 82 | 83 | PVDER = SolarPVDERThreePhase(events = events,configFile=config_file,**self.kwargs) 84 | 85 | PVDER.initialize_parameter_dict(parameter_ID = new_ID,source_parameter_ID = source_ID) 86 | 87 | self.assertEqual(PVDER.module_parameters[source_ID]['Np'],PVDER.module_parameters[new_ID]['Np']) 88 | self.assertEqual(PVDER.inverter_ratings[source_ID]['Srated'],PVDER.inverter_ratings[new_ID]['Srated']) 89 | self.assertEqual(PVDER.circuit_parameters[source_ID]['Rf_actual'],PVDER.circuit_parameters[new_ID]['Rf_actual']) 90 | 91 | PVDER.update_parameter_dict(parameter_ID = new_ID,parameter_type = 'module_parameters',parameter_dict = new_module_parameters) 92 | PVDER.update_parameter_dict(parameter_ID = new_ID,parameter_type = 'circuit_parameters',parameter_dict = new_circuit_parameters) 93 | 94 | self.assertEqual(PVDER.module_parameters[new_ID]['Np'],new_module_parameters['Np']) 95 | self.assertEqual(PVDER.circuit_parameters[new_ID]['C_actual'],new_circuit_parameters['C_actual']) 96 | 97 | def test_jacobian(self): 98 | """Test PV-DER three phase mode.""" 99 | 100 | events = SimulationEvents() 101 | 102 | PVDER = SolarPVDERThreePhase(events = events,configFile=config_file,**self.kwargs) 103 | 104 | jac_CHECK,Jn,Ja = PVDER.check_jacobian() 105 | self.assertTrue(jac_CHECK,'Analytical and numerical Jacobian should be same.') 106 | self.assertEqual(Jn.shape,(PVDER.n_ODE,PVDER.n_ODE)) 107 | 108 | def test_steady_state_calc(self): 109 | """Test PV-DER three phase mode.""" 110 | 111 | events = SimulationEvents() 112 | 113 | voltage_list = [(self.Va,self.Vb,self.Vc), 114 | (cmath.rect(206.852, math.radians(-36.9906)),cmath.rect(206.128, math.radians(-157.745)),cmath.rect(208.387, math.radians(82.7291))),(169.18+118.52j,utility_functions.Ub_calc(169.18+118.52j),utility_functions.Uc_calc(169.18+118.52j))] 115 | 116 | for voltages in voltage_list: 117 | Va = voltages[0] 118 | Vb = voltages[1] 119 | Vc = voltages[2] 120 | 121 | print('Testing voltages:{}'.format(voltages)) 122 | 123 | PVDER = SolarPVDERThreePhase(events = events,configFile=config_file,**self.kwargs) 124 | 125 | self.assertAlmostEqual(PVDER.Ppv,PVDER.S.real,delta=0.001,msg='Inverter power output must be equal to PV module power output at steady-state!') 126 | self.assertAlmostEqual(PVDER.S_PCC.imag,PVDER.Q_ref,delta=0.001,msg='Inverter reactive power output must be equal to Q reference!') 127 | 128 | self.assertAlmostEqual(PVDER.Vdc,PVDER.Vdc_ref,delta=0.001,msg='DC link voltage should be equal to reference!') 129 | 130 | self.assertAlmostEqual(PVDER.ma+PVDER.mb+PVDER.mc,0.0+1j*0.0,delta=0.001,msg='Duty cycles should sum to zero!') 131 | self.assertLess(abs(PVDER.ma),1.0,msg='Magnitude of duty cycle should be less than 1!') 132 | 133 | if __name__ == '__main__': 134 | runner = unittest.TextTestRunner() 135 | runner.run(suite()) 136 | -------------------------------------------------------------------------------- /tests/unittest_utilities.py: -------------------------------------------------------------------------------- 1 | """Common utility functions used in unit tests.""" 2 | 3 | 4 | 5 | def show_DER_status(pvder_object): 6 | """Show DER states.""" 7 | 8 | pvder_object.show_PV_DER_states(quantity='power') 9 | pvder_object.show_PV_DER_states(quantity='current') 10 | pvder_object.show_PV_DER_states(quantity='voltage') 11 | pvder_object.show_PV_DER_states(quantity='duty cycle') 12 | pvder_object.show_RT_settings(settings_type='LVRT') 13 | pvder_object.show_RT_settings(settings_type='HVRT') 14 | 15 | def plot_DER_trajectories(results_object): 16 | """PLot DER trajectory.""" 17 | 18 | results_object.PER_UNIT = False 19 | results_object.PLOT_TITLE = True 20 | results_object.font_size = 18 21 | 22 | results_object.plot_DER_simulation(plot_type='active_power_Ppv_Pac_PCC')# 23 | results_object.plot_DER_simulation(plot_type='reactive_power') 24 | results_object.plot_DER_simulation(plot_type='current') 25 | results_object.plot_DER_simulation(plot_type='voltage_Vdc') 26 | results_object.plot_DER_simulation(plot_type='voltage_LV') 27 | results_object.plot_DER_simulation(plot_type='voltage_Vpcclv_all_phases') 28 | results_object.plot_DER_simulation(plot_type='duty_cycle') --------------------------------------------------------------------------------