Skip to content

Commit

Permalink
Init yagat application (#1)
Browse files Browse the repository at this point in the history
Signed-off-by: Damien Jeandemange <[email protected]>
  • Loading branch information
jeandemanged authored Nov 10, 2024
1 parent 23a989a commit b0bd5e3
Show file tree
Hide file tree
Showing 49 changed files with 2,786 additions and 1 deletion.
69 changes: 69 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: CI

on:
pull_request:
workflow_dispatch:
inputs:
upload_artifacts:
description: 'Upload build artifacts'
required: true
default: false
type: boolean

permissions: { }

jobs:
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

env:
DISPLAY: :99

steps:
- name: Checkout sources
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Xvfb (Linux only)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update -y
sudo apt-get install -y xvfb
Xvfb -ac $DISPLAY -screen 0 1280x1024x24 > /dev/null 2>&1 &
- name: Test with pytest
run: |
coverage run --branch -m pytest tests
coverage xml
coverage report
- name: SonarCloud Scan (Linux only)
if: matrix.os == 'ubuntu-latest'
uses: SonarSource/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

- name: Build application with PyInstaller
run: pyinstaller -y yagat.spec

- name: Upload application Artifact
if: ${{ github.event_name == 'workflow_dispatch' && inputs.upload_artifacts }}
uses: actions/upload-artifact@v4
with:
name: yagat-${{ matrix.os }}
path: dist/yagat*
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.iml
*.zip
.idea
venv
/build/
/dist/
__pycache__/
/.coverage
/coverage.xml
/junit/
96 changes: 95 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,97 @@
# YAGAT

Yet Another Grid Analysis Tool
**Y**et **A**nother **G**rid **A**nalysis **T**ool

## Overview

YAGAT provides a graphical user interface built on top of the [PowSyBl](https://www.powsybl.org) open source grid analysis libraries.

With YAGAT no computer science skill is required: just download the application and run it.

Today with YAGAT you can:
- Load grid models from the various formats supported by [PowSyBl](https://www.powsybl.org):
- CIM/CGMES
- UCTE-DEF
- IEEE-CDF
- MATPOWER
- Siemens PSS®E
- Display and navigate the grid model with electrical buses represented in tabular form
- Run a Load Flow, visualize solved bus voltages and branch flows

## Installation

### Binary releases

Binary releases are provided for Windows, Linux and macOS on the [releases page](https://github.com/jeandemanged/yagat/releases).
No additional software is required for installation.
Download and extract the zip archive for your platform, then run YAGAT.

### Building from source

With Python 3.12 and e.g. using a Virtual Environment and `pip`.

```bash
# clone the git repository
git clone https://github.com/jeandemanged/yagat.git
cd yagat
```

```bash
# install requirements
python -m venv venv
. venv/bin/activate
pip install -r requirements.txt
```

```bash
# build the application
pyinstaller -y yagat.spec
```

YAGAT is then available for your platform in the `dist` directory.

## Quick Start

- **Open a sample network**: Go to `File` | `Open Sample network` | `IEEE 9 Bus` to load a sample grid model.
- **Navigate the grid**: Use the tree view on the left to browse through the network model and its elements.
- **Run the Load Flow**: Select `Run` | `Load Flow` to execute the analysis.
- Once completed, view the solved bus voltages and branch flows for insights into the grid's state.

## Roadmap

YAGAT today lacks many features, but you may already find it useful. What is planned for the future is:

### Short-Term to Mid-Term Plans:
- **General**: A log view
- **Grid Model Navigation**: Tabular views per equipment type
- **Grid Model Updates**: Adjust grid configurations, such as opening/closing switches, changing generator set points, etc.
- **Load Flow**:
- Add ability to save/load the Load Flow parameters
- View Load Flow reports in order to troubleshoot e.g. non-convergence

### Future Plans:
- **Security Analysis**:
- Configure a list of contingencies to simulate
- Run contingency analysis
- View contingency violations

## Under the Hood

YAGAT is:
- **Written in Python**: a high-level, general-purpose programming language.
- **Using [PyPowSyBl](https://pypowsybl.readthedocs.io/en/latest/index.html)**: Provides the core grid analysis functionalities.
- **Using [Tkinter](https://wiki.python.org/moin/TkInter)**: Supplies the graphical user interface.
- **Using [Tksheet](https://github.com/ragardner/tksheet)**: An amazing tkinter table widget.
- **Using [PyInstaller](https://pyinstaller.org/en/stable/)**: Packages the application.

## Data Confidentiality

We take data confidentiality seriously.
All data processed by the application remains on the user's local machine and is not transmitted to any external servers.
This ensures complete data privacy for users working with sensitive grid models.

## Contributing and Support

Should you encounter any issues with YAGAT, please let us know.
We welcome contributions, ideas, and feedback. Please open an [issue](https://github.com/jeandemanged/yagat/issues)
or [pull request](https://github.com/jeandemanged/yagat/pulls) to get involved.
Binary file added requirements.in
Binary file not shown.
62 changes: 62 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile requirements.in
#
altgraph==0.17.4
# via pyinstaller
colorama==0.4.6
# via pytest
coverage[toml]==7.6.4
# via pytest-cov
iniconfig==2.0.0
# via pytest
networkx==3.4.2
# via pypowsybl
numpy==2.1.3
# via pandas
packaging==24.2
# via
# pyinstaller
# pyinstaller-hooks-contrib
# pytest
pandas==2.2.3
# via pypowsybl
pefile==2023.2.7
# via pyinstaller
pillow==11.0.0
# via -r requirements.in
pluggy==1.5.0
# via pytest
prettytable==3.12.0
# via pypowsybl
pyinstaller==6.11.0
# via -r requirements.in
pyinstaller-hooks-contrib==2024.9
# via pyinstaller
pypowsybl==1.8.1
# via -r requirements.in
pytest==8.3.3
# via
# -r requirements.in
# pytest-cov
pytest-cov==6.0.0
# via -r requirements.in
python-dateutil==2.9.0.post0
# via pandas
pytz==2024.2
# via pandas
pywin32-ctypes==0.2.3
# via pyinstaller
six==1.16.0
# via python-dateutil
tksheet==7.2.21
# via -r requirements.in
tzdata==2024.2
# via pandas
wcwidth==0.2.13
# via prettytable

# The following packages are considered to be unsafe in a requirements file:
# setuptools
6 changes: 6 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sonar.projectKey=jeandemanged_yagat
sonar.organization=jeandemanged

sonar.sources=yagat
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
7 changes: 7 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#
# Copyright (c) 2024, Damien Jeandemange (https://github.com/jeandemanged)
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
#
99 changes: 99 additions & 0 deletions tests/test_app_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#
# Copyright (c) 2024, Damien Jeandemange (https://github.com/jeandemanged)
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
#
import tkinter as tk

import pypowsybl.network as pn
import pytest

from yagat.app_context import AppContext


class TestListener:

__test__ = False

def __init__(self, context: AppContext):
self._status_text_from_listener = None
self._network_from_listener = None
self._selection_from_listener = None
self._selected_tab_from_listener = None
context.add_status_text_listener(lambda value: self.status_text_listener(value))
context.add_network_changed_listener(lambda value: self.network_listener(value))
context.add_selection_changed_listener(lambda value: self.selection_listener(value))
context.add_tab_changed_listener(lambda value: self.selected_tab_listener(value))

def status_text_listener(self, value):
self._status_text_from_listener = value

def network_listener(self, value):
self._network_from_listener = value

def selection_listener(self, value):
self._selection_from_listener = value

def selected_tab_listener(self, value):
self._selected_tab_from_listener = value

@property
def status_text_from_listener(self):
return self._status_text_from_listener

@property
def network_from_listener(self) -> pn.Network:
return self._network_from_listener

@property
def selection_from_listener(self):
return self._selection_from_listener

@property
def selected_tab_from_listener(self):
return self._selected_tab_from_listener


class TestAppContext:

@pytest.fixture
def context(self):
context = AppContext(tk.Tk())
yield context

def test_initial_state(self, context):
assert context.tk_root is not None
assert context.network is None
assert context.network_structure is None
assert context.selection[0] is None
assert context.selection[1] is None
assert context.selection[2] is None
assert context.status_text == 'Welcome'

def test_status_text(self, context):
test = TestListener(context)
context.status_text = 'test status text'
assert test.status_text_from_listener == 'test status text'

def test_network(self, context):
test = TestListener(context)
context.network = pn.create_ieee9()
assert test.network_from_listener.name == 'ieee9cdf'
context.network = None
assert test.network_from_listener is None

def test_selection(self, context):
test = TestListener(context)
context.selection = 'test selection'
assert test.selection_from_listener == 'test selection'
context.selection = None
assert test.selection_from_listener is None

def test_selected_tab(self, context):
test = TestListener(context)
context.selected_tab = 'test selected tab'
assert test.selected_tab_from_listener == 'test selected tab'
context.selected_tab = None
assert test.selected_tab_from_listener is None
Loading

0 comments on commit b0bd5e3

Please sign in to comment.