Skip to content

Commit

Permalink
Merge branch 'main' into test_transform
Browse files Browse the repository at this point in the history
  • Loading branch information
HighestAuto authored Aug 19, 2024
2 parents 3a49aec + d84f2b4 commit 5427110
Show file tree
Hide file tree
Showing 23 changed files with 497 additions and 73 deletions.
Binary file modified .DS_Store
Binary file not shown.
Binary file added .github/.DS_Store
Binary file not shown.
Binary file added .github/workflows/.DS_Store
Binary file not shown.
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on: [push, pull_request]

jobs:
run-unittests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4

- name: Create and activate virtual environment
run: |
python -m venv venv # Create a virtual environment
source venv/bin/activate # Activate the virtual environment
- name: Install dependencies
run: |
source venv/bin/activate # Ensure the virtual environment is active
pip install --upgrade pip # Upgrade pip within the virtual environment
pip install -r requirements.txt # Install dependencies from requirements.txt
- name: Set up environment
run: |
source venv/bin/activate # Ensure the virtual environment is active
source ./add_root_to_path.sh # Run the script to modify PYTHONPATH
- name: Run tests
run: |
source venv/bin/activate # Ensure the virtual environment is active
pytest # Run tests directly with pytest
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
*.tfvars

# Logs and Logging-Related
logs/*
data/*
tmp/logs/*
tmp/data/*
*.parquet
*.feather
*.log


# Python Module Data
#(e.g __pychache__ directories)
__pycache__/
Expand Down
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 J J Marden

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@
! We'll add some visuals here maybe, using [Asciinema](https://asciinema.org/) or using [ttygif](https://github.com/icholy/ttygif) and a recording. !

## Badges
! Are we doing badges? !

[![CI](https://github.com/JoshuaMarden/energy_monitor/actions/workflows/ci.yml/badge.svg)](https://github.com/JoshuaMarden/energy_monitor/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/JoshuaMarden/energy_monitor/branch/main/graph/badge.svg)](https://codecov.io/gh/JoshuaMarden/energy_monitor)
![Python Versions](https://img.shields.io/pypi/pyversions/energy_monitor.svg)
![License](https://img.shields.io/github/license/JoshuaMarden/energy_monitor)
![GitHub Stars](https://img.shields.io/github/stars/JoshuaMarden/energy_monitor.svg?style=social&label=Star)
![GitHub Last Commit](https://img.shields.io/github/last-commit/JoshuaMarden/energy_monitor)
![GitHub Contributors](https://img.shields.io/github/contributors/JoshuaMarden/energy_monitor)
[![Maintainability](https://api.codeclimate.com/v1/badges/2b5506b2c460c017238e/maintainability)](https://codeclimate.com/github/JoshuaMarden/energy_monitor/maintainability)
[![Requirements Status](https://requires.io/github/JoshuaMarden/energy_monitor/requirements.svg?branch=main)](https://requires.io/github/JoshuaMarden/energy_monitor/requirements/?branch=main)
[![Test Coverage](https://api.codeclimate.com/v1/badges/2b5506b2c460c017238e/test_coverage)](https://codeclimate.com/github/JoshuaMarden/energy_monitor/test_coverage)



## Installation
[Clone the repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) from github.
Expand Down Expand Up @@ -47,11 +59,17 @@ If you are having trouble using our app please open a ticket we'll get back to y

If people have pushed to this repo, they are authors!

## License

## License

[Elexon License](https://www.elexon.co.uk/data/balancing-mechanism-reporting-agent/copyright-licence-bmrs-data/) requires the following statement:

Contains BMRS data © Exelon Limited copyright and database right 2024.


MIT License

Copyright (c) [year] [fullname]
Copyright (c) 2024 J J Marden

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
18 changes: 18 additions & 0 deletions dashboard/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM python:latest



# Copy the current directory contents into the container at /app
COPY dashboard_script.py .
COPY requirements.txt .

# Install any needed packages specified in requirements.txt
RUN pip3 install -r requirements.txt


EXPOSE 8501

EXPOSE 5432

# Run the application --platform "linux/amd64"
CMD ["streamlit", "run", "dashboard_script.py", "--server.port=8501"]
179 changes: 179 additions & 0 deletions dashboard/dashboard_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import pandas as pd
import os
import streamlit as st
import psycopg2
import altair as alt
from dotenv import load_dotenv

load_dotenv('.env')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = os.getenv('DB_PORT')
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB_NAME = os.getenv('DB_NAME')


class DataLoader:
@staticmethod
def connect_to_db(db_host: str = DB_HOST, db_port: str = DB_PORT,
db_user: str = DB_USER, db_password: str = DB_PASSWORD,
db_name: str = DB_NAME):
conn = psycopg2.connect(
host=db_host,
database=db_name,
user=db_user,
password=db_password,
port=db_port
)
return conn

@staticmethod
def fetch_data_from_tables(conn):

demand_query = "SELECT * FROM Demand"
gen_query = "SELECT * FROM Generation"
cost_query = "SELECT * FROM Cost"
carbon_query = "SELECT * FROM Carbon"
demand_df = pd.read_sql_query(demand_query, conn)
cost_df = pd.read_sql_query(cost_query, conn)
gen_df = pd.read_sql_query(gen_query, conn)
carbon_df = pd.read_sql_query(carbon_query, conn)

if conn:
conn.close()
return gen_df, demand_df, cost_df, carbon_df
raise ConnectionError("Failed to connect to Database")


class dashboard:
def __init__(self) -> None:
pass

def carbon_plots(self, carbon_df: pd.DataFrame) -> alt.Chart:
return alt.Chart(carbon_df).mark_bar().encode(alt.X('publish_time:T', title='Time'),
y=alt.Y('forecast:Q', title='Carbon Intensity'), color='carbon_level').properties(width=650, height=400)

def time_of_lowest_carbon(self, carbon_df: pd.DataFrame):
return carbon_df.loc[carbon_df["forecast"] == min(
carbon_df["forecast"].values), "publish_time"].dt.time.iloc[0]

def time_of_highest_carbon(self, carbon_df: pd.DataFrame):
return carbon_df.loc[carbon_df["forecast"] == max(
carbon_df["forecast"].values), "publish_time"].dt.time.iloc[0]

def generation_fuel_type(self, gen_df: pd.DataFrame) -> alt.Chart:
fuel_types = ["BIOMASS", "CCGT", "COAL", "NPSHYD",
"NUCLEAR", "OCGT", "OIL", "OTHER", "PS", "WIND"]
df_fuel_types = gen_df[gen_df['fuel_type'].isin(
fuel_types)]

return alt.Chart(df_fuel_types[["publish_time", "generated", "fuel_type"]]).mark_line().encode(
x=alt.X('publish_time:T', title='Time'), y=alt.Y('generated:Q', title='Energy Generated MW'),
color="fuel_type").properties(width=1000, height=1000)

def intensity_factors_df(self) -> alt.Chart:
intensity_factors = {'Energy Source': ['Biomass', 'Coal', 'Dutch Imports', 'French Imports', 'Gas (Combined Cycle)', 'Gas (Open Cycle)',
'Hydro', 'Irish Imports', 'Nuclear', 'Oil', 'Other', 'Pumped Storage', 'Solar', 'Wind'],
'Intensity (gCO₂/kWh)': [120, 937, 474, 53, 394, 651, 0, 458, 0, 935, 300, 0, 0, 0],
'Explanation': [
'Uses organic materials which result in moderate emissions.',
'High carbon emissions due to combustion of fossil fuels.',
'Mixed source imports with moderate carbon impact.',
'Imported nuclear and renewable energy with low emissions.',
'More efficient gas-burning technology, lower emissions.',
'Less efficient gas technology, higher emissions.',
'Zero emissions as it relies on water flow.',
'Mixed source imports with moderate carbon impact.',
'Zero emissions from nuclear reactions.',
'High emissions from oil combustion.',
'Varied sources with different emissions levels.',
'Storage method that does not emit carbon.',
'Zero emissions harnessing solar power.',
'Zero emissions capturing wind energy.']}

return pd.DataFrame(intensity_factors)

def generation_interconnection(self, gen_df: pd.DataFrame) -> alt.Chart:
interconnector_mapping = {
"INTELEC": "England(INTELEC)",
"INTEW": "Wales(INTEW)",
"INTFR": "France(INTFR)",
"INTGRNL": "Greenland(INTGRNL)",
"INTIFA2": "Ireland-Scotland(INTIFA2)",
"INTIRL": "Ireland(INTIRL)",
"INTNED": "Netherlands(INTNED)",
"INTNEM": "Belgium(INTNEM)",
"INTNSL": "Norway(INTNSL)",
"INTVKL": "Denmark(INTVKL)"
}

df_interconnectors = gen_df[gen_df['fuel_type'].isin(
interconnector_mapping)].copy()

df_interconnectors['fuel_type'] = df_interconnectors['fuel_type'].map(
interconnector_mapping)
df_interconnectors.rename(
columns={'fuel_type': 'country'}, inplace=True)

return alt.Chart(df_interconnectors[["publish_time", "generated", "country"]]).mark_line().encode(x=alt.X('publish_time:T', title='Time'), y=alt.Y('generated:Q', title='Energy Generated MW'), color="country").properties(width=1400, height=1000)

def generate_dashboard(self, gen_df: pd.DataFrame, demand_df: pd.DataFrame, cost_df: pd.DataFrame, carbon_df: pd.DataFrame):
st.set_page_config(layout="wide")
st.title("Energy Monitor")
with st.container():
st.header("Carbon Intensity Forecast")
col1, col2 = st.columns(2)
with col1:
st.altair_chart(self.carbon_plots(carbon_df))
carbon_container = st.container(border=True)
with carbon_container:
low_col, high_col = carbon_container.columns(2)
with low_col:
st.metric(label="Lowest Carbon Footprint at:", value=f"{self.time_of_lowest_carbon(
carbon_df)}", delta=f"{min(carbon_df["forecast"].values)}")
with high_col:
st.metric(label="Highest Carbon Footprint at:", value=f"{
self.time_of_highest_carbon(carbon_df)}", delta=f"{max(carbon_df["forecast"].values)}", delta_color="inverse")
with col2:
st.header("What is Carbon Intensity?")
st.write("The carbon intensity of electricity is a measure of how much CO2 emissions are produced per kilowatt hour of electricity consumed.The carbon intensity of electricity is sensitive to small changes in carbon-intensive generation. Carbon intensity varies due to changes in electricity demand, low carbon generation (wind, solar, hydro, nuclear, biomass) and conventional generation.")
with st.expander("Find out more"):
Method_tab, Factors_tab = st.tabs(
["Methodology", "Intensity Factors"])
with Method_tab:
st.write("The demand and generation by fuel type (gas, coal, wind, nuclear, solar etc.) for each region is forecast several days ahead at 30-min temporal resolution using an ensemble of state-of-the-art supervised Machine Learning (ML) regression models. An advanced model ensembling technique is used to blend the ML models to generate a new optimised meta-model. The forecasts are updated every 30 mins using a nowcasting technique to adjust the forecasts a short period ahead. The carbon intensity factors are applied to each technology type for each import generation mix to calculate the forecast")
with Factors_tab:
st.table(self.intensity_factors_df())

with st.container():
st.header("Energy Generation by Fuel Type")
col1, col2 = st.columns([3, 1])
with col1:
st.altair_chart(self.generation_fuel_type(gen_df))
with col2:
with st.expander("Fuel Type Information"):
st.write("""
- **BIOMASS**: Organic materials used as a renewable energy source. It includes plant materials and animal waste that can be burned or converted to biofuels.
- **CCGT (Combined Cycle Gas Turbine)**: A form of highly efficient energy generation that combines a gas-fired turbine with a steam turbine.
- **COAL**: Traditional fossil fuel used for generating electricity through combustion.
- **NPSHYD (Non-Pumped Storage Hydro)**: Refers to the conventional hydroelectric generation where water flows through turbines to generate power, with no pumped storage components.
- **NUCLEAR**: Power generated using nuclear reactions.Provides a stable base-load energy supply.
- **OCGT (Open Cycle Gas Turbine)**: A type of gas power plant where air is compressed, mixed with fuel, and burned in a simple cycle. It's less efficient than CCGT but is often quicker to start and stop.
- **OIL**: Oil-fired power plants burn petroleum or its derivatives to generate electricity. Similar to coal, it has large environmental impacts.
- **OTHER**: This category usually encompasses any generation sources not specifically listed, which could include experimental or less common technologies like tidal or certain types of bio-gas plants.
- **PS (Pumped Storage)**: A type of hydropower generation where water is pumped to a higher elevation during low demand periods and released through turbines for electricity during high demand.
- **WIND**: The use of wind turbines to convert wind energy into electricity. It's a clean, renewable source increasingly used worldwide, particularly in areas with strong, consistent winds.
""")

with st.container():
st.header("Where is your energy coming from ? ")
st.write("Interconnectors are high-voltage links allowing electricity to flow between regions or countries, providing crucial flexibility and reliability to power supplies. They help balance energy demand and supply, integrate renewable energy sources, and enhance grid resilience and sustainability. This leads to more stable electricity prices and a more reliable power supply for everyone.")
st.altair_chart(self.generation_interconnection(gen_df))


if __name__ == "__main__":
connection = DataLoader.connect_to_db()
gen_df, demand_df, cost_df, carbon_df = DataLoader.fetch_data_from_tables(
connection)
energy_dashboard = dashboard()
energy_dashboard.generate_dashboard(gen_df, demand_df, cost_df, carbon_df)
5 changes: 5 additions & 0 deletions dashboard/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pandas
streamlit
psycopg2
altair
python-dotenv
12 changes: 6 additions & 6 deletions pipeline/extract_carbon.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def fetch_data(self) -> Optional[Dict[str, Any]]:
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
self.logger.error(f"An error occurred: {e}")
self.logger.error("An error occurred: %s", e)
return None


Expand Down Expand Up @@ -135,19 +135,18 @@ def execute(self) -> Optional[pd.DataFrame]:

if data:
self.logger.info("Data fetched successfully from API.")
self.logger.debug(f"Fetched Data: {data}")
self.logger.debug("Fetched Data: %s", data)

self.logger.info("Processing the fetched data.")
df = self.data_processor.process_data(data)

if df is not None:
self.logger.info("Data processed successfully into DataFrame.")
self.logger.debug("DataFrame of Carbon Forecast Data:")
self.logger.debug(df.to_string())
self.logger.debug("DataFrame of Carbon Forecast Data:\n%s", df.to_string())

self.logger.info("Saving the processed data locally.")
self.data_processor.save_data_locally(df)
self.logger.info(f"Data saved locally at `{self.data_processor.save_location}`.")
self.logger.info("Data saved locally at `%s`.", self.data_processor.save_location)

self.logger.info("Attempting to get S3 client.")
s3_client = self.data_processor.get_s3_client()
Expand All @@ -156,7 +155,8 @@ def execute(self) -> Optional[pd.DataFrame]:
self.logger.info("S3 client retrieved successfully.")
self.logger.info("Uploading the data to S3.")
self.data_processor.save_data_to_s3()
self.logger.info(f"Data successfully uploaded to S3 bucket `{self.s3_bucket}` as `{self.s3_file_name}`.")
self.logger.info("Data successfully uploaded to S3 bucket `%s` as `%s`.",
self.s3_bucket, self.s3_file_name)
else:
self.logger.error("Failed to get S3 client. Data was not uploaded to S3.")
return df
Expand Down
Loading

0 comments on commit 5427110

Please sign in to comment.