diff --git a/analysis/Snakefile b/analysis/Snakefile index b9e5328..d8245f9 100644 --- a/analysis/Snakefile +++ b/analysis/Snakefile @@ -27,6 +27,7 @@ rule collect_figures: elasticity_plot = f"{docs_path}figures/elasticity.pgf", example_pareto_plot = f"{docs_path}figures/truss2d_pareto.pgf", near_optimal_plot = f"{docs_path}figures/near-optimal-pareto.pgf", + interior_points_plot = f"{docs_path}figures/nd-mga-paretofront.pgf", pareto_3d = f"{docs_path}figures/3d-mga-paretofront.pgf" output: log = figure_log @@ -82,6 +83,7 @@ rule plot_example_fronts: input: "scripts/pareto_front.py" output: example_pareto_plot = f"{docs_path}figures/truss2d_pareto.pgf", + interior_points_plot = f"{docs_path}figures/nd-mga-paretofront.pgf", near_optimal_plot = f"{docs_path}figures/near-optimal-pareto.pgf" script: f"{input}" diff --git a/analysis/notebooks/energy-justice.ipynb b/analysis/notebooks/energy-justice.ipynb new file mode 100644 index 0000000..571dfba --- /dev/null +++ b/analysis/notebooks/energy-justice.ipynb @@ -0,0 +1,196 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import requests\n", + "import matplotlib.pyplot as plt\n", + "import json\n", + "import numpy as np\n", + "import pandas as pd\n", + "import geopandas as gpd\n", + "import us\n", + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv(\"../../.env\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "eia_key = os.environ['EIA_API_KEY']" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Access power plant data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "params = {\n", + " \"frequency\": \"monthly\",\n", + " \"data\": [\n", + " \"latitude\",\n", + " \"longitude\",\n", + " \"nameplate-capacity-mw\",\n", + " \"operating-year-month\"\n", + " ],\n", + " \"facets\": {\n", + " \"energy_source_code\": [\n", + " \"BIT\",\n", + " \"SUB\"\n", + " ]\n", + " },\n", + " \"start\": \"2024-09\",\n", + " \"end\": None,\n", + " \"sort\": [\n", + " {\n", + " \"column\": \"period\",\n", + " \"direction\": \"desc\"\n", + " }\n", + " ],\n", + " \"offset\": 0,\n", + " \"length\": 5000\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "BASE_URL = \"https://api.eia.gov/v2/\"\n", + "\n", + "dataset = \"electricity/operating-generator-capacity\"\n", + "route = dataset + \"/data\"" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "headers = {\n", + " \"X-Api-Key\": eia_key,\n", + " \"X-Params\": json.dumps(params),\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.get(BASE_URL+route, headers=headers)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total records: 428\n" + ] + } + ], + "source": [ + "response = r.json()['response']\n", + "\n", + "print(f\"Total records: {response['total']}\")\n", + "\n", + "data = response['data']\n", + "\n", + "df = pd.DataFrame(data)\n", + "df.drop(columns=['stateName','sector','sectorName','entityid',\n", + " 'balancing-authority-name','statusDescription', \n", + " 'entityName','nameplate-capacity-mw-units',\n", + " 'energy_source_code','generatorid','status',\n", + " 'unit', 'period'\n", + " ],\n", + " inplace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "df['nameplate-capacity-mw'] = df['nameplate-capacity-mw'].astype('float')" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "df = df.pivot_table(index=['stateid','plantid','balancing_authority_code','latitude','longitude'],\n", + " columns=['technology'],\n", + " values=['nameplate-capacity-mw']).reset_index(drop=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "thesis", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/analysis/scripts/farthest_first_example.py b/analysis/scripts/farthest_first_example.py new file mode 100644 index 0000000..039e63c --- /dev/null +++ b/analysis/scripts/farthest_first_example.py @@ -0,0 +1,82 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib as mpl +import pandas as pd +import itertools as it +from matplotlib import patches +from mycolorpy import colorlist as mcp + +from scipy.optimize import nnls +from scipy.optimize import curve_fit +from scipy.spatial.distance import pdist +from scipy.spatial.distance import squareform + +# pymoo imports +from pymoo.problems import get_problem +from pymoo.algorithms.moo.nsga2 import NSGA2 +from pymoo.optimize import minimize +from pymoo.util.plotting import plot +from pymoo.visualization.scatter import Scatter + +from osier import n_mga +from osier.utils import * +from osier import distance_matrix +from osier import farthest_first +from osier import check_if_interior + +mpl.use("pgf") +plt.rcParams['pgf.texsystem'] = 'pdflatex' +plt.rcParams['text.usetex'] = True +plt.rcParams['pgf.rcfonts'] = False +plt.rcParams['figure.edgecolor'] = 'k' +plt.rcParams['figure.facecolor'] = 'w' +plt.rcParams['savefig.dpi'] = 600 +plt.rcParams['savefig.bbox'] = 'tight' +plt.rcParams['font.family'] = "serif" + + + +if __name__ == "__main__": + problem = get_problem("bnh") + + pop_size = 100 + n_gen = 200 + algorithm = NSGA2(pop_size=pop_size) + + res = minimize(problem, + algorithm, + ('n_gen', n_gen), + seed=1, + verbose=False, + save_history=True + ) + + F = problem.pareto_front() + a = min(F[:,0]) + b = max(F[:,0]) + f1 = F[:,0] + f2 = F[:,1] + shift = 0.75 + slack = 0.2 + alpha = 0.5 + F1 = f1 * (1+slack) + F2 = f2 * (1+slack) + X_hist = np.array([history.pop.get("X") + for history in res.history]).reshape(n_gen*pop_size,2) + F_hist = np.array([history.pop.get("F") + for history in res.history]).reshape(n_gen*pop_size,2) + + slack_front = np.c_[F1,F2] + int_pts = check_if_interior(points=F_hist, + par_front=F, + slack_front=slack_front) + X_int = X_hist[int_pts] + F_int = F_hist[int_pts] + + D = distance_matrix(X=X_int) + + n_pts = 10 + idxs = farthest_first(X=X_int, D=D, n_points=n_pts, seed=45) + + F_select = F_int[idxs] + X_select = X_int[idxs] \ No newline at end of file diff --git a/analysis/scripts/pareto_front.py b/analysis/scripts/pareto_front.py index 715dda0..b5bc7d0 100644 --- a/analysis/scripts/pareto_front.py +++ b/analysis/scripts/pareto_front.py @@ -67,4 +67,37 @@ ax.set_xlim(min(F1),max(F1)) ax.set_ylim(min(F2),max(F2)) ax.legend(fontsize=14) - plt.savefig("../docs/figures/near-optimal-pareto.pgf") \ No newline at end of file + plt.savefig("../docs/figures/near-optimal-pareto.pgf") + + + """ + Visualize interior points + """ + F3 = F*(1+slack) + + rng = np.random.default_rng(seed=1234) + R = rng.uniform(1000,2) + R[:,1] = R[:,1]*15e4 + R[:,0] = R[:,0]*8e-2 + R_sub = R[(R[:,1] < 12e4) & (R[:,0] < 0.06)] + + interior_pts = [] + for p in R_sub: + cond_1 = np.any((p < F3).sum(axis=1) == 2) + cond_2 = np.any((p > F).sum(axis=1) == 2) + if cond_1 and cond_2: + interior_pts.append(p) + + fig, ax = plt.subplots(figsize=(8,6)) + ax.plot(*zip(*F), color='black', lw=3, label='Pareto Front') + ax.plot(*zip(*F3), color='black', lw=1, alpha=0.2) + ax.scatter(*zip(*R_sub), c='tab:blue', s=3, label='Tested points') + ax.scatter(*zip(*np.array(interior_pts)), c='tab:red', s=20, label="Alternative solutions") + ax.fill(np.append(F[:,0], F3[:,0][::-1]), np.append(F[:,1],F3[:,1][::-1]), alpha=0.2, color='gray') + ax.fill_between(f1*0.98, f2*0.98, alpha=1, color='w') + ax.set_xlim(min(F1),max(F1)) + ax.set_ylim(min(F2),max(F2)) + ax.set_xlabel('f1', fontsize=14) + ax.set_ylabel('f2', fontsize=14) + ax.legend(fontsize=14, shadow=True, loc='upper right') + plt.savefig("../docs/figures/nd-mga-paretofront.pgf") \ No newline at end of file diff --git a/docs/3-osier/34-mga.tex b/docs/3-osier/34-mga.tex index 7164f8e..e3c1bfc 100644 --- a/docs/3-osier/34-mga.tex +++ b/docs/3-osier/34-mga.tex @@ -1,2 +1,64 @@ -\section{\acf{mga}} -This section talks about handling ``structural uncertainty'' with \ac{osier}. \ No newline at end of file +\section{\acs{mga} with \acl{moo}} +\label{section:mga-moo} + +\textcolor{red}{This section talks about handling ``structural uncertainty'' with \ac{osier}.} + + +This thesis applies some ideas from \ac{mga} to the analysis of the sub-optimal +space from a \acl{moo} problem. Due to their iterative process, \acp{ga} +naturally generate many samples in a problem's feasible space. However, this +does not lead to a ``limited set'' of solutions but rather a potentially +infinite set. Some literature developed \acp{ga} that directly use \ac{mga} in +the iterative process +\cite{zechman_evolutionary_2004,zechman_evolutionary_2013}. However, existing +Python libraries such as \ac{pymoo} and \ac{deap} do not implement these +methods, and the challenge is not an inability to sample the sub-optimal space, +but rather to provide a comprehensible subset of solutions. The algorithm I +developed in this thesis to search the near-feasible space is the following: + +\begin{enumerate} + \item Obtain a set of Pareto-optimal solutions \textit{using any \ac{ga}}. + \item Decide on a slack value (e.g., 10\% or 0.1), which represents an + acceptable deviation from the Pareto front. + \item Create a ``near-feasible front'' where the coordinates of each point + are multiplied by unity plus the slack value. This is equivalent to relaxing + the objective functions and converting them to a constraint. + \item Every individual is checked if all of its coordinates are + \begin{itemize} + \item below all of the coordinates for at least one point on the + near-feasible front and + \item above all of the coordinates for at least one point on the Pareto + front. + \end{itemize} + \item Lastly, the set of interior points may be sampled either randomly or with a + farthest-first-traversal algorithm to restrict the number of analyzed solutions. +\end{enumerate} +\noindent +Figure \ref{fig:nd-mga} and Figure \ref{fig:3d-mga} demonstrate this algorithm +with 10 percent slack for a 2-D and 3-D Pareto front, respectively. Figure +\ref{fig:nd-mga} shows clearly that only points within the near-optimal space +(gray) are considered. Illustrating this behavior in three dimensions (and +above) is considerably more difficult. The 3-D interior points should be covered +by both surfaces, obstructing their view. Figure \ref{fig:3d-mga} shows that +this is the case in three panels. First, a top view of an opaque Pareto front +(green) where no interior points can be observed. Second, the same view with a +translucent Pareto front, revealing interior points and the near-optimal front +(blue). Finally, the view from underneath the near-optimal front once again +obscures the interior points, except for two near the edges of the sub-optimal +space. The tested points are omitted for clarity. + + +\begin{figure}[h] + \centering + \resizebox{0.6\columnwidth}{!}{\input{figures/nd-mga-paretofront.pgf}} + \caption{All of the alternative points inside the near-feasible space selected + using the algorithm described in Section \ref{section:mga-moo}.} + \label{fig:nd-mga} +\end{figure} + +\begin{figure}[H] + \centering + \resizebox{1\columnwidth}{!}{\input{figures/3d-mga-paretofront.pgf}} + \caption{From left to right: An opaque Pareto front; a translucent Pareto front showing the interior points above a sub-optimal front; and the sub-optimal front hiding the interior points from a different angle.} + \label{fig:3d-mga} +\end{figure} \ No newline at end of file diff --git a/environment.yml b/environment.yml index d394adb..a87e975 100644 --- a/environment.yml +++ b/environment.yml @@ -17,7 +17,7 @@ dependencies: - dask - pandas - scipy - - numpy + - numpy<2.0 - matplotlib - seaborn - unyt @@ -53,9 +53,10 @@ dependencies: - deap - dill - openpyxl - - osier + - git+https://github.com/arfc/osier.git - highspy - python-dotenv - git+https://github.com/kmax12/gridstatus.git - us + - mycolorpy \ No newline at end of file