From 2462804d549a81b4c64bdb720579b391e2748800 Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Thu, 14 Mar 2024 20:04:19 +0000 Subject: [PATCH 01/10] initial commit --- CHIMERA_V1.ipynb | 923 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 923 insertions(+) create mode 100644 CHIMERA_V1.ipynb diff --git a/CHIMERA_V1.ipynb b/CHIMERA_V1.ipynb new file mode 100644 index 0000000..d7d0759 --- /dev/null +++ b/CHIMERA_V1.ipynb @@ -0,0 +1,923 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "source": [ + "pip install sunpy" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "aMdifgamAEpp", + "outputId": "9894abba-dd40-4480-f47a-6b3296220a41" + }, + "execution_count": 30, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Requirement already satisfied: sunpy in /usr/local/lib/python3.10/dist-packages (5.1.1)\n", + "Requirement already satisfied: astropy!=5.1.0,>=5.0.6 in /usr/local/lib/python3.10/dist-packages (from sunpy) (5.3.4)\n", + "Requirement already satisfied: numpy>=1.21.0 in /usr/local/lib/python3.10/dist-packages (from sunpy) (1.25.2)\n", + "Requirement already satisfied: packaging>=19.0 in /usr/local/lib/python3.10/dist-packages (from sunpy) (24.0)\n", + "Requirement already satisfied: parfive[ftp]>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from sunpy) (2.0.2)\n", + "Requirement already satisfied: pyerfa>=2.0 in /usr/local/lib/python3.10/dist-packages (from astropy!=5.1.0,>=5.0.6->sunpy) (2.0.1.1)\n", + "Requirement already satisfied: PyYAML>=3.13 in /usr/local/lib/python3.10/dist-packages (from astropy!=5.1.0,>=5.0.6->sunpy) (6.0.1)\n", + "Requirement already satisfied: tqdm>=4.27.0 in /usr/local/lib/python3.10/dist-packages (from parfive[ftp]>=2.0.0->sunpy) (4.66.2)\n", + "Requirement already satisfied: aiohttp in /usr/local/lib/python3.10/dist-packages (from parfive[ftp]>=2.0.0->sunpy) (3.9.3)\n", + "Requirement already satisfied: aioftp>=0.17.1 in /usr/local/lib/python3.10/dist-packages (from parfive[ftp]>=2.0.0->sunpy) (0.22.3)\n", + "Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (1.3.1)\n", + "Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (23.2.0)\n", + "Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (1.4.1)\n", + "Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (6.0.5)\n", + "Requirement already satisfied: yarl<2.0,>=1.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (1.9.4)\n", + "Requirement already satisfied: async-timeout<5.0,>=4.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (4.0.3)\n", + "Requirement already satisfied: idna>=2.0 in /usr/local/lib/python3.10/dist-packages (from yarl<2.0,>=1.0->aiohttp->parfive[ftp]>=2.0.0->sunpy) (3.6)\n" + ] + } + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "id": "sqDeNTSrJ7YA" + }, + "outputs": [], + "source": [ + "#import required libraries\n", + "import astropy\n", + "from astropy import wcs\n", + "from astropy.io import fits\n", + "from astropy.modeling.models import Gaussian2D\n", + "import astropy.units as u\n", + "from astropy.utils.data import download_file\n", + "from astropy.visualization import astropy_mpl_style\n", + "import cv2\n", + "import glob\n", + "import mahotas\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import scipy\n", + "import scipy.interpolate\n", + "import sunpy\n", + "import sunpy.map\n", + "import sys\n", + "from scipy.interpolate import interp2d, RectBivariateSpline\n", + "\n", + "plt.style.use(astropy_mpl_style)" + ] + }, + { + "cell_type": "code", + "source": [ + "#load in required fits file. Make sure to use full disk images\n", + "im171 = glob.glob('171.fts')\n", + "im193 = glob.glob('193.fts')\n", + "im211 = glob.glob('211.fts')\n", + "imhmi = glob.glob('hmi.fts')" + ], + "metadata": { + "id": "npCkEG_xLQEz" + }, + "execution_count": 32, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#make sure all required files exist\n", + "if im171 == [] or im193 == [] or im211 == [] or imhmi == []:\n", + "\tprint(\"Not all required files present\")\n", + "\tsys.exit()" + ], + "metadata": { + "id": "6EwDwosRLS00" + }, + "execution_count": 33, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#reads in fits files and scales images to a size of 4096. Ensures correct image resolution before processing.\n", + "\n", + "'''Changes: switched interp2d to RectBivariateSpline, changed syntax for np.arrange\n", + "to be more compatabile with current python version, changed variable name x to \"scaled_array\",\n", + "added an \"if statement\" at the end to ensure that image resolutions are correct before processing'''\n", + "\n", + "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", + "\n", + "scaled_array=np.arange(start = 0, stop = 4096, step = 4)\n", + "\n", + "hdu_number=0\n", + "heda=fits.getheader(im171[0],hdu_number)\n", + "data= fits.getdata(im171[0], ext=0)/(heda[\"EXPTIME\"])\n", + "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,data)\n", + "data=dn(np.arange(0,4096),np.arange(0,4096))\n", + "\n", + "if len(data) != 4096:\n", + " print(\"Incorrect image resolution\")\n" + ], + "metadata": { + "id": "mp__D-fxLmTO" + }, + "execution_count": 34, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#reads in fits files and scales images to a size of 4096. Ensures correct image resolution before processing.\n", + "\n", + "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", + "\n", + "hedb=fits.getheader(im193[0],hdu_number)\n", + "datb= fits.getdata(im193[0], ext=0)/(hedb[\"EXPTIME\"])\n", + "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datb)\n", + "datb=dn(np.arange(0,4096),np.arange(0,4096))\n", + "\n", + "if len(datb) != 4096:\n", + " print(\"Incorrect image resolution\")\n" + ], + "metadata": { + "id": "woeQladjLn_q" + }, + "execution_count": 35, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", + "\n", + "hedc=fits.getheader(im211[0],hdu_number)\n", + "datc= fits.getdata(im211[0], ext=0)/(hedc[\"EXPTIME\"])\n", + "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datc)\n", + "datc=dn(np.arange(0,4096),np.arange(0,4096))\n", + "\n", + "if len(datc) != 4096:\n", + " print(\"Incorrect image resolution\")\n", + "\n", + "print(datc)\n" + ], + "metadata": { + "id": "EAqYkrcXLp9I", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "e7813ba4-5457-4527-e5b1-8ad6ed3634f1" + }, + "execution_count": 36, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[[ 3.42794344e-19 -9.93202024e-18 -1.28940303e-17 ... 1.50616176e-18\n", + " 1.50616176e-18 1.50616176e-18]\n", + " [ 6.10773115e-18 -9.98323479e-02 -1.14094112e-01 ... -1.70009813e-02\n", + " -1.70009813e-02 -1.70009813e-02]\n", + " [ 8.81061434e-18 -1.50645453e-01 -1.72166232e-01 ... -1.94296929e-02\n", + " -1.94296929e-02 -1.94296929e-02]\n", + " ...\n", + " [ 3.53679056e-18 -4.80522035e-02 -5.76100609e-02 ... 2.80098706e-01\n", + " 2.80098706e-01 2.80098706e-01]\n", + " [ 3.53679056e-18 -4.80522035e-02 -5.76100609e-02 ... 2.80098706e-01\n", + " 2.80098706e-01 2.80098706e-01]\n", + " [ 3.53679056e-18 -4.80522035e-02 -5.76100609e-02 ... 2.80098706e-01\n", + " 2.80098706e-01 2.80098706e-01]]\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "'''Changes: to get rid of indexing error, copied scaling from data, datb, datc so that cell 189 runs correctly (before the data was out of the range of the image resolution which was 1024 instead of 4096)\n", + "Exposure time for hmi is zero, didn't scale by exposure time'''\n", + "\n", + "hedm=fits.getheader(imhmi[0],hdu_number)\n", + "datm= fits.getdata(imhmi[0], ext=0)\n", + "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datm)\n", + "datm=dn(np.arange(0,4096),np.arange(0,4096))\n", + "\n", + "if len(datm) != 4096:\n", + " print(\"Incorrect image resolution\")\n", + "\n", + "print(datm)\n" + ], + "metadata": { + "id": "vifW2C7HLr2u", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "4b998d95-4a45-441d-e9af-c754235949d3" + }, + "execution_count": 63, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "WARNING: VerifyWarning: Invalid 'BLANK' keyword in header. The 'BLANK' keyword is only applicable to integer data, and will be ignored in this HDU. [astropy.io.fits.hdu.image]\n", + "WARNING:astropy:VerifyWarning: Invalid 'BLANK' keyword in header. The 'BLANK' keyword is only applicable to integer data, and will be ignored in this HDU.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[[-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", + " -2.14748368e+08 -2.14748368e+08]\n", + " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", + " -2.14748368e+08 -2.14748368e+08]\n", + " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", + " -2.14748368e+08 -2.14748368e+08]\n", + " ...\n", + " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", + " -2.14748368e+08 -2.14748368e+08]\n", + " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", + " -2.14748368e+08 -2.14748368e+08]\n", + " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", + " -2.14748368e+08 -2.14748368e+08]]\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "#rotates array if 'crota1' is greawter than 90\n", + "if hedm['crota1'] > 90:\n", + "\tdatm=np.rot90(np.rot90(datm))" + ], + "metadata": { + "id": "z4JBQBiULtrG" + }, + "execution_count": 64, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#defines the shape (length) of the array as \"s\" and the solar radius as \"rs\"\n", + "s=np.shape(data)\n", + "rs=heda['rsun']" + ], + "metadata": { + "id": "0ZPZxHgrLwfY" + }, + "execution_count": 39, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#ensures \"cype1\" and \"ctype2\" are correctly defined as \"solar_x\" and \"solar_y\" respectively\n", + "if hedb[\"ctype1\"] != 'solar_x ':\n", + "\thedb[\"ctype1\"]='solar_x '\n", + "\thedb[\"ctype2\"]='solar_y '" + ], + "metadata": { + "id": "qS50C1BZLyMI" + }, + "execution_count": 40, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#rescales \"cdelt1\", \"cdelt2\", \"cpix1\", and \"cpix2\" if \"cdelt1\" > 1\n", + "if heda['cdelt1'] > 1:\n", + "\theda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0\n", + "\thedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0\n", + "\thedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0" + ], + "metadata": { + "id": "Kz4S85e4L1_S" + }, + "execution_count": 41, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#converts pixel values to arcseconds\n", + "dattoarc=heda['cdelt1']\n", + "conver=(s[0]/2)*dattoarc/hedm['cdelt1']-(s[1]/2)\n", + "convermul=dattoarc/hedm['cdelt1']" + ], + "metadata": { + "id": "acXRp68LL33M" + }, + "execution_count": 42, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#Changes to Heliographic Stonyhurst coordinate system\n", + "\n", + "'''TO FIX: Warnings for illegal keyword names'''\n", + "\n", + "aia=sunpy.map.Map(im171)\n", + "adj=4096./aia.dimensions[0].value\n", + "x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj\n", + "hpc = aia.pixel_to_world(x, y)\n", + "hg=hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst)\n", + "\n", + "csys=wcs.WCS(hedb)\n" + ], + "metadata": { + "id": "K1sknmYxL6hf", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "8fdcf6a0-99cf-4bcc-a332-6b457fe964d9" + }, + "execution_count": 43, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "WARNING: VerifyWarning: Verification reported errors: [astropy.io.fits.verify]\n", + "WARNING:astropy:VerifyWarning: Verification reported errors:\n", + "WARNING: VerifyWarning: Card 35: [astropy.io.fits.verify]\n", + "WARNING:astropy:VerifyWarning: Card 35:\n", + "WARNING: VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O' [astropy.io.fits.verify]\n", + "WARNING:astropy:VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O'\n", + "WARNING: VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", + " [astropy.io.fits.verify]\n", + "WARNING:astropy:VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", + "\n", + "WARNING: VerifyWarning: Verification reported errors: [astropy.io.fits.verify]\n", + "WARNING:astropy:VerifyWarning: Verification reported errors:\n", + "WARNING: VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O' [astropy.io.fits.verify]\n", + "WARNING:astropy:VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O'\n", + "WARNING: VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", + " [astropy.io.fits.verify]\n", + "WARNING:astropy:VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", + "\n", + "WARNING: FITSFixedWarning: CROTACN1= 0.00000000000 / Rotation x center \n", + "keyword looks very much like CROTAn but isn't. [astropy.wcs.wcs]\n", + "WARNING:astropy:FITSFixedWarning: CROTACN1= 0.00000000000 / Rotation x center \n", + "keyword looks very much like CROTAn but isn't.\n", + "WARNING: FITSFixedWarning: CROTACN2= 0.00000000000 / Rotation y center \n", + "keyword looks very much like CROTAn but isn't. [astropy.wcs.wcs]\n", + "WARNING:astropy:FITSFixedWarning: CROTACN2= 0.00000000000 / Rotation y center \n", + "keyword looks very much like CROTAn but isn't.\n", + "WARNING: FITSFixedWarning: 'datfix' made the change 'Invalid DATE-OBS format '31-Jan-2024 00:24:40.843''. [astropy.wcs.wcs]\n", + "WARNING:astropy:FITSFixedWarning: 'datfix' made the change 'Invalid DATE-OBS format '31-Jan-2024 00:24:40.843''.\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "#setting up arrays to be used in later processing\n", + "ident=1\n", + "iarr=np.zeros((s[0],s[1]),dtype=np.byte)\n", + "offarr,slate=np.array(iarr),np.array(iarr)\n", + "bmcool=np.zeros((s[0],s[1]),dtype=np.float32)\n", + "cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool)\n", + "circ=np.zeros((s[0],s[1]),dtype=int)" + ], + "metadata": { + "id": "QUE4rIA7L-UZ" + }, + "execution_count": 44, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#creation of a 2d gaussian for magnetic cut offs\n", + "\n", + "r = (s[1]/2.0)-450\n", + "xgrid,ygrid=np.meshgrid(np.arange(s[0]),np.arange(s[1]))\n", + "center=[int(s[1]/2.0),int(s[1]/2.0)]\n", + "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2)\n", + "y,x=np.mgrid[0:4096,0:4096]\n", + "garr=Gaussian2D(1,s[0]/2,s[1]/2,2000/2.3548,2000/2.3548)(x,y)\n", + "garr[w]=1.0" + ], + "metadata": { + "id": "elqZFWcnMBrZ" + }, + "execution_count": 45, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#creates sub-arrays of props to isolate column of index 0 and column of index 1\n", + "props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','',''\n", + "props[:,1]='num','\"','\"','H°','\"','\"','\"','\"','\"','\"','\"','\"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx'" + ], + "metadata": { + "id": "06jPD4pRMGdw" + }, + "execution_count": 46, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#removes negative data values\n", + "data[np.where(data <= 0)]=0\n", + "datb[np.where(datb <= 0)]=0\n", + "datc[np.where(datc <= 0)]=0" + ], + "metadata": { + "id": "XpbOolCwMVMF" + }, + "execution_count": 47, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#ignores division errors in the following logarithms and sets conditions for t0, t1, and t2\n", + "with np.errstate(divide = 'ignore'):\n", + "\tt0=np.log10(datc)\n", + "\tt1=np.log10(datb)\n", + "\tt2=np.log10(data)\n", + "t0[np.where(t0 < 0.8)] = 0.8\n", + "t0[np.where(t0 > 2.7)] = 2.7\n", + "t1[np.where(t1 < 1.4)] = 1.4\n", + "t1[np.where(t1 > 3.0)] = 3.0\n", + "t2[np.where(t2 < 1.2)] = 1.2\n", + "t2[np.where(t2 > 3.9)] = 3.9" + ], + "metadata": { + "id": "L3c366LOMQcW" + }, + "execution_count": 48, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#makes a multi-wavelength image for contours\n", + "t0=np.array(((t0-0.8)/(2.7-0.8))*255,dtype=np.float32)\n", + "t1=np.array(((t1-1.4)/(3.0-1.4))*255,dtype=np.float32)\n", + "t2=np.array(((t2-1.2)/(3.9-1.2))*255,dtype=np.float32)" + ], + "metadata": { + "id": "3oO_rrbgMZ4x" + }, + "execution_count": 49, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#ignores division and invalid erros in the following conditions to create 3 segmented bitmasks\n", + "with np.errstate(divide = 'ignore',invalid='ignore'):\n", + "\tbmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1\n", + "\tbmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1\n", + "\tbmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1" + ], + "metadata": { + "id": "Q9jr_gA-Mc6J" + }, + "execution_count": 50, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#conjunction of 3 segmentations\n", + "cand=bmcool*bmmix*bmhot" + ], + "metadata": { + "id": "U_avvniSMe7a" + }, + "execution_count": 51, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#plot tricolour image with lon/lat contours\n", + "'''TO FIX: there is no code written for this section'''\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "id": "-cady694uJY9", + "outputId": "bbd1fe49-75f1-4794-d970-415127af8f2b" + }, + "execution_count": 52, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "'TO FIX: there is no code written for this section'" + ], + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" + } + }, + "metadata": {}, + "execution_count": 52 + } + ] + }, + { + "cell_type": "code", + "source": [ + "#removes off detector mis-identifications\n", + "r=(s[1]/2.0)-100\n", + "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2)\n", + "circ[w]=1.0\n", + "cand=cand*circ" + ], + "metadata": { + "id": "rpZ36REKMggU" + }, + "execution_count": 53, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#seperates on-disk and off-limb coronal holes\n", + "circ[:]=0\n", + "r=(rs/dattoarc)-10\n", + "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2)\n", + "circ[w]=1.0\n", + "r=(rs/dattoarc)+40\n", + "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 >= r**2)\n", + "circ[w]=1.0\n", + "cand=cand*circ" + ], + "metadata": { + "id": "q8AzT5xXMgh-" + }, + "execution_count": 54, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#open file for property storage\n", + "'''TO FIX: No code for this section'''" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "id": "iVgAh6Y-ut80", + "outputId": "df72cbe1-0887-443c-f826-e7cd914436e0" + }, + "execution_count": 55, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "'TO FIX: No code for this section'" + ], + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" + } + }, + "metadata": {}, + "execution_count": 55 + } + ] + }, + { + "cell_type": "code", + "source": [ + "#contours the identified datapoints\n", + "cand=np.array(cand,dtype=np.uint8)\n", + "cont,heir=cv2.findContours(cand,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)" + ], + "metadata": { + "id": "G911hr4d01R0" + }, + "execution_count": 57, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#sorts contours by size\n", + "sizes=[]\n", + "for i in range(len(cont)):\n", + "\tsizes=np.append(sizes,len(cont[i]))\n", + "reord=sizes.ravel().argsort()[::-1]\n", + "tmp=list(cont)\n", + "for i in range(len(cont)):\n", + "\ttmp[i]=cont[reord[i]]\n", + "cont=list(tmp)" + ], + "metadata": { + "id": "lQI5QT8mMnQ9" + }, + "execution_count": 58, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "#=====cycles through contours=========\n", + "\n", + "for i in range(len(cont)):\n", + "\n", + "\tx=np.append(x,len(cont[i]))\n", + "\n", + "#=====only takes values of minimum surface length and calculates area======\n", + "\n", + "\tif len(cont[i]) <= 100:\n", + "\t\tcontinue\n", + "\tarea=0.5*np.abs(np.dot(cont[i][:,0,0],np.roll(cont[i][:,0,1],1))-np.dot(cont[i][:,0,1],np.roll(cont[i][:,0,0],1)))\n", + "\tarcar=(area*(dattoarc**2))\n", + "\tif arcar > 1000:\n", + "\n", + "#=====finds centroid=======\n", + "\n", + "\t\tchpts=len(cont[i])\n", + "\t\tcent=[np.mean(cont[i][:,0,0]),np.mean(cont[i][:,0,1])]\n", + "\n", + "#===remove quiet sun regions encompassed by coronal holes======\n", + "\n", + "\t\tif (cand[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0) and (iarr[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0):\n", + "\t\t\tmahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate)\n", + "\t\t\tiarr[np.where(slate == 1)]=0\n", + "\t\t\tslate[:]=0\n", + "\n", + "\t\telse:\n", + "\n", + "#====create a simple centre point======\n", + "\n", + "\t\t\tarccent=csys.all_pix2world(cent[0],cent[1],0)\n", + "\n", + "#====classifies off limb CH regions========\n", + "\n", + "\t\t\tif (((arccent[0]**2)+(arccent[1]**2)) > (rs**2)) or (np.sum(np.array(csys.all_pix2world(cont[i][0,0,0],cont[i][0,0,1],0))**2) > (rs**2)):\n", + "\t\t\t\tmahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),offarr)\n", + "\t\t\telse:\n", + "\n", + "#=====classifies on disk coronal holes=======\n", + "\n", + "\t\t\t\tmahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate)\n", + "\t\t\t\tposlin=np.where(slate == 1)\n", + "\t\t\t\tslate[:]=0\n", + "\t\t\t\tprint(poslin)\n", + "\n", + "#====create an array for magnetic polarity========\n", + "\n", + "\t\t\t\tpos=np.zeros((len(poslin[0]),2),dtype=np.uint)\n", + "\t\t\t\tpos[:,0]=np.array((poslin[0]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint)\n", + "\t\t\t\tpos[:,1]=np.array((poslin[1]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint)\n", + "\t\t\t\tnpix=list(np.histogram(datm[pos[:,0],pos[:,1]],bins=np.arange(np.round(np.min(datm[pos[:,0],pos[:,1]]))-0.5,np.round(np.max(datm[pos[:,0],pos[:,1]]))+0.6,1)))\n", + "\t\t\t\tnpix[0][np.where(npix[0]==0)]=1\n", + "\t\t\t\tnpix[1]=npix[1][:-1]+0.5\n", + "\n", + "\t\t\t\twh1=np.where(npix[1] > 0)\n", + "\t\t\t\twh2=np.where(npix[1] < 0)\n", + "\n", + "#=====magnetic cut offs dependant on area=========\n", + "\n", + "\t\t\t\tif np.absolute((np.sum(npix[0][wh1])-np.sum(npix[0][wh2]))/np.sqrt(np.sum(npix[0]))) <= 10 and arcar < 9000:\n", + "\t\t\t\t\tcontinue\n", + "\t\t\t\tif np.absolute(np.mean(datm[pos[:,0],pos[:,1]])) < garr[int(cent[0]),int(cent[1])] and arcar < 40000:\n", + "\t\t\t\t\tcontinue\n", + "\t\t\t\tiarr[poslin]=ident\n", + "\n", + "#====create an accurate center point=======\n", + "\n", + "\t\t\t\typos=np.sum((poslin[0])*np.absolute(hg.lat[poslin]))/np.sum(np.absolute(hg.lat[poslin]))\n", + "\t\t\t\txpos=np.sum((poslin[1])*np.absolute(hg.lon[poslin]))/np.sum(np.absolute(hg.lon[poslin]))\n", + "\n", + "\t\t\t\tarccent=csys.all_pix2world(xpos,ypos,0)\n", + "\n", + "#======calculate average angle coronal hole is subjected to======\n", + "\n", + "\t\t\t\tdist=np.sqrt((arccent[0]**2)+(arccent[1]**2))\n", + "\t\t\t\tang=np.arcsin(dist/rs)\n", + "\n", + "#=====calculate area of CH with minimal projection effects======\n", + "\n", + "\t\t\t\ttrupixar=abs(area/np.cos(ang))\n", + "\t\t\t\ttruarcar=trupixar*(dattoarc**2)\n", + "\t\t\t\ttrummar=truarcar*((6.96e+08/rs)**2)\n", + "\n", + "\n", + "#====find CH extent in lattitude and longitude========\n", + "\n", + "\t\t\t\tmaxxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])]\n", + "\t\t\t\tmaxxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])]\n", + "\t\t\t\tmaxylat=hg.lat[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]]\n", + "\t\t\t\tmaxylon=hg.lon[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]]\n", + "\t\t\t\tminxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])]\n", + "\t\t\t\tminxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])]\n", + "\t\t\t\tminylat=hg.lat[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]]\n", + "\t\t\t\tminylon=hg.lon[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]]\n", + "\n", + "#=====CH centroid in lat/lon=======\n", + "\n", + "\t\t\t\tcentlat=hg.lat[int(ypos),int(xpos)]\n", + "\t\t\t\tcentlon=hg.lon[int(ypos),int(xpos)]\n", + "\n", + "#====caluclate the mean magnetic field=====\n", + "\n", + "\t\t\t\tmB=np.mean(datm[pos[:,0],pos[:,1]])\n", + "\t\t\t\tmBpos=np.sum(npix[0][wh1]*npix[1][wh1])/np.sum(npix[0][wh1])\n", + "\t\t\t\tmBneg=np.sum(npix[0][wh2]*npix[1][wh2])/np.sum(npix[0][wh2])\n", + "\n", + "#=====finds coordinates of CH boundaries=======\n", + "\n", + "\t\t\t\tYwb,Xwb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0]),0)\n", + "\t\t\t\tYeb,Xeb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0]),0)\n", + "\t\t\t\tYnb,Xnb=csys.all_pix2world(np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0],0)\n", + "\t\t\t\tYsb,Xsb=csys.all_pix2world(np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0],0)\n", + "\n", + "\t\t\t\twidth=round(maxxlon.value)-round(minxlon.value)\n", + "\n", + "\t\t\t\tif minxlon.value >= 0.0 : eastl='W'+str(int(np.round(minxlon.value)))\n", + "\t\t\t\telse : eastl='E'+str(np.absolute(int(np.round(minxlon.value))))\n", + "\t\t\t\tif maxxlon.value >= 0.0 : westl='W'+str(int(np.round(maxxlon.value)))\n", + "\t\t\t\telse : westl='E'+str(np.absolute(int(np.round(maxxlon.value))))\n", + "\n", + "\t\t\t\tif centlat >= 0.0 : centlat='N'+str(int(np.round(centlat.value)))\n", + "\t\t\t\telse : centlat='S'+str(np.absolute(int(np.round(centlat.value))))\n", + "\t\t\t\tif centlon >= 0.0 : centlon='W'+str(int(np.round(centlon.value)))\n", + "\t\t\t\telse : centlon='E'+str(np.absolute(int(np.round(centlon.value))))\n", + "\n", + "#====insertions of CH properties into property array=====\n", + "\n", + "\t\t\t\tprops[0,ident+1]=str(ident)\n", + "\t\t\t\tprops[1,ident+1]=str(np.round(arccent[0]))\n", + "\t\t\t\tprops[2,ident+1]=str(np.round(arccent[1]))\n", + "\t\t\t\tprops[3,ident+1]=str(centlon+centlat)\n", + "\t\t\t\tprops[4,ident+1]=str(np.round(Xeb))\n", + "\t\t\t\tprops[5,ident+1]=str(np.round(Yeb))\n", + "\t\t\t\tprops[6,ident+1]=str(np.round(Xwb))\n", + "\t\t\t\tprops[7,ident+1]=str(np.round(Ywb))\n", + "\t\t\t\tprops[8,ident+1]=str(np.round(Xnb))\n", + "\t\t\t\tprops[9,ident+1]=str(np.round(Ynb))\n", + "\t\t\t\tprops[10,ident+1]=str(np.round(Xsb))\n", + "\t\t\t\tprops[11,ident+1]=str(np.round(Ysb))\n", + "\t\t\t\tprops[12,ident+1]=str(eastl+'-'+westl)\n", + "\t\t\t\tprops[13,ident+1]=str(width)\n", + "\t\t\t\tprops[14,ident+1]='{:.1e}'.format(trummar/1e+12)\n", + "\t\t\t\tprops[15,ident+1]=str(np.round((arcar*100/(np.pi*(rs**2))),1))\n", + "\t\t\t\tprops[16,ident+1]=str(np.round(mB,1))\n", + "\t\t\t\tprops[17,ident+1]=str(np.round(mBpos,1))\n", + "\t\t\t\tprops[18,ident+1]=str(np.round(mBneg,1))\n", + "\t\t\t\tprops[19,ident+1]=str(np.round(np.max(npix[1]),1))\n", + "\t\t\t\tprops[20,ident+1]=str(np.round(np.min(npix[1]),1))\n", + "\t\t\t\ttbpos= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] > 0)])\n", + "\t\t\t\tprops[21,ident+1]='{:.1e}'.format(tbpos)\n", + "\t\t\t\ttbneg= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] < 0)])\n", + "\t\t\t\tprops[22,ident+1]='{:.1e}'.format(tbneg)\n", + "\t\t\t\tprops[23,ident+1]='{:.1e}'.format(mB*trummar*1e+16)\n", + "\t\t\t\tprops[24,ident+1]='{:.1e}'.format(mBpos*trummar*1e+16)\n", + "\t\t\t\tprops[25,ident+1]='{:.1e}'.format(mBneg*trummar*1e+16)\n", + "\n", + "#=====sets up code for next possible coronal hole=====\n", + "\n", + "\t\t\t\tident=ident+1\n", + "\n", + "#=====sets ident back to max value of iarr======\n", + "\n", + "ident=ident-1\n", + "np.savetxt('ch_summary.txt', props, fmt = '%s')\n", + "\n", + "#====create image in output folder=======\n", + "#from scipy.misc import bytescale\n", + "\n", + "from skimage.util import img_as_ubyte\n", + "\n", + "def rescale01(arr, cmin=None, cmax=None, a=0, b=1):\n", + " if cmin or cmax:\n", + " arr = np.clip(arr, cmin, cmax)\n", + " return (b-a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a\n", + "\n", + "\n", + "def plot_tricolor():\n", + "\ttricolorarray = np.zeros((4096, 4096, 3))\n", + "\n", + "\tdata_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9))\n", + "\tdata_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0))\n", + "\tdata_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7))\n", + "\n", + "\ttricolorarray[..., 0] = data_c/np.max(data_c)\n", + "\ttricolorarray[..., 1] = data_b/np.max(data_b)\n", + "\ttricolorarray[..., 2] = data_a/np.max(data_a)\n", + "\n", + "\n", + "\tfig, ax = plt.subplots(figsize = (10, 10))\n", + "\n", + "\tplt.imshow(tricolorarray, origin = 'lower')#, extent = )\n", + "\tcs=plt.contour(xgrid,ygrid,slate,colors='white',linewidths=0.5)\n", + "\tplt.savefig('tricolor.png')\n", + "\tplt.close()\n", + "\n", + "def plot_mask(slate=slate):\n", + "\tchs=np.where(iarr > 0)\n", + "\tslate[chs]=1\n", + "\tslate=np.array(slate,dtype=np.uint8)\n", + "\tcont,heir=cv2.findContours(slate,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)\n", + "\n", + "\tcirc[:]=0\n", + "\tr=(rs/dattoarc)\n", + "\tw=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2)\n", + "\tcirc[w]=1.0\n", + "\n", + "\tplt.figure(figsize=(10,10))\n", + "\tplt.xlim(143,4014)\n", + "\tplt.ylim(143,4014)\n", + "\tplt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2)\n", + "\tplt.gca().set_aspect('equal', adjustable='box')\n", + "\tplt.axis('off')\n", + "\tcs=plt.contour(xgrid,ygrid,slate,colors='black',linewidths=0.5)\n", + "\tcs=plt.contour(xgrid,ygrid,circ,colors='black',linewidths=1.0)\n", + "\n", + "\tplt.savefig('CH_mask_'+hedb[\"DATE\"]+'.png',transparent=True)\n", + "\t#plt.close()\n", + "#====stores all CH properties in a text file=====\n", + "\n", + "plot_tricolor()\n", + "plot_mask()\n", + "\n", + "#====EOF====" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "OGhJ_fJT6j3t", + "outputId": "c00d4c6e-bdbb-4ca3-e426-ad0c36a70112" + }, + "execution_count": 62, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "(array([ 582, 582, 582, ..., 3207, 3208, 3208]), array([1674, 1675, 1676, ..., 3069, 3067, 3068]))\n", + "(array([2606, 2606, 2606, ..., 2687, 2688, 2688]), array([2408, 2409, 2410, ..., 2347, 2345, 2346]))\n", + "(array([1611, 1611, 1611, ..., 1826, 1826, 1826]), array([2278, 2279, 2280, ..., 2332, 2333, 2334]))\n", + "(array([1268, 1268, 1268, ..., 1551, 1551, 1551]), array([2994, 2995, 2996, ..., 3285, 3286, 3287]))\n", + "(array([ 621, 621, 621, ..., 1790, 1791, 1791]), array([1463, 1464, 1465, ..., 1153, 1151, 1152]))\n", + "(array([582, 582, 582, ..., 934, 935, 935]), array([1674, 1675, 1676, ..., 1729, 1727, 1728]))\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + ":180: RuntimeWarning: divide by zero encountered in log10\n", + " data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9))\n", + ":181: RuntimeWarning: divide by zero encountered in log10\n", + " data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0))\n", + ":182: RuntimeWarning: divide by zero encountered in log10\n", + " data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7))\n", + ":210: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored\n", + " plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2)\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAMWCAYAAABsvhCnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACvpUlEQVR4nOzdZ2CT5cLG8etJ0klL2RvZQ1DAY9kgIiqKgiIOhr5OXIB7Cx4VFHEdFXAheHAPEMU92HvJlL1lr1JaupO8H3rySJmduTP+vy+2aZpcraXN9dzL8nq9XgEAAABAEThMBwAAAAAQ/CgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIrMZToAAKDodu7cKbfbLUnavXu3nnrqKWVlZeW5z/79+7Vt2zZ5vd5CPYfD4VD9+vWVkJCQ5/b4+Hi99tprKlWqlCQpOjpalSpVKtRzAACCl+Ut7F8YAECx83g82rp1q5KSkvTYY4/pyJEjWr16tdxut7Kzs+XxeExHLBKn0ymXy6WIiAidc845Kl++vF599VXFxsbqrLPOMh0PAFAEFAsA8BOv16tNmzZp9OjR+u2337R582ZlZ2fbIw1FYVmWXK5/BqETEhJUrVq1E+536623qm3btoV6jokTJ+rXX3894fZt27YpLS3Nfj8nJ6fQoyLHcjqdioyMVOPGjXXttdeqb9++ql27dpEfFwBQMigWAFCMUlJSNGvWLP373//W6tWrlZ6eXuAX2Q6HQy6XS/Hx8apZs6ZatGihgQMHyuFwqHnz5nI4gnN5XGZmplatWiVJGjp0qLZt26bNmzcrIyND2dnZhfo+lSpVSi1atNCIESP0r3/9S1FRUSURHQCQDxQLACiErVu36oknntCMGTO0f//+fI86OBwORUVFqW7durr66qt1ww03qEmTJnI6nSWcOLhkZGRow4YNGjlypGbMmKFt27YpKysr3+XD6XSqRo0a6tatmwYPHnzS0RsAQPGiWADAaRw5ckQff/yxhg0blu8C4XK5VKlSJXXu3FnPPvus6tev74ek4cXj8Wjt2rV6/PHHNX/+fB0+fFg5OTln/Dyn06maNWvqlVde0ZVXXqno6Gg/pAWA8ECxAID/OXLkiPr376/ffvtNycnJp706blmWYmNj1axZMw0bNkwXXHBBnjUOMCc9PV3ff/+9hg8frvXr1+dZ/3EylmWpfPnyuvbaa/XGG28wnQoAColiASAs5eTk6KmnntKYMWPOWCIiIiJUt25djR49WhdccIEiIiL8mBTFJS0tTd99950ef/xx7d69+7QjHJZlqUKFCnrqqad0//33y7IsPyYFgOBEsQAQFnbs2KErr7xSq1atOu10pujoaHXo0EHvv/++6tSp48eEMGXRokUaMGCAli9ffsLZH8eKiIhQp06d9NVXX6ls2bJ+TAgAwYFiASAkzZ07V71799aOHTtOORrhcrmUmJiozz//nG1MkceiRYt02223ac2aNacsog6HQw0bNtTkyZPVoEEDPycEgMATnHsWAsBx5s6dq7POOksOh0OWZal9+/b6+++/85SKhIQEvfPOO/buQtnZ2Zo3bx6lAido2bKlVq5caZ/JkZ6eriFDhigmJsa+j28BecOGDWVZlhwOh84++2xt2LDBYHIAMIcRCwBBadu2bbr66qu1YsWKU55GXa1aNX399ddq166dn9MhHHz22We67777dPDgwZN+3Ol0qnPnzvriiy9Uvnx5P6cDAP9jxAJA0HjmmWcUHR0ty7JUu3ZtLVu2LE+pqFatmmbNmiWv1yuv16udO3dSKlBi+vbtqwMHDsjr9crj8eizzz5TuXLl7I+73W798ccfqlChgizLUnx8vD744AODiQGgZDFiASBgbd26VZdddpnWr19/0nUSpUuX1ssvv6z+/fsH7WnUCE1ut1vPPPOM3njjjZNud2tZllq1aqUff/yR0QwAIYNiASCgfP3117rrrruUlJR0wsccDocuueQSTZ48WZGRkQbSAYWTlJSkrl27avHixSctyZUrV9Z3332n1q1bG0gHAMWDS3wAjBsyZIgiIiJkWZauv/76PKUiISFB06ZNk9frldvt1i+//EKpQNApW7asFi5cKI/HI4/Ho3feeUexsbH2x/fu3as2bdrIsizFxMQwZQpAUGLEAoARQ4YM0fDhw0+6lWeTJk00depUVa5c2UAywL9Wrlypbt26aceOHSd8LDIyUqNGjVL//v0NJAOAgmHEAoDfDB48WC6XS5ZladiwYXapsCxLffr0kdvtltfr1V9//UWpQNg499xz7a2Rs7OzdcEFF9gfy8rK0p133inLshQVFaX333/fYFIAOD1GLACUqNdff12PPfbYCSMTvjLxySefyLIsQ+mAwOV2u3XRRRdp5syZJ3wsMjJSn332mXr16mUgGQCcHMUCQLGbPn26unXrpvT09Dy3UyaAwnG73ercubNmzZp1wscSEhK0cuVK1axZ00AyAPgHxQJAsTh69KiaNGmi7du3n/Cxnj17auLEiZQJoBi43W61atVKf/755wkfa9GihRYvXiyn02kgGYBwxxoLAEXSp08fORwOxcXF5SkVjRo1UlZWlrxer7755htKBVBMnE6nlixZIq/Xq+TkZFWpUsX+2LJly+RyueR0OjV48GCDKQGEI0YsABTYrFmz1LVr1xOmOsXFxWnx4sVq1KiRoWRA+Pruu+90ww03KDMzM8/tCQkJWrt2bZ4CAgAlgRELAPni8XjUqVMnWZalCy64wC4VDodDw4cPl9frVUpKCqUCMOSqq65SRkaGvF6vbrvtNvv25ORkVa1aVZZl5bkdAIobIxYATmv9+vU677zzlJaWluf2Ro0aaeXKlYqIiDCUDMCZJCUlqUmTJtqzZ0+e2ytWrKj169erTJkyZoIBCEmMWAA4qT59+siyLDVq1MguFU6nU2PHjpXX69XatWspFUCAK1u2rHbv3i2v16shQ4bYa53279+vsmXLyuFw6OmnnzacEkCoYMQCgC0tLU2NGjU64QTgatWqacuWLYqMjDSUDEBx2bFjh5o2baojR47kub158+ZasmQJO0oBKDRGLABo7ty5ioiIUKlSpexSYVmWXn75ZXm9Xu3cuZNSAYSIGjVqKDk5WV6vV7fccot9+/Lly+VyuRQbG6sDBw6YCwggaFEsgDD28ssvy7IstW/fXjk5OZKk2NhYLV++XB6PR48++qjhhABK0ocffiiv16tJkybZUxvT09NVsWJFORwOff/994YTAggmTIUCwlD37t31ww8/5LmtadOmWrVqlaFEAAKB2+1WtWrVtG/fvjy3P/roo3r55ZcNpQIQLCgWQJjweDxq2LChNm3alOf2G264QV988YWhVAACVdu2bTV//vw8t3Xs2FEzZ840lAhAoGMqFBDiUlNTVbp0aTmdTrtUOBwOvffee/J6vZQKACc1b948eb1eDRgwwL5t1qxZsixL1atXl9vtNpgOQCBixAIIUampqapatapSU1Pt21wul5YsWaJmzZoZTAYgGE2aNEm9evXSsS8bqlatqu3bt8vlchlMBiBQUCyAEJOSkqJq1arlKRSlS5fW3r17FR0dbTAZgFCwdetWNWrUSFlZWfZt1apV07Zt2ygYQJijWAAhgkIBwJ+2bNmis88+W5mZmfZtFAwgvFEsgCCXnJysGjVqUCgAGMEIBgAfigUQpE5WKBISErRnzx4KBQC/27Ztmxo2bJinYLAGAwgvFAsgyBw9elRVqlShUAAISFu3blXjxo1PmCK1fft2OZ1Og8kAlDS2mwWChNfrVb169RQXF2eXioSEBB09elSHDx+mVAAICLVr11ZGRoa2bt2qyMhISdKuXbvkcrl04YUXmg0HoERRLIAg0KVLFzkcDm3evFmSFBMTYxeK2NhYw+kA4ES1atVSZmamtm7dao9UzJgxQ5ZlaeDAgYbTASgJFAsggN1///2yLEtTp06VlHsOxV9//aW0tDQKBYCgUKtWLeXk5Oibb76RZVmSpNGjR8uyLI0ZM8ZwOgDFiTUWQAD69ttv1bNnzzy3vf/+++rfv7+hRABQPB588EG98cYb9vsOh0Nr165V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U+3bt1e9evX0+OOPq3///pKkc889V6tWrZLEDAL4H5PgUWgrVqywS8VFF11EqQAAhJU///xTGzdu1Lp16+T1evXmm28W+TE7dOigm266SQ6HQ4mJiae8X1RUlGrUqKHNmzfrzjvv1FdffSWPx6OVK1eqSpUqkqTGjRtzOjf8ihELFMqxB+BVqlRJe/fuNZwIAAD/evjhh+0D7qKjozV79ux8f+7atWt11113qVq1avr888/t25OTk7Vv3748i7ZPZ9++ferWrZv9vm/tRVxcnLKysuRwOOR2u/OdCygKRixQKL4TRp1OJ6UCABCWXnvtNXm9Xg0ZMqTAB9TdeOONSk9P14YNG5SYmKhWrVrpzTffVJcuXdSnTx/16dOnQI933nnnacGCBZKkmJgYezqUx+NRfHx8gR4LKCyKBQqsXLly9mIx35kVAACEq48//liWZRX489xutzIyMuytZT/++GPVqlVLzz//vDZs2KCjR4+e8TEqVaqkxYsXa8yYMXI6nZo0aZLcbrcaNmyoIUOGSJJSU1MLdL4GUFgUCxRI27Zt7ZM+d+zYwVkVAICwNmDAAG3dulXVqlXL9+csWbLEfjsqKkqDBg1SRkaGvF6vtm7daheCa665psB5atSoocWLFysxMVFDhw7VzTffLCl3XeS9995b4McDCoJXhci3oUOH2mdVjB07VtWrVzecCAAAs8aMGSMpd6vX/Nq/f78kadu2bae8z5dffqmDBw/af3cL6t1331Xz5s01fvx4u6C88847+u233wr1eEB+sHgb+bJ8+XK1aNFCktS9e3dNnjzZbCAAAALAJ598optuukm33XbbKUcE9uzZY+/U5NOqVSt5vV7l5OSccvT/rLPO0t9//605c+YoKiqqUPk6d+6slJQU1atXT5s2bZIkpaSkKC4urlCPB5wOxQJndOwOUDVq1NDff/9tOBEAAIHBt7ZixowZKlWqVJ6P9e/fX0uXLpUk1a1bV1999ZUkqV27dqpataq2bdumK664Qj/88MMpH9/hcMjr9Wr06NFq3bp1gfNNmDBBL730khwOhyIjI5WRkXHCwXpAcaFY4IxcLpfcbrciIiJYrA0AwDFq1aql7du3n/Qgu9atW8uyLFmWpezs7JN+/siRIzVw4MDTPofL5VKpUqU0derUfOfKzs5Wnz59tHXrVvu2mJgYpaenS5LKlCljr5kEigtrLHBatWvXtve/LuhWegAAhDrfi3NfcdiwYYM95eiuu+5STk6OlixZou7du+vHH3/U4sWLlZGRoVmzZikjI+OMpULKLRY5OTl5bhswYIDGjx9/ys/p0qWLtm7dqvr169u3paen61//+pck6fDhw+rRo0fBvljgDCgWOKVBgwbZC8tWrVrFDlAAABzn8OHDcjgcateundasWaM+ffrohhtukCQdPHhQknTuuedq8uTJ6tatm84//3xFRUWpQ4cO+V434fF4lJaWpvvvv1+S9Pzzz2vBggUaOXKkVqxYcdLPadeunSSdsFvVn3/+qeuvv16S9P333+vHH38s+BcNnAJToXBSS5YsUWJioqTcgvHWW28ZTgQAQGDyeDxyOp2S/hld6NmzpyZNmqQ6depo8+bNRX4Op9Op0qVL648//tCyZct0xx132B+75557dPvtt5/wOf369dO6devkcrk0b948bd++Xffcc4/27dunxMREe/oWi7lRXCgWOMGxi7Xr1aunjRs3Gk4EAEBg27Jli9asWaMrrrjCvq24SoXb7ZbL5dJ//vMfdezYUZK0a9cu9ejRQ7GxsUpLS1Pv3r31yCOP5OvxfDtSxcbG6ujRoyzmRrFhbgtOEBMTI0mKiIigVAAAkA916tRR6dKlJUmRkZHyer3FUip8jy3JLhVS7hSn1q1bKy0tTZL0xRdfaO3atad8jJtvvlmtWrWSJFWpUkVer9e+iOj1elW2bNliyYrwRrFAHi1atGCxNgAAhdChQwd5vV5lZmYW6+OeanLJ6NGjNWjQIPv9J554wn77iy++UGJiohITE7Vq1SqtWbNGHo9HKSkpGjBggGJiYpSUlGSXlcOHD+uuu+4q1twIPxQL2IYNG6bly5dLkn744QcWawMAEAC++eabPP/12bBhg0aPHm2/f9VVV9lvv/nmm/bbt9xyiz3VqXv37nr66aeVnp6uzp07a9asWerXr58k6f3339f06dNL6stAGOCVIyRJR48e1ZAhQyTl/mI6do4oAAAwp2/fvpJ0wkhInz59FBkZqYsvvliSNG7cOPtjQ4cOtXeduvTSS7Vv3z5dd911ec6jqlmzphwOhz7//HPVq1dPUu5J3UBhsXgbkv452TMuLk4pKSmm4wAAAOWe2L1lyxZ16dJFI0aM0O+//67p06fr119/lSRNnDhR11xzjb78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lK/D75555hlJ0hNPPFGimYBQ4dskITExUVu3bi3x5xs8eLA8Ho/atWtX4s8FwCzf2TTDhw83nCT0sMYiHziZESeze/duVatWTZK0YMEC+6T2U0lMTFS3bt30448/+iMeENQOHjyoZs2aac+ePbIsSwsWLCix5+rRo4d27dqlK664Qj/++KMcDscZD8EEEJyys7MVGRkpidd1JYERiwJg1ALHqlq1qj1tok2bNrrjjjtOe/9zzjlHP/30Ey9YgHwoX768du7cqe3bt8vtduuqq64qsefatWuXJNmlv0yZMiX2XADMGjt2rCSd8WIgCodikQ++xbfXXXed4SQINC6XSy1btlSZMmW0bNmy0973ww8/lGVZ9pUSAGdWvXp19ejRQzt37lSrVq30zjvvFPtzvPXWWxo5cqR9GN/BgweL/TkABAbflORzzjnHcJLQRLHIB98P4dy5cw0nQSBauHChoqOjJeWOXJyKZVmaO3euPB6POnTo4K94QND77rvvFBkZKY/HY19tLE7t2rXThg0bdOjQIQ0aNKjYHx9A4EhOTpYk/fDDD4aThCbWWORDVlaWvZc63y6czK233qrx48fL6/Wqd+/eeuSRR05532uvvVbbtm1jhyiggG6//XaNGzdOCQkJmjJlSpEfb8+ePerZs6c9pTEiIkJpaWn52pABQHBi3WzJYsQiH46dupKfrUURfj788EN5PB5FRkbq+++/P+19L774Ynm9XnvXGwD5M3bsWJUrV65Yzpl46qmndOWVV8rtdmv48OHyeDzKysqiVAAhbObMmZLEgccliGKRT75ycc899xhOgkDmdrvP+Avr7rvvltPp1NNPP+2nVEDouO++++T1ejVy5MhCP8aoUaP022+/aeDAgXK73XriiSd4oQGEgT59+kiSatasaThJ6KJY5NPVV18tSfr000/NBkFAq1q1qlJTU7Vz587T3q969eo6cOCAlixZ4qdkQGj497//re7du2v8+PGFLheffPKJ4uLiilROAAQf3w5wn3/+ueEkoYs1FvnEOgvkl8PhUNmyZfXbb7+d9n4tW7ZUXFxcsUzrAMJNjx499P333+v5559Xt27dCvS5rVq1UuXKle0XGQDCA+srSh4jFvl07DqLjIwMg0kQ6KpXr65Dhw6d8X4NGjRQSkqKHxIBoWfy5MmqWbOmnnnmGe3YsaNAn+vxeBQR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p1Nutzvs1sXcfvvtWr58OVd8AKCYVKtWTbt37y7Q35PExETFxcXpgw8+4IVyPnB2RWCiBpeQ+Ph4zrQwaMSIEfZVnmnTpuUpFT7PPvusduzYoZdeekkffvihfWjRzTff7Nespo0dO1Z33nmnvF4vxQIATqFFixbq0KGDqlSpYp+P5PPJJ5/YZyRdccUV2r17t66//vp8P/bcuXMl5U7TpVTkD2dXBCaKRQm66667JElvvPGG2SBh5sCBA3ryySflcrm0ePFiRUZG2h/bt2+f2rVrJ0nq2LGjqlevrscff1yWZal9+/a69dZb9ddff5mKboyvhDmdTv3000+G0wBA4Fm+fLnmzJmjvXv36vfff1fFihUVExMjy7J000032Wcm/fTTT2rWrJkee+yxfD+272yKTz/9tESyh6J169ZJkn777TfDSXAspkKVMN+oxd69e1WpUiXDacKDb7vf2bNn51mM7fV61bZtW+Xk5Khv3772L/CuXbvm+cVUunRpTZ061e+5A8Ell1yipKQkde7cOWy/BwBwMsfuyuTbRvZYcXFxmj59eqEe++DBg+rataveeOMN3X///UWJGRbGjRun22+/XRLToAINIxYlLCEhQZLsw1tQsvbu3ausrCzdf//9dql49NFHlZiYqJYtWyonJ0evvvpqnqtCM2bMsK/YJyYmhvUL6t9//13du3fXtGnTVLp0aWVnZ5uOBAABYeTIkfbbl1xyiV599VX7/cWLFxeqVIwfP16dOnVS165dFRUVRanIpzvvvFOSdMEFFxhOguMxYlHC/v77b5111lmSaNX+MHLkSN13332KjIxUTk6OvWbguuuuU69evU46d3Xq1Knq0qWLZs6cqdjYWH9HDkjffPONXnzxRUVHRys9Pd10HAAwzuPxKC4uTunp6Zo/f76cTqfuvvtuDR48WDVr1izw4/3444/697//LYfDoY8++kj9+vUrgdSh59hF2x6Ph/M9AgwjFiWsZs2a9g993759DacJfYMGDdK5554rKXdXjiFDhsjr9eqrr7465YK4iy66SFLu/Fnkuuaaa/TAAw8oIyPDdBQA8JvExERZlqWLL744z+07duyQ0+lUenq6xowZI5fLJcuy9N577xWqVEiyt0J3u92UigKoU6eOJNnrWxBYGLHwgxdeeEGDBw+WxKhFIMrJyVFERITGjx+vpk2bmo4TMLxer1q2bKmoqCgtXLjQ3oEDAEJVzZo17RLhcrn0448/qkuXLnI6nfJ6vZo7d669SLsoli1bpgEDBigzM1MbNmwo8CF64cxXJvi+BSZGLPzg6aeftt/etWuXwSQ4ma+++kqSKBXHsSxLL730krKzs9W8eXN17tzZdCQAKFF///23pNxRhMzMTI0aNUrr1q2Tx+MptlIhSRMnTlRmZqYkqUGDBsXymOFgxIgR9tuUisBEsfAT31BpvXr1DCfB8XxTpI4tgMh18cUXa+HChSpbtqxmzJhhOg4AlKi33npLUu5UWim3YLRp00aSiq1USNLQoUPtx33kkUeK7XFD3ZNPPilJ6t27t+EkOBWKhZ9s3bpVkpSRkaGcnByzYWB77rnnFBUVpTJlyujXX381HSdguVwueb1etW/f3nQUACgx9913nyTpiiuukCR9//33Onz4sIYMGVLszzVq1ChJ0ubNm4v9sUPRihUr7Onkn3/+ueE0OBWKhZ84HA77oLbzzjvPcBpIuSecPvvss3K73Tp8+DCnd55G6dKlJeWeDrtw4ULDaQCg+EyfPl1OpzPPi9VbbrlFixcv1vz58zVr1ixdddVVJfb8FIv8adu2rSSpRo0ahpPgdCgWfjRp0iRJ0qpVqwwnCW933323LMtSpUqVZFmWunfvrq5du2rWrFmmowWsL7/8Uj/88IMkqXXr1qpbty5nXAAICdu3b5fH41Hfvn3thcG+C4Eul6tELzo1aNBAy5Yt4/fpGXi9XqWlpUn658RtBCaKhR9169bNfvvBBx80mCS8ffDBB4qOjlabNm3sfcRfeOEF07ECXpUqVewrelu2bFFkZKQsy9KHH35oOBkAFN6GDRvst6tUqaJZs2b5bRtT3wFvTJE+Pd828i6Xi/OmAhzFws98i7TeeOMNs0HCVNmyZeV2u9WxY0eNGjVKlSpVMh0pqDRo0EBTp061TyqXpJ07dxpMBACF07lzZ3322WcaPny4IiMjdd555+k///mP36bF3n777Ro7dqzKly/PVNwz+OuvvyRJs2fPNpwEZ8I5Fgb4roR8/fXXuvbaaw2nCS/x8fFKTU213x8xYoS6dOliMFFweu6557R8+XJt375dK1assK8mAUAwiImJUUZGhlwul3JyclShQgX98ssvfs2QmJioChUqaP/+/X593mDTp08fffHFF5I4CywYMGJhQKtWrSTl/mOBf6WkpMjr9crr9apGjRp6/PHH7e0FfX755RetWLHCUMLg8O9//1vvvPOOJKlZs2ZyOBzyeDyGUwHA6X300UeyLEsZGRmS/pmCdODAAb+8aJ0zZ45atmypxMRESdKNN95Y4s8Z7Hyl4tgzLBC4GLEwwOPxyOl0Sso9MK9q1aqGE4Wv9u3ba+7cuVq8eLH+85//6NNPP7U/dsUVV+i5554zmC6wpaamqnPnzipXrpwOHjwoSXI6nTpy5AhzYAEEjP379+vaa6/VnDlz5Ha7FRERoezsbFWoUEEffPCBrr76aklSbGysZs6cWWI5nn/+eU2ePFmxsbHavHmzKleuXGLPFSref/993XXXXZIYrQgWFAtDKleurH379ik+Pl5HjhwxHSdspaamKj4+3n6/UaNGWrt2rSIiImRZlubNm2cwXfDIzs5Wt27dlJSUJCl3e+XmzZurf//+uvPOO+V0OrVr1y7169dP06ZNM5wWQDipWLGiDhw4IMuyNGvWLEVHR+f5eMuWLeX1ejVo0CDdfPPNxf78s2fP1gMPPCBJuummm/TRRx8V+3OEKqfTKY/Ho+7du2vy5Mmm4yAfKBaGHDlyRAkJCZJyX5S5XC7DicLXK6+8ounTp+v999/X3LlzNXz4cC1dulRvvvkmB8IV0KeffqopU6Zo5cqV9tWlhIQEHT58WLVq1dL27dt1/fXX68svv/R7tjvvvFNLly5VvXr17KF1AKHP4XCobNmy+u2330768W3btmnr1q3q1KlTsT/3119/rREjRiguLk579+5lNLcAVqxYoebNm0titCKYUCwMio6OVmZmpipVqqS9e/eajhP2mjVrppUrV0rKHVH68ccfDScKbm63W998841GjBihyMhIZWVlybIseb1ebdy4UW3atFFGRoYSEhLsfeRLomBnZ2dr//79ql69un3b9u3bVbNmzWJ/LgCBx7IsDR482J7y5C/t27dXZmamXC6X0tLSFBER4dfnD3YRERHKyclRvXr1tHHjRtNxkE8s3jZo27ZtkqR9+/YZTgJJatOmjf12dna2MjIylJKSoldeeYXDiwrB6XTquuuu0x133KGsrCzVqVPHvupUv359HTx4UDk5Odq5c6ecTqciIiIUFRWlunXrqmHDhurVq5fmzJljjy4MGjRIvXr10nXXXadDhw5p2LBhGj58uIYP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LpDH49FVV10lSbrxxhsNpwJCn+/K+JEjR/Lc7vF47Dn+heHxeIqUK1ytXr3aPoB07969htOguFAsAlRiYqK9r71v/iFCV5kyZSRJ119/vdkgKDETJ07MsyvUe++9p1KlSun1119XcnLySbepBVB83n33XUlSnz59lJiYqFtuuUWS1Lp1a3Xp0kU9e/bUihUrTvsYXq9XaWlpeu6553TllVcqMTFR06dPV9euXUs6fshp3ry5JKlu3bqnPGAWwYf5FgFsx44dqlixovbt26ft27fbw7gILR999JFSUlJ07733MgUqRD3++OOaMmWKIiMjlZmZqYoVK2rs2LFauHChqlatKkl65plnDKcEQlurVq0UHR0tp9Op1q1ba+rUqZo0aZK8Xq+aNGmiNWvW6LbbbtPAgQPt0nG8rl276tChQ3kek10cC27MmDH29rwbN240nAbFicXbAa5ixYo6cOCA/YIEoeX888/Xn3/+KUlavHix4TQoCZs3b9b111+vqKgo/fXXX6pXr54kqV27dpo3b559P34VA/7l24nv2L+vjRs31rp162RZlipXrqwDBw6oadOmuuOOO3TffffJ6/WqatWqmjhxotq2bWsyflDzfe8vuugiTZkyxXAaFCemQgU43wmUWVlZxbqLBczyeDxq166d/vzzT91+++2UihDWp08fOZ1OZWRk2KVCkubOncv6KcAg33lBx24Tu3btWk2ePFlly5bV3r17FRUVpeXLl2vQoEHyer2KiorSli1bKBVFMHjwYPttSkXoYd5FgHO5XGrRooWWLVumzp07s0gsRFSoUEFJSUmSpCVLlhhOg5Iyfvx4ud1uzZw586QfdzgcjFQAhpzqgk737t118OBBP6cJHy+88IIk6b777jOcBCWBEYsgsHTpUkm5UyWefPJJw2lQHLp27SqHwyGXy6Vly5Zp4MCBpiOhmLndbo0cOVL169dXx44dTccBAOPat29vv/3mm28aTIKSQrEIEnfccYck6aWXXjKcBEXl8Xi0du1azZ07V+vWrZMkLVu2zGwoFLsuXbpIyt0yGgDCXWZmpubOnStJmjRpkuE0KCks3g4ivsVObdu2tf9xIvhccMEFmjVrlv1+bGyspk+fbh+KiOC0adMm1alTRw6HQ8nJyerSpYsWLVqUZ4tZAAhXZcuW1eHDh9mMJsRRLILI5MmT7QO1cnJy5HQ6DSdCYSQkJCg1NVWvv/66vvzyS40cOdJ0JBTRnXfeae/u5eNwOFicDQDKXRR/9tlnS5IOHz6shIQEw4lQUigWQSY6OlqZmZkqU6aMvfgXwcU38uQzduxY+6AgBI9XX31V06ZNU1JSkrKysiRJF154oR555BE9+OCD+u233zj0DgAkOZ1OeTwe1a5dW1u2bDEdByWIuRdBZufOnZJyG/+mTZsMp0Fh/P3337r55ptVqlQpSVL//v0NJ0JBDRw4UF988YUOHDhglwopd1rbFVdcofXr11MqAEDSyJEj7R0tN2/ebDgNShrFIsiUL19ederUkSQ1bNjQcBoURo0aNfTf//5Xqamp2rNnjzwej66++mp16NBBl19+uXr16qXs7GzTMXEMj8ejJ598Ujt27FDr1q01f/589evXTzk5OXm2C/b92wQA5PJtK9urV68TRuwRepgKFaR8/zjvvfdejR492nAaFMXtt9+ucePGybKsPPPy77nnHt1+++2G04Wf++67T4sXL9aFF16oyy+/XO3bt1e7du2Uk5MjKXdIf86cOWrdurX9OU2bNlX9+vX13XffmYoNAAGnadOmWr16tSzL4hyuMEGxCFKPPfaYXnnlFUm5++Wzo1Do2LZtm1q2bKn9+/erbdu2mjdvnv2xr7/+mqviJejyyy/X/v37T/qx6OhoXXLJJZo8ebKfUwFA8Dl8+LDKli0rSZo3b57atGljOBH8gWIRxHyLoSpXrqw9e/aYjoNidskll+iPP/5QlSpVtGLFCtWsWVOZmZlasGABO4IVwdGjR7V27Vqdf/759m379+/X5ZdffsJ9t27dqtatW+upp57ilFgAKICIiAjl5OSobNmyOnTokOk48BOKRRDbu3evqlSpIklat24day5CnNvtlsvl0r/+9S+9//77puMEpWeeeUY//fSTJGnx4sWSck+0b9Wq1Qnbw27ZsoUF2ABQCCNHjrQvxng8HtZWhBHmzwSxypUr29NifPtDI3RlZGRIks466yzDSYLLCy+8oMTERF100UX66aef7JGKm266SV6vV23btpXX67VLhWVZcrvdlAoAKCQWbIcvikWQ823d5vF41Lt3b8NpUJJ8e39///33hpMEl0mTJikmJkYZGRlq27atPVKRnp6u9u3b24uyfeuUHn74YdYsAUAh1a1bV1LuRZoJEyYYTgN/YypUCBg1apQGDRokiYXcoczj8ahOnTravn27/eIYp/fCCy9o0qRJOv7XnK9oSLmFwuPx2P/l3xAAFM7mzZtVr149SdLKlSt1zjnnGE4Ef+OvZwgYOHCgoqKiJEnx8fGG06CkOJ1Obd++XX369DEdJWjMmDHjpMPw6enp8nq98nq99sd9WyEeu40sACD/6tevL0mqWbMmpSJMUSxCRGpqqiQpLS1N7733nuE0KAkPPfSQpNxdjZA/1157rbxerypWrHjK++Tk5Mjr9SotLU3r16/XwoUL/ZgQAEJDz5497dHh7du3G04DUygWIcLlcunqq6+WJN19991mw6BENGrUSJLsaW+S9NRTTykxMVGJiYm66667TEULWHfccYf69OmjAwcOaPfu3ae833vvvaekpCQ1aNCAhYYAUEAZGRn69ttvJUlvvfWW2TAwijUWIcbhcMjr9apOnTr2wm6EhmrVqmn37t1yOp1yu9365JNPdOONN6pcuXL2HuGsvTi5xMRERUREKCMj44T1E1FRUcrKyjphu1kAQP5ER0crMzNTsbGxjKqHOUYsQsyff/4pKXcHoalTpxpOg+L0559/qkGDBvbi4htvvFGSVKpUKfs+vtPYkdcdd9yh7Oxs7dmzR127dpVlWbIsSxEREXapiIyMNB0TAILOk08+qczMTEnSwYMHDaeBaRSLENOiRQudd955kqQuXboYToPiVKVKFa1fv14ZGRnasmWLSpcurZkzZ2rLli12yfj5558NpwxMH3zwgaTc815+++03nXPOOerUqZO8Xq8qVKggj8ej/v37G04JAMElKytLL730kiTp/vvvV3R0tOFEMI2pUCHKNyWqVq1a2rp1q+k4KGHx8fFKTU3VhAkTTjjYzbeVarhauXKlbr31Vvv9u+++W3fccYf9vsfjUatWrZSSkqK4uDgTEQEgKPmmQEVFRdlbeCO8he+rjRC3bNkySdK2bds0ZcoUs2FQ4mJiYiRJtWrVynP7I488olatWql9+/bq2rWrunXrpkmTJpmIaIxvNy2Hw6ERI0bkKRWS1LdvX0miVABAATz++OP2FKjDhw+bDYOAwYhFCPvXv/6lpUuXStIJB4QhtHg8HjmdTsXGxqpnz56aOHGiqlWrps2bN6t58+Zavny5LMuyz21YtGjRSR8nJydHDocjZEY4LrjgAqWlpUmSXnrpJV188cV5Pt66dWu53W61aNHC/rcCADi97Oxse13awIEDNXLkSMOJECgoFiHONyWqdu3a2rJli+k4KEFbt25VnTp1JOXudJSZmXnCTkelS5dWamqq5s+fL6fTmefzk5KSdMkll0iSZs6cqdjYWP+FLyE9evTQrl27VLt2bU2YMEFSbslu2bKlXbTS09OZFwwABcAUKJxKaFyWxCktX75cUu6LTqZEhbbatWvbp0lnZGTI6/WesH3qmDFj5PV67dOlMzIy1KpVKyUmJtqlQpJWrFjh1+wl5bvvvtNdd92lzz//3L7Ndy3F6/XqnHPOoVQAQAE8+uijTIHCKTFiEQbOO+88e80F/7shSZZlqVWrVnK73VqyZInmzp2rPn36aNu2berRo4eeeeYZ0xFLjG/EYs+ePapcubLpOAAQNI6dAjVgwACNGjXKcCIEGopFmPBNiapSpcppTyBGeBg2bJiGDBliv+9bo1GpUiX9+OOPBpOVvP79+2vp0qWqWrWqdu3aZToOAASNyMhIZWdnMwUKp8RUqDCxceNGSdKePXvsPf0RvgYPHqzrr79eTZo0kfRP8Xz33XcNJys59957rxITE7V06VJZlqULL7zQdCQACBrXXnutsrOzJTEFCqfGiEUY6dmzp7799ltJubv/HL94F+HHt+Db4XDo6quv1lNPPWU6UolJTExU69attWDBAh09ejQkFqcDgD9s2rRJ9evXlySNHDlSAwcONJwIgYpiEWaioqKUlZWl6Ohopaenm44Dw3xToAYNGqSbb77ZdJwSlZiYKEmqU6eONm/ebDgNAAQP36h2tWrVtHPnTtNxEMCYChVmjh49Kil3N6DevXsbTgPTfLtGLViwwHCSkjdmzBhJ0sMPP2w4CQAEj8aNG9sbv1AqcCYUizDjcrk0YsQISdKXX36pNWvWGE4Ek7p16yZJevvttw0nKXn33nuvnE6nBgwYYDoKAASFUaNGad26dZKkefPmGU6DYECxCEOPPfaYatSoIUn24l2Ep6ysLEnShRdeqKSkJMNpSlZ2drbKly9vOgYABIWcnBwNGjRIknTFFVeoTZs2hhMhGLDGIoxZliVJbEEbxtxutxo1aqTNmzfL6/UqNjZW06ZNC8mF/YmJiTr33HND5vA/AChJvq1lIyIi7ItQwJkwYhHGtm7dKil3C9rhw4ebDQMjnE6nNm7cKI/HoylTpigtLU2tW7dWz549TUcrNg899JDatWsnSfrXv/5lOA0ABL5u3brZW8umpqYaToNgwohFmOvdu7e+/PJLSVJycrJKly5tOBFM2rhxozp16qRdu3apdevWGj16tOlIRdayZUt5vV7VrVtXmzZtMh0HAALa9OnT1blzZ0m5ayxYl4aCoFhA8fHxSk1NlcPhsHcJQnjzbUvs88gjjwTtLmKtWrVSdHS0vSMaAODkvF6vHI7cySxnn322Vq9ebTgRgg1ToaDk5GRJuWca1K5d22wYBITdu3erQYMGeu655+RyufTmm2/q0KFD8ng8pqMV2J133qm0tDTTMQAg4JUqVUpS7rkVlAoUBiMWkCStWrVK5557riRp2LBhevrppw0nQqAoXbq0UlJSJOX+sVm4cKHhRAXTpUsXJScni191AHBql19+uX755RdJuTsGRkREGE6EYMSIBSRJ55xzjj3VZfDgwVq/fr3hRAgUR44ckdfrldPpDLoRiz59+tgjcgCAk/vkk0/sUjFq1ChKBQqNEQvkUblyZe3bt0+SuMKLPHzbE0dGRmr27Nn2PNxA9eSTT+r333/XU089pV69erEjFACchNvtlsvlkiRdcMEFmjFjhuFECGaB/coAfrd37177bQ4Tw7E2bdqk0aNHKysrS6+//ro9PSpQ+dYLvfDCC5QKADiF6OhoSVJERASlAkXGiAVOsHPnTvtk7ksuuUS//fab4UQIJJUqVdL+/fslSYsXLzac5tRWrFih2267jZE3ADiFWrVqafv27ZJYV4HiwYgFTlC9enWNGTNGkvT777/rv//9r9lACCj79u1Tp06dJEnp6emG05xa9erVTUcAgIB166232qViyZIllAoUC4oFTuqOO+7QhRdeKCn3lw9nAOBY06dPlyRt3rzZbJDTWLZsmekIABCQZs6caV80HDJkCNNFUWyYCoXTiouL09GjR2VZVtDtCISSZVmWHA6HvvzyS9WpU8d0nBOMHTtW77zzDlOhAOAYHo9HTqdTktSoUSOtXbvWcCKEEkYscFpHjhyRlLtDFIu5caw///xTHo9H1113nTIyMkzHOQGHOwHAiSIjIyXlLtamVKC4USxwWg6HQzt27JAkHTp0SO3atTOcCIGgQ4cOeYbOX3zxRYNpTm7WrFkqV66c6RgAEDCqVq0qt9stSUxxRomgWOCMqlevrrFjx0qS5s2bp2effdZsIBg3d+5cxcfHa/Hixfr111/1/PPPm450goiICKWmppqOAQABoXv37tqzZ4+k3DVoLNZGSaBYIF9uu+02XXnllZKk5557joWxYSw1NVVer1dnn322pMA976RUqVKsCwIA5a45++GHHyRJzz//vJo3b244EUIVi7dRIDVq1NDOnTslsed1uLruuus0YcIETZ8+XXFxcabjnNJDDz2kmTNnsngbQFg79myqTp062bv6ASWBEQsUyI4dO+RyuST9c1onwktCQoIk2YfkBapzzz3XdAQAMMrj8dilokKFCpQKlDhGLFBgx25VV7p0aSUnJxtOBH+yLCvP+7GxscrOzlZ2drbuvvtu3XHHHYaS/WPHjh26+uqr2UoRQFhzuVxyu91yOBz2om2gJFEsUCi7d+9WtWrVJEmJiYlatGiR4UTwl7S0NE2ePFmzZ8/W7t279eOPP8rhcKhhw4Zavny5nE6nZs+ebXSaXLt27ZSVlcU0KABhq1KlSvbIMlOX4S9MhUKhVK1aVR9++KEkafHixXrkkUcMJ4K/xMbGqnfv3ho1apQmTpyojIwMpaWladmyZVq3bp3cbre6du1qLN9HH32krKwsLVy40FgGADCpdevWdqlYvnw5pQJ+Q7FAod1yyy265557JEmvvfaaXnrpJcOJYFrDhg119tln2wcr+ttnn32mt956S9WqVVPLli2NZAAAk3r27GlfWJk4caKaNWtmOBHCCcUCRfL222+re/fukqQnn3xSf/75p+FEMG3fvn1Gnvftt9/W66+/rooVK9o7lwFAOHn//ff17bffSpKGDh2qa665xmwghB3WWKBYVKtWTbt375Ykpaens2NUGNu2bZtq164tKXcEoWHDhn553tatWys6OppD8QCEpXnz5qldu3aSpDZt2mjevHmGEyEcUSxQbEqVKqW0tDRJLBQLd9u3b1etWrUUExOjWbNm+eU5fX9QMzMz/fJ8ABAofL9zJalBgwZav3694UQIV0yFQrFJTU21t6GNiooynAYmtWjRQpL0wAMP+O05o6KilJWV5bfnA4BAkJqaapeK0qVLUypgFMUCxcayLGVnZ8uyLHm93oA+lRnFq3LlyrIsS9HR0brhhhuUlJSk8uXLq1evXn7L4Cu1HAAFIFx4PB6VLl1aUu6htZwrBdMoFihWlmVp165dkqSjR48qPj7ecCKUNI/Ho3379ql+/frKycnRV199JUn64IMP/JZh165dOnz4sCRpzZo1fnteADDF4/EoIiJCXq9XlmUZ240POBZrLFAi9uzZo6pVq0qS6tatq02bNhlOhOKSkpKi5ORkvfDCC3r33XflcDjk8Xi0aNGiE07l9peLL75Yhw8f5kA8AGEjNjZW6enpsixLmZmZrGtEQGDEAiWiSpUq9j7amzdvVmJiouFEKIqYmBhFR0crLi5OpUuXVs2aNbVs2TJJktfr1XPPPWesVEjS4cOHVaFCBT377LNFehyPx2OPfABAoKpYsaLS09MlSWlpaZQKBAxGLFCiPvroI918882SpPPPP1+LFy82nAgFtXLlSjVr1kwRERHyeDyyLEs5OTmSJIfDERAnXC9atMg+rLFRo0Zau3btSe+3ZcsWXXLJJYqJidGqVatUp04dTZs2TZK0detWXXjhhZKknJwce80GAASSihUr6sCBA5KkVatWqWnTpoYTAf+gWKDEjR8/XrfccoskqX379po9e7bZQCiQunXrasuWLXYpzMjI0KBBg5SWlqZq1arplVdeMZzwHz169NCuXbtOmBKVnZ2tJ598UkuWLLEXd8fHxyslJSXP/eLi4pSamspZLAACUtWqVbVnzx5J0l9//aUmTZoYTgTkRbGAX3z88cf6v//7P0nS4MGDNXToUMOJkF//+c9/9NBDD9lrKXwSEhI0ZcoUv2bJyclRt27dNGjQIPvE92MlJiae9GAoh8Nhl42zzjpL33zzzSmfwzdtr1+/ftqwYYM9ItO5c2dNnTq1uL4UACiQyy+/XL/88oskSgUCF8UCftOzZ099++23kigXweall17SK6+8ovLly6t79+5auXKlfv/9d7300ku6+OKLS/S5X375ZXXq1Enjxo3TkiVL8nysatWqevLJJ+3D8S655BIlJSXlGbEYNmyYhgwZooULF8rhOPWysoyMDEVHR2vlypW6//77T7rDyrp16/x2kjgA+BxbKoYPH64nnnjCcCLg5CgW8KtrrrlGkyZNkkS5CHaWZal06dIlchX/8OHDJy0sF198sVq3bq3hw4erfPny2r9/vyRp8eLF+uqrr/Tyyy+ratWq9pbHvpwREREnjGIca8qUKXr88cftqVCSThih8alXr542btxY1C8RAPLl2FLx4osv6sknnzScCDg1igX8jpGL0GBZluLi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KHAA+mlVLi4u8PHxwZgxY/D222/D39+/nKut2M6ePYuYmBisW7cOaWlpKCgoeOlokoG9vT28vb0xfPhwfP7559KO7kREVD4YLIiIzESj0WDbtm1YtWoVzpw5g7y8vFJ3nDKED6VSibZt26JXr14IDAxEYGBgOVVtHfv27cMff/yBLVu24Pr168jKyoJKpSpxaDCQyWRwd3dHp06d8NlnnyEkJIQBgojIShgsiIgsQKfTIT09HdOnT0dsbCwePnyIp0+fmtTq1tAGFdBP76lWrZr0taCgIHTu3LnIOYMHD4aXl1eZ7pecnIyYmJgix2NiYpCcnCy9zszMlEZxRFE0+Xt0cXFB3bp18frrr2PRokVwcnKCTMZu6UREtobBgojIRuh0OqSlpWHWrFlIS0vD8ePHUVhYiLy8PACoNPttGAKRUqmEnZ0devbsierVq2PhwoUMDUREFRiDBRFRBfTsj26VSoXp06dDpVIBAO7du4cTJ04UCSIqlUp6T1k5OzsbbTwIAHK5HD169JBGTLy8vDBr1iyjgMAF0kRElR+DBRERERERmYzjzUREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhk/w9GtSHWpgpaQwAAAABJRU5ErkJggg==\n" + }, + "metadata": {} + } + ] + } + ] +} \ No newline at end of file From 9d1f36a5c6c1ea01730b20f800029b6274223eec Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Thu, 11 Apr 2024 20:23:39 +0100 Subject: [PATCH 02/10] Working CHIMERA 11/4 --- .DS_Store | Bin 0 -> 6148 bytes CHIMERA_V2.py | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 .DS_Store create mode 100644 CHIMERA_V2.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f23f47c8aa6559a33596e0f23a54d830f83d66f7 GIT binary patch literal 6148 zcmeHKy-LJD5dKC}oY5WaX*u=yVPJ+MMzxGAqaAJAP{BYj5DWwZ!N89)z?!WxJ9Z2m3K{04~N*!%#(NiXkH{Y9#WY*Bsqo- z27-aF3~295DYgE${FEk}d~*pE3sT5t7gg5<=Xn~oLXxuj#~~D_3L#);oM3w l(MmBN^d31equVQ;)1ET+j+#Z+Eu0uX0!B!vVBi-Rcn6BoHueAj literal 0 HcmV?d00001 diff --git a/CHIMERA_V2.py b/CHIMERA_V2.py new file mode 100644 index 0000000..40822e3 --- /dev/null +++ b/CHIMERA_V2.py @@ -0,0 +1,220 @@ +import astropy +from astropy import wcs +from astropy.io import fits +from astropy.modeling.models import Gaussian2D +import astropy.units as u +from astropy.utils.data import download_file +from astropy.visualization import astropy_mpl_style +import cv2 +import glob +import mahotas +import matplotlib.pyplot as plt +import numpy as np +import scipy +import scipy.interpolate +import sunpy +import sunpy.map +import sys +from scipy.interpolate import interp2d, RectBivariateSpline +import sunpy.data.sample + + +plt.style.use(astropy_mpl_style) + +# loading in the images as fits files + +im171 = glob.glob('171.fts') +im193 = glob.glob('193.fts') +im211 = glob.glob('211.fts') +imhmi = glob.glob('hmi.fts') + +#ensure that all images are present + +if im171 == [] or im193 == [] or im211 == [] or imhmi == []: + print("Not all required files present") + sys.exit() + +#Two functions that rescale the aia and hmi images from any original size to any final size + +#didn't normalize by exposure time for hmi because it was equal to 0 + +def rescale_aia(image: np.array, orig_res: int, desired_res: int): + hdu_number = 0 + hed = fits.getheader(image[0],hdu_number) + dat= fits.getdata(image[0], ext=0)/(hed["EXPTIME"]) + if desired_res > orig_res: + scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + elif desired_res < orig_res: + scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + + +def rescale_hmi(image: np.array, orig_res: int, desired_res: int): + hdu_number = 0 + hed = fits.getheader(image[0],hdu_number) + dat= fits.getdata(image[0], ext=0) + if desired_res > orig_res: + scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + elif desired_res < orig_res: + scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + +#defining data and headers which are used in later steps +hdu_number = 0 + +data = rescale_aia(im171, 1024, 4096) +datb = rescale_aia(im193, 1024, 4096) +datc = rescale_aia(im211, 1024, 4096) +datm = rescale_hmi(imhmi, 1024, 4096) + +heda=fits.getheader(im171[0],0) +hedb=fits.getheader(im193[0],0) +hedc=fits.getheader(im211[0],0) +hedm=fits.getheader(imhmi[0],0) + +#filter_all: rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 +#filter_b: ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively +#filter_hmi: rotates array if 'crota1' is greater than 90 degrees + +def filter_all(aiaa: np.array, aiab: np.array, aiac: np.array): + hdu_number = 0 + heda = fits.getheader(aiaa[0],hdu_number) + hedb = fits.getheader(aiab[0],hdu_number) + hedc = fits.getheader(aiac[0],hdu_number) + if heda['cdelt1'] > 1: + heda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0 + hedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0 + hedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0 + +def filter_b(aiab: np.array): + hdu_number = 0 + hedb = fits.getheader(aiab[0],hdu_number) + if hedb["ctype1"] != 'solar_x ': + hedb["ctype1"]='solar_x ' + hedb["ctype2"]='solar_y ' + +def filter_hmi(aiac: np.array): + hdu_number = 0 + hedm=fits.getheader(imhmi[0],hdu_number) + if hedm['crota1'] > 90: + datm=np.rot90(np.rot90(datm)) + +filter_all(im171, im193, im211) +filter_hmi(imhmi) +filter_b(im193) + + +#removes negative values from an array +def remove_neg(aiaa: np.array, aiab:np.array, aiac: np.array): + data[np.where(data <= 0)] = 0 + datb[np.where(datb <= 0)] = 0 + datc[np.where(datc <= 0)] = 0 + + +remove_neg(data, datb, datc) + +#defines shape of the array and the solar radius +def define_shape(aia: np.array): + hdu_number = 0 + return np.shape(aia) + +def define_radius(image: np.array): + hdu_number = 0 + hed = fits.getheader(image[0],hdu_number) + return hed['rsun'] + +#defining important variables +s = define_shape(data) +rs = define_radius(im171) +print(s) +print(rs) + +#converting pixel values to arcsec +dattoarc = fits.getheader(im171[0],hdu_number)['cdelt1'] +conver=(s[0]/2)*dattoarc/hedm['cdelt1']-(s[1]/2) +convermul = dattoarc/hedm['cdelt1'] + +#converts to the Heliographic Stonyhurst coordinate system + +def to_helio(image: np.array): + aia = sunpy.map.Map(image) + adj = 4096/aia.dimensions[0].value + x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj + hpc = aia.pixel_to_world(x, y) + return hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + +hpc = to_helio(im171) +csys=wcs.WCS(hedb) + +#setting up arrays to be used in later processing +#only difference between iarr and bmcool is integer vs. float? +ident = 1 +iarr = np.zeros((s[0],s[1]),dtype=np.byte) +bmcool=np.zeros((s[0],s[1]),dtype=np.float32) + +offarr,slate=np.array(iarr),np.array(iarr) +cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool) + +#define the locations of the magnetic cutoffs +def cutoff_loc(size: int): + r = (size[1]/2.0)-450 + xgrid,ygrid=np.meshgrid(np.arange(size[0]),np.arange(size[1])) + center=[int(size[1]/2.0),int(size[1]/2.0)] + return np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2) + +#create 2D gaussian array for mag cutoffs +def create_gauss(size: int): + y,x=np.mgrid[0:4096,0:4096] + return Gaussian2D(1,size[0]/2,size[1]/2,2000/2.3548,2000/2.3548)(x,y) + +w = cutoff_loc(s) +garr = create_gauss(s) +garr[w] = 1.0 + +#creates sub-arrays of props to isolate column of index 0 and column of index 1 +#what is props?? +props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','','' +props[:,1]='num','"','"','H°','"','"','"','"','"','"','"','"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx' + +#define threshold values in log s +def set_thresh(dat: np.array, b_val: float, u_val: float): + with np.errstate(divide = 'ignore'): + t = np.log10(dat) + t[np.where(t < b_val)] = b_val + t[np.where(t > u_val)] = u_val + return np.array(((t - b_val)/(u_val - b_val))*255,dtype=np.float32) + +t0 = set_thresh(datc, .8, 2.7) +t1 = set_thresh(datb, 1.4, 3.0) +t2 = set_thresh(data, 1.2, 3.9) + +#ignores division and invalid erros in the following conditions to create 3 segmented bitmasks +with np.errstate(divide = 'ignore',invalid='ignore'): + bmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1 + bmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1 + bmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1 + +print(bmcool) \ No newline at end of file From bf10f60858b988f46fdca8c8ff67f61bb016dc15 Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:52:26 -0400 Subject: [PATCH 03/10] work in progress --- CHIMERA_V1.ipynb | 552 ++++++++++++++++++++++++----------------------- CHIMERA_V2.py | 481 +++++++++++++++++++++++++++++++++-------- CHIMERA_V3.py | 511 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1182 insertions(+), 362 deletions(-) create mode 100644 CHIMERA_V3.py diff --git a/CHIMERA_V1.ipynb b/CHIMERA_V1.ipynb index d7d0759..084ac4d 100644 --- a/CHIMERA_V1.ipynb +++ b/CHIMERA_V1.ipynb @@ -1,24 +1,8 @@ { - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "provenance": [] - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "code", - "source": [ - "pip install sunpy" - ], + "execution_count": 30, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -26,11 +10,10 @@ "id": "aMdifgamAEpp", "outputId": "9894abba-dd40-4480-f47a-6b3296220a41" }, - "execution_count": 30, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "Requirement already satisfied: sunpy in /usr/local/lib/python3.10/dist-packages (5.1.1)\n", "Requirement already satisfied: astropy!=5.1.0,>=5.0.6 in /usr/local/lib/python3.10/dist-packages (from sunpy) (5.3.4)\n", @@ -51,15 +34,27 @@ "Requirement already satisfied: idna>=2.0 in /usr/local/lib/python3.10/dist-packages (from yarl<2.0,>=1.0->aiohttp->parfive[ftp]>=2.0.0->sunpy) (3.6)\n" ] } + ], + "source": [ + "pip install sunpy" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 1, "metadata": { "id": "sqDeNTSrJ7YA" }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/imogennagle/opt/miniconda3/envs/sunpy/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], "source": [ "#import required libraries\n", "import astropy\n", @@ -86,35 +81,40 @@ }, { "cell_type": "code", + "execution_count": 32, + "metadata": { + "id": "npCkEG_xLQEz" + }, + "outputs": [], "source": [ "#load in required fits file. Make sure to use full disk images\n", "im171 = glob.glob('171.fts')\n", "im193 = glob.glob('193.fts')\n", "im211 = glob.glob('211.fts')\n", "imhmi = glob.glob('hmi.fts')" - ], - "metadata": { - "id": "npCkEG_xLQEz" - }, - "execution_count": 32, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 33, + "metadata": { + "id": "6EwDwosRLS00" + }, + "outputs": [], "source": [ "#make sure all required files exist\n", "if im171 == [] or im193 == [] or im211 == [] or imhmi == []:\n", "\tprint(\"Not all required files present\")\n", "\tsys.exit()" - ], - "metadata": { - "id": "6EwDwosRLS00" - }, - "execution_count": 33, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 34, + "metadata": { + "id": "mp__D-fxLmTO" + }, + "outputs": [], "source": [ "#reads in fits files and scales images to a size of 4096. Ensures correct image resolution before processing.\n", "\n", @@ -134,15 +134,15 @@ "\n", "if len(data) != 4096:\n", " print(\"Incorrect image resolution\")\n" - ], - "metadata": { - "id": "mp__D-fxLmTO" - }, - "execution_count": 34, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 35, + "metadata": { + "id": "woeQladjLn_q" + }, + "outputs": [], "source": [ "#reads in fits files and scales images to a size of 4096. Ensures correct image resolution before processing.\n", "\n", @@ -155,40 +155,22 @@ "\n", "if len(datb) != 4096:\n", " print(\"Incorrect image resolution\")\n" - ], - "metadata": { - "id": "woeQladjLn_q" - }, - "execution_count": 35, - "outputs": [] + ] }, { "cell_type": "code", - "source": [ - "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", - "\n", - "hedc=fits.getheader(im211[0],hdu_number)\n", - "datc= fits.getdata(im211[0], ext=0)/(hedc[\"EXPTIME\"])\n", - "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datc)\n", - "datc=dn(np.arange(0,4096),np.arange(0,4096))\n", - "\n", - "if len(datc) != 4096:\n", - " print(\"Incorrect image resolution\")\n", - "\n", - "print(datc)\n" - ], + "execution_count": 36, "metadata": { - "id": "EAqYkrcXLp9I", "colab": { "base_uri": "https://localhost:8080/" }, + "id": "EAqYkrcXLp9I", "outputId": "e7813ba4-5457-4527-e5b1-8ad6ed3634f1" }, - "execution_count": 36, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "[[ 3.42794344e-19 -9.93202024e-18 -1.28940303e-17 ... 1.50616176e-18\n", " 1.50616176e-18 1.50616176e-18]\n", @@ -205,44 +187,43 @@ " 2.80098706e-01 2.80098706e-01]]\n" ] } - ] - }, - { - "cell_type": "code", + ], "source": [ - "'''Changes: to get rid of indexing error, copied scaling from data, datb, datc so that cell 189 runs correctly (before the data was out of the range of the image resolution which was 1024 instead of 4096)\n", - "Exposure time for hmi is zero, didn't scale by exposure time'''\n", + "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", "\n", - "hedm=fits.getheader(imhmi[0],hdu_number)\n", - "datm= fits.getdata(imhmi[0], ext=0)\n", - "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datm)\n", - "datm=dn(np.arange(0,4096),np.arange(0,4096))\n", + "hedc=fits.getheader(im211[0],hdu_number)\n", + "datc= fits.getdata(im211[0], ext=0)/(hedc[\"EXPTIME\"])\n", + "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datc)\n", + "datc=dn(np.arange(0,4096),np.arange(0,4096))\n", "\n", - "if len(datm) != 4096:\n", + "if len(datc) != 4096:\n", " print(\"Incorrect image resolution\")\n", "\n", - "print(datm)\n" - ], + "print(datc)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 63, "metadata": { - "id": "vifW2C7HLr2u", "colab": { "base_uri": "https://localhost:8080/" }, + "id": "vifW2C7HLr2u", "outputId": "4b998d95-4a45-441d-e9af-c754235949d3" }, - "execution_count": 63, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "WARNING: VerifyWarning: Invalid 'BLANK' keyword in header. The 'BLANK' keyword is only applicable to integer data, and will be ignored in this HDU. [astropy.io.fits.hdu.image]\n", "WARNING:astropy:VerifyWarning: Invalid 'BLANK' keyword in header. The 'BLANK' keyword is only applicable to integer data, and will be ignored in this HDU.\n" ] }, { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "[[-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", " -2.14748368e+08 -2.14748368e+08]\n", @@ -259,104 +240,105 @@ " -2.14748368e+08 -2.14748368e+08]]\n" ] } + ], + "source": [ + "'''Changes: to get rid of indexing error, copied scaling from data, datb, datc so that cell 189 runs correctly (before the data was out of the range of the image resolution which was 1024 instead of 4096)\n", + "Exposure time for hmi is zero, didn't scale by exposure time'''\n", + "\n", + "hedm=fits.getheader(imhmi[0],hdu_number)\n", + "datm= fits.getdata(imhmi[0], ext=0)\n", + "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datm)\n", + "datm=dn(np.arange(0,4096),np.arange(0,4096))\n", + "\n", + "if len(datm) != 4096:\n", + " print(\"Incorrect image resolution\")\n", + "\n", + "print(datm)\n" ] }, { "cell_type": "code", + "execution_count": 64, + "metadata": { + "id": "z4JBQBiULtrG" + }, + "outputs": [], "source": [ "#rotates array if 'crota1' is greawter than 90\n", "if hedm['crota1'] > 90:\n", "\tdatm=np.rot90(np.rot90(datm))" - ], - "metadata": { - "id": "z4JBQBiULtrG" - }, - "execution_count": 64, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 39, + "metadata": { + "id": "0ZPZxHgrLwfY" + }, + "outputs": [], "source": [ "#defines the shape (length) of the array as \"s\" and the solar radius as \"rs\"\n", "s=np.shape(data)\n", "rs=heda['rsun']" - ], - "metadata": { - "id": "0ZPZxHgrLwfY" - }, - "execution_count": 39, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 40, + "metadata": { + "id": "qS50C1BZLyMI" + }, + "outputs": [], "source": [ "#ensures \"cype1\" and \"ctype2\" are correctly defined as \"solar_x\" and \"solar_y\" respectively\n", "if hedb[\"ctype1\"] != 'solar_x ':\n", "\thedb[\"ctype1\"]='solar_x '\n", "\thedb[\"ctype2\"]='solar_y '" - ], - "metadata": { - "id": "qS50C1BZLyMI" - }, - "execution_count": 40, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 41, + "metadata": { + "id": "Kz4S85e4L1_S" + }, + "outputs": [], "source": [ "#rescales \"cdelt1\", \"cdelt2\", \"cpix1\", and \"cpix2\" if \"cdelt1\" > 1\n", "if heda['cdelt1'] > 1:\n", "\theda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0\n", "\thedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0\n", "\thedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0" - ], - "metadata": { - "id": "Kz4S85e4L1_S" - }, - "execution_count": 41, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 42, + "metadata": { + "id": "acXRp68LL33M" + }, + "outputs": [], "source": [ "#converts pixel values to arcseconds\n", "dattoarc=heda['cdelt1']\n", "conver=(s[0]/2)*dattoarc/hedm['cdelt1']-(s[1]/2)\n", "convermul=dattoarc/hedm['cdelt1']" - ], - "metadata": { - "id": "acXRp68LL33M" - }, - "execution_count": 42, - "outputs": [] + ] }, { "cell_type": "code", - "source": [ - "#Changes to Heliographic Stonyhurst coordinate system\n", - "\n", - "'''TO FIX: Warnings for illegal keyword names'''\n", - "\n", - "aia=sunpy.map.Map(im171)\n", - "adj=4096./aia.dimensions[0].value\n", - "x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj\n", - "hpc = aia.pixel_to_world(x, y)\n", - "hg=hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst)\n", - "\n", - "csys=wcs.WCS(hedb)\n" - ], + "execution_count": 43, "metadata": { - "id": "K1sknmYxL6hf", "colab": { "base_uri": "https://localhost:8080/" }, + "id": "K1sknmYxL6hf", "outputId": "8fdcf6a0-99cf-4bcc-a332-6b457fe964d9" }, - "execution_count": 43, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "WARNING: VerifyWarning: Verification reported errors: [astropy.io.fits.verify]\n", "WARNING:astropy:VerifyWarning: Verification reported errors:\n", @@ -388,10 +370,28 @@ "WARNING:astropy:FITSFixedWarning: 'datfix' made the change 'Invalid DATE-OBS format '31-Jan-2024 00:24:40.843''.\n" ] } + ], + "source": [ + "#Changes to Heliographic Stonyhurst coordinate system\n", + "\n", + "'''TO FIX: Warnings for illegal keyword names'''\n", + "\n", + "aia=sunpy.map.Map(im171)\n", + "adj=4096./aia.dimensions[0].value\n", + "x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj\n", + "hpc = aia.pixel_to_world(x, y)\n", + "hg=hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst)\n", + "\n", + "csys=wcs.WCS(hedb)\n" ] }, { "cell_type": "code", + "execution_count": 44, + "metadata": { + "id": "QUE4rIA7L-UZ" + }, + "outputs": [], "source": [ "#setting up arrays to be used in later processing\n", "ident=1\n", @@ -400,15 +400,15 @@ "bmcool=np.zeros((s[0],s[1]),dtype=np.float32)\n", "cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool)\n", "circ=np.zeros((s[0],s[1]),dtype=int)" - ], - "metadata": { - "id": "QUE4rIA7L-UZ" - }, - "execution_count": 44, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 45, + "metadata": { + "id": "elqZFWcnMBrZ" + }, + "outputs": [], "source": [ "#creation of a 2d gaussian for magnetic cut offs\n", "\n", @@ -419,43 +419,43 @@ "y,x=np.mgrid[0:4096,0:4096]\n", "garr=Gaussian2D(1,s[0]/2,s[1]/2,2000/2.3548,2000/2.3548)(x,y)\n", "garr[w]=1.0" - ], - "metadata": { - "id": "elqZFWcnMBrZ" - }, - "execution_count": 45, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 46, + "metadata": { + "id": "06jPD4pRMGdw" + }, + "outputs": [], "source": [ "#creates sub-arrays of props to isolate column of index 0 and column of index 1\n", "props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','',''\n", "props[:,1]='num','\"','\"','H°','\"','\"','\"','\"','\"','\"','\"','\"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx'" - ], - "metadata": { - "id": "06jPD4pRMGdw" - }, - "execution_count": 46, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 47, + "metadata": { + "id": "XpbOolCwMVMF" + }, + "outputs": [], "source": [ "#removes negative data values\n", "data[np.where(data <= 0)]=0\n", "datb[np.where(datb <= 0)]=0\n", "datc[np.where(datc <= 0)]=0" - ], - "metadata": { - "id": "XpbOolCwMVMF" - }, - "execution_count": 47, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 48, + "metadata": { + "id": "L3c366LOMQcW" + }, + "outputs": [], "source": [ "#ignores division errors in the following logarithms and sets conditions for t0, t1, and t2\n", "with np.errstate(divide = 'ignore'):\n", @@ -468,60 +468,52 @@ "t1[np.where(t1 > 3.0)] = 3.0\n", "t2[np.where(t2 < 1.2)] = 1.2\n", "t2[np.where(t2 > 3.9)] = 3.9" - ], - "metadata": { - "id": "L3c366LOMQcW" - }, - "execution_count": 48, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 49, + "metadata": { + "id": "3oO_rrbgMZ4x" + }, + "outputs": [], "source": [ "#makes a multi-wavelength image for contours\n", "t0=np.array(((t0-0.8)/(2.7-0.8))*255,dtype=np.float32)\n", "t1=np.array(((t1-1.4)/(3.0-1.4))*255,dtype=np.float32)\n", "t2=np.array(((t2-1.2)/(3.9-1.2))*255,dtype=np.float32)" - ], - "metadata": { - "id": "3oO_rrbgMZ4x" - }, - "execution_count": 49, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 50, + "metadata": { + "id": "Q9jr_gA-Mc6J" + }, + "outputs": [], "source": [ "#ignores division and invalid erros in the following conditions to create 3 segmented bitmasks\n", "with np.errstate(divide = 'ignore',invalid='ignore'):\n", "\tbmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1\n", "\tbmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1\n", "\tbmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1" - ], - "metadata": { - "id": "Q9jr_gA-Mc6J" - }, - "execution_count": 50, - "outputs": [] + ] }, { "cell_type": "code", - "source": [ - "#conjunction of 3 segmentations\n", - "cand=bmcool*bmmix*bmhot" - ], + "execution_count": 51, "metadata": { "id": "U_avvniSMe7a" }, - "execution_count": 51, - "outputs": [] + "outputs": [], + "source": [ + "#conjunction of 3 segmentations\n", + "cand=bmcool*bmmix*bmhot" + ] }, { "cell_type": "code", - "source": [ - "#plot tricolour image with lon/lat contours\n", - "'''TO FIX: there is no code written for this section'''\n" - ], + "execution_count": 52, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -530,40 +522,48 @@ "id": "-cady694uJY9", "outputId": "bbd1fe49-75f1-4794-d970-415127af8f2b" }, - "execution_count": 52, "outputs": [ { - "output_type": "execute_result", "data": { - "text/plain": [ - "'TO FIX: there is no code written for this section'" - ], "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" - } + }, + "text/plain": [ + "'TO FIX: there is no code written for this section'" + ] }, + "execution_count": 52, "metadata": {}, - "execution_count": 52 + "output_type": "execute_result" } + ], + "source": [ + "#plot tricolour image with lon/lat contours\n", + "'''TO FIX: there is no code written for this section'''\n" ] }, { "cell_type": "code", + "execution_count": 53, + "metadata": { + "id": "rpZ36REKMggU" + }, + "outputs": [], "source": [ "#removes off detector mis-identifications\n", "r=(s[1]/2.0)-100\n", "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2)\n", "circ[w]=1.0\n", "cand=cand*circ" - ], - "metadata": { - "id": "rpZ36REKMggU" - }, - "execution_count": 53, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 54, + "metadata": { + "id": "q8AzT5xXMgh-" + }, + "outputs": [], "source": [ "#seperates on-disk and off-limb coronal holes\n", "circ[:]=0\n", @@ -574,19 +574,11 @@ "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 >= r**2)\n", "circ[w]=1.0\n", "cand=cand*circ" - ], - "metadata": { - "id": "q8AzT5xXMgh-" - }, - "execution_count": 54, - "outputs": [] + ] }, { "cell_type": "code", - "source": [ - "#open file for property storage\n", - "'''TO FIX: No code for this section'''" - ], + "execution_count": 55, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -595,38 +587,46 @@ "id": "iVgAh6Y-ut80", "outputId": "df72cbe1-0887-443c-f826-e7cd914436e0" }, - "execution_count": 55, "outputs": [ { - "output_type": "execute_result", "data": { - "text/plain": [ - "'TO FIX: No code for this section'" - ], "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" - } + }, + "text/plain": [ + "'TO FIX: No code for this section'" + ] }, + "execution_count": 55, "metadata": {}, - "execution_count": 55 + "output_type": "execute_result" } + ], + "source": [ + "#open file for property storage\n", + "'''TO FIX: No code for this section'''" ] }, { "cell_type": "code", + "execution_count": 57, + "metadata": { + "id": "G911hr4d01R0" + }, + "outputs": [], "source": [ "#contours the identified datapoints\n", "cand=np.array(cand,dtype=np.uint8)\n", "cont,heir=cv2.findContours(cand,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)" - ], - "metadata": { - "id": "G911hr4d01R0" - }, - "execution_count": 57, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 58, + "metadata": { + "id": "lQI5QT8mMnQ9" + }, + "outputs": [], "source": [ "#sorts contours by size\n", "sizes=[]\n", @@ -637,15 +637,57 @@ "for i in range(len(cont)):\n", "\ttmp[i]=cont[reord[i]]\n", "cont=list(tmp)" - ], - "metadata": { - "id": "lQI5QT8mMnQ9" - }, - "execution_count": 58, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": 62, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "OGhJ_fJT6j3t", + "outputId": "c00d4c6e-bdbb-4ca3-e426-ad0c36a70112" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(array([ 582, 582, 582, ..., 3207, 3208, 3208]), array([1674, 1675, 1676, ..., 3069, 3067, 3068]))\n", + "(array([2606, 2606, 2606, ..., 2687, 2688, 2688]), array([2408, 2409, 2410, ..., 2347, 2345, 2346]))\n", + "(array([1611, 1611, 1611, ..., 1826, 1826, 1826]), array([2278, 2279, 2280, ..., 2332, 2333, 2334]))\n", + "(array([1268, 1268, 1268, ..., 1551, 1551, 1551]), array([2994, 2995, 2996, ..., 3285, 3286, 3287]))\n", + "(array([ 621, 621, 621, ..., 1790, 1791, 1791]), array([1463, 1464, 1465, ..., 1153, 1151, 1152]))\n", + "(array([582, 582, 582, ..., 934, 935, 935]), array([1674, 1675, 1676, ..., 1729, 1727, 1728]))\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":180: RuntimeWarning: divide by zero encountered in log10\n", + " data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9))\n", + ":181: RuntimeWarning: divide by zero encountered in log10\n", + " data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0))\n", + ":182: RuntimeWarning: divide by zero encountered in log10\n", + " data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7))\n", + ":210: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored\n", + " plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2)\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAMWCAYAAABsvhCnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACvpUlEQVR4nOzdZ2CT5cLG8etJ0klL2RvZQ1DAY9kgIiqKgiIOhr5OXIB7Cx4VFHEdFXAheHAPEMU92HvJlL1lr1JaupO8H3rySJmduTP+vy+2aZpcraXN9dzL8nq9XgEAAABAEThMBwAAAAAQ/CgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIrMZToAAKDodu7cKbfbLUnavXu3nnrqKWVlZeW5z/79+7Vt2zZ5vd5CPYfD4VD9+vWVkJCQ5/b4+Hi99tprKlWqlCQpOjpalSpVKtRzAACCl+Ut7F8YAECx83g82rp1q5KSkvTYY4/pyJEjWr16tdxut7Kzs+XxeExHLBKn0ymXy6WIiAidc845Kl++vF599VXFxsbqrLPOMh0PAFAEFAsA8BOv16tNmzZp9OjR+u2337R582ZlZ2fbIw1FYVmWXK5/BqETEhJUrVq1E+536623qm3btoV6jokTJ+rXX3894fZt27YpLS3Nfj8nJ6fQoyLHcjqdioyMVOPGjXXttdeqb9++ql27dpEfFwBQMigWAFCMUlJSNGvWLP373//W6tWrlZ6eXuAX2Q6HQy6XS/Hx8apZs6ZatGihgQMHyuFwqHnz5nI4gnN5XGZmplatWiVJGjp0qLZt26bNmzcrIyND2dnZhfo+lSpVSi1atNCIESP0r3/9S1FRUSURHQCQDxQLACiErVu36oknntCMGTO0f//+fI86OBwORUVFqW7durr66qt1ww03qEmTJnI6nSWcOLhkZGRow4YNGjlypGbMmKFt27YpKysr3+XD6XSqRo0a6tatmwYPHnzS0RsAQPGiWADAaRw5ckQff/yxhg0blu8C4XK5VKlSJXXu3FnPPvus6tev74ek4cXj8Wjt2rV6/PHHNX/+fB0+fFg5OTln/Dyn06maNWvqlVde0ZVXXqno6Gg/pAWA8ECxAID/OXLkiPr376/ffvtNycnJp706blmWYmNj1axZMw0bNkwXXHBBnjUOMCc9PV3ff/+9hg8frvXr1+dZ/3EylmWpfPnyuvbaa/XGG28wnQoAColiASAs5eTk6KmnntKYMWPOWCIiIiJUt25djR49WhdccIEiIiL8mBTFJS0tTd99950ef/xx7d69+7QjHJZlqUKFCnrqqad0//33y7IsPyYFgOBEsQAQFnbs2KErr7xSq1atOu10pujoaHXo0EHvv/++6tSp48eEMGXRokUaMGCAli9ffsLZH8eKiIhQp06d9NVXX6ls2bJ+TAgAwYFiASAkzZ07V71799aOHTtOORrhcrmUmJiozz//nG1MkceiRYt02223ac2aNacsog6HQw0bNtTkyZPVoEEDPycEgMATnHsWAsBx5s6dq7POOksOh0OWZal9+/b6+++/85SKhIQEvfPOO/buQtnZ2Zo3bx6lAido2bKlVq5caZ/JkZ6eriFDhigmJsa+j28BecOGDWVZlhwOh84++2xt2LDBYHIAMIcRCwBBadu2bbr66qu1YsWKU55GXa1aNX399ddq166dn9MhHHz22We67777dPDgwZN+3Ol0qnPnzvriiy9Uvnx5P6cDAP9jxAJA0HjmmWcUHR0ty7JUu3ZtLVu2LE+pqFatmmbNmiWv1yuv16udO3dSKlBi+vbtqwMHDsjr9crj8eizzz5TuXLl7I+73W798ccfqlChgizLUnx8vD744AODiQGgZDFiASBgbd26VZdddpnWr19/0nUSpUuX1ssvv6z+/fsH7WnUCE1ut1vPPPOM3njjjZNud2tZllq1aqUff/yR0QwAIYNiASCgfP3117rrrruUlJR0wsccDocuueQSTZ48WZGRkQbSAYWTlJSkrl27avHixSctyZUrV9Z3332n1q1bG0gHAMWDS3wAjBsyZIgiIiJkWZauv/76PKUiISFB06ZNk9frldvt1i+//EKpQNApW7asFi5cKI/HI4/Ho3feeUexsbH2x/fu3as2bdrIsizFxMQwZQpAUGLEAoARQ4YM0fDhw0+6lWeTJk00depUVa5c2UAywL9Wrlypbt26aceOHSd8LDIyUqNGjVL//v0NJAOAgmHEAoDfDB48WC6XS5ZladiwYXapsCxLffr0kdvtltfr1V9//UWpQNg499xz7a2Rs7OzdcEFF9gfy8rK0p133inLshQVFaX333/fYFIAOD1GLACUqNdff12PPfbYCSMTvjLxySefyLIsQ+mAwOV2u3XRRRdp5syZJ3wsMjJSn332mXr16mUgGQCcHMUCQLGbPn26unXrpvT09Dy3UyaAwnG73ercubNmzZp1wscSEhK0cuVK1axZ00AyAPgHxQJAsTh69KiaNGmi7du3n/Cxnj17auLEiZQJoBi43W61atVKf/755wkfa9GihRYvXiyn02kgGYBwxxoLAEXSp08fORwOxcXF5SkVjRo1UlZWlrxer7755htKBVBMnE6nlixZIq/Xq+TkZFWpUsX+2LJly+RyueR0OjV48GCDKQGEI0YsABTYrFmz1LVr1xOmOsXFxWnx4sVq1KiRoWRA+Pruu+90ww03KDMzM8/tCQkJWrt2bZ4CAgAlgRELAPni8XjUqVMnWZalCy64wC4VDodDw4cPl9frVUpKCqUCMOSqq65SRkaGvF6vbrvtNvv25ORkVa1aVZZl5bkdAIobIxYATmv9+vU677zzlJaWluf2Ro0aaeXKlYqIiDCUDMCZJCUlqUmTJtqzZ0+e2ytWrKj169erTJkyZoIBCEmMWAA4qT59+siyLDVq1MguFU6nU2PHjpXX69XatWspFUCAK1u2rHbv3i2v16shQ4bYa53279+vsmXLyuFw6OmnnzacEkCoYMQCgC0tLU2NGjU64QTgatWqacuWLYqMjDSUDEBx2bFjh5o2baojR47kub158+ZasmQJO0oBKDRGLABo7ty5ioiIUKlSpexSYVmWXn75ZXm9Xu3cuZNSAYSIGjVqKDk5WV6vV7fccot9+/Lly+VyuRQbG6sDBw6YCwggaFEsgDD28ssvy7IstW/fXjk5OZKk2NhYLV++XB6PR48++qjhhABK0ocffiiv16tJkybZUxvT09NVsWJFORwOff/994YTAggmTIUCwlD37t31ww8/5LmtadOmWrVqlaFEAAKB2+1WtWrVtG/fvjy3P/roo3r55ZcNpQIQLCgWQJjweDxq2LChNm3alOf2G264QV988YWhVAACVdu2bTV//vw8t3Xs2FEzZ840lAhAoGMqFBDiUlNTVbp0aTmdTrtUOBwOvffee/J6vZQKACc1b948eb1eDRgwwL5t1qxZsixL1atXl9vtNpgOQCBixAIIUampqapatapSU1Pt21wul5YsWaJmzZoZTAYgGE2aNEm9evXSsS8bqlatqu3bt8vlchlMBiBQUCyAEJOSkqJq1arlKRSlS5fW3r17FR0dbTAZgFCwdetWNWrUSFlZWfZt1apV07Zt2ygYQJijWAAhgkIBwJ+2bNmis88+W5mZmfZtFAwgvFEsgCCXnJysGjVqUCgAGMEIBgAfigUQpE5WKBISErRnzx4KBQC/27Ztmxo2bJinYLAGAwgvFAsgyBw9elRVqlShUAAISFu3blXjxo1PmCK1fft2OZ1Og8kAlDS2mwWChNfrVb169RQXF2eXioSEBB09elSHDx+mVAAICLVr11ZGRoa2bt2qyMhISdKuXbvkcrl04YUXmg0HoERRLIAg0KVLFzkcDm3evFmSFBMTYxeK2NhYw+kA4ES1atVSZmamtm7dao9UzJgxQ5ZlaeDAgYbTASgJFAsggN1///2yLEtTp06VlHsOxV9//aW0tDQKBYCgUKtWLeXk5Oibb76RZVmSpNGjR8uyLI0ZM8ZwOgDFiTUWQAD69ttv1bNnzzy3vf/+++rfv7+hRABQPB588EG98cYb9vsOh0Nr165V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U+3bt1e9evX0+OOPq3///pKkc889V6tWrZLEDAL4H5PgUWgrVqywS8VFF11EqQAAhJU///xTGzdu1Lp16+T1evXmm28W+TE7dOigm266SQ6HQ4mJiae8X1RUlGrUqKHNmzfrzjvv1FdffSWPx6OVK1eqSpUqkqTGjRtzOjf8ihELFMqxB+BVqlRJe/fuNZwIAAD/evjhh+0D7qKjozV79ux8f+7atWt11113qVq1avr888/t25OTk7Vv3748i7ZPZ9++ferWrZv9vm/tRVxcnLKysuRwOOR2u/OdCygKRixQKL4TRp1OJ6UCABCWXnvtNXm9Xg0ZMqTAB9TdeOONSk9P14YNG5SYmKhWrVrpzTffVJcuXdSnTx/16dOnQI933nnnacGCBZKkmJgYezqUx+NRfHx8gR4LKCyKBQqsXLly9mIx35kVAACEq48//liWZRX489xutzIyMuytZT/++GPVqlVLzz//vDZs2KCjR4+e8TEqVaqkxYsXa8yYMXI6nZo0aZLcbrcaNmyoIUOGSJJSU1MLdL4GUFgUCxRI27Zt7ZM+d+zYwVkVAICwNmDAAG3dulXVqlXL9+csWbLEfjsqKkqDBg1SRkaGvF6vtm7daheCa665psB5atSoocWLFysxMVFDhw7VzTffLCl3XeS9995b4McDCoJXhci3oUOH2mdVjB07VtWrVzecCAAAs8aMGSMpd6vX/Nq/f78kadu2bae8z5dffqmDBw/af3cL6t1331Xz5s01fvx4u6C88847+u233wr1eEB+sHgb+bJ8+XK1aNFCktS9e3dNnjzZbCAAAALAJ598optuukm33XbbKUcE9uzZY+/U5NOqVSt5vV7l5OSccvT/rLPO0t9//605c+YoKiqqUPk6d+6slJQU1atXT5s2bZIkpaSkKC4urlCPB5wOxQJndOwOUDVq1NDff/9tOBEAAIHBt7ZixowZKlWqVJ6P9e/fX0uXLpUk1a1bV1999ZUkqV27dqpataq2bdumK664Qj/88MMpH9/hcMjr9Wr06NFq3bp1gfNNmDBBL730khwOhyIjI5WRkXHCwXpAcaFY4IxcLpfcbrciIiJYrA0AwDFq1aql7du3n/Qgu9atW8uyLFmWpezs7JN+/siRIzVw4MDTPofL5VKpUqU0derUfOfKzs5Wnz59tHXrVvu2mJgYpaenS5LKlCljr5kEigtrLHBatWvXtve/LuhWegAAhDrfi3NfcdiwYYM95eiuu+5STk6OlixZou7du+vHH3/U4sWLlZGRoVmzZikjI+OMpULKLRY5OTl5bhswYIDGjx9/ys/p0qWLtm7dqvr169u3paen61//+pck6fDhw+rRo0fBvljgDCgWOKVBgwbZC8tWrVrFDlAAABzn8OHDcjgcateundasWaM+ffrohhtukCQdPHhQknTuuedq8uTJ6tatm84//3xFRUWpQ4cO+V434fF4lJaWpvvvv1+S9Pzzz2vBggUaOXKkVqxYcdLPadeunSSdsFvVn3/+qeuvv16S9P333+vHH38s+BcNnAJToXBSS5YsUWJioqTcgvHWW28ZTgQAQGDyeDxyOp2S/hld6NmzpyZNmqQ6depo8+bNRX4Op9Op0qVL648//tCyZct0xx132B+75557dPvtt5/wOf369dO6devkcrk0b948bd++Xffcc4/27dunxMREe/oWi7lRXCgWOMGxi7Xr1aunjRs3Gk4EAEBg27Jli9asWaMrrrjCvq24SoXb7ZbL5dJ//vMfdezYUZK0a9cu9ejRQ7GxsUpLS1Pv3r31yCOP5OvxfDtSxcbG6ujRoyzmRrFhbgtOEBMTI0mKiIigVAAAkA916tRR6dKlJUmRkZHyer3FUip8jy3JLhVS7hSn1q1bKy0tTZL0xRdfaO3atad8jJtvvlmtWrWSJFWpUkVer9e+iOj1elW2bNliyYrwRrFAHi1atGCxNgAAhdChQwd5vV5lZmYW6+OeanLJ6NGjNWjQIPv9J554wn77iy++UGJiohITE7Vq1SqtWbNGHo9HKSkpGjBggGJiYpSUlGSXlcOHD+uuu+4q1twIPxQL2IYNG6bly5dLkn744QcWawMAEAC++eabPP/12bBhg0aPHm2/f9VVV9lvv/nmm/bbt9xyiz3VqXv37nr66aeVnp6uzp07a9asWerXr58k6f3339f06dNL6stAGOCVIyRJR48e1ZAhQyTl/mI6do4oAAAwp2/fvpJ0wkhInz59FBkZqYsvvliSNG7cOPtjQ4cOtXeduvTSS7Vv3z5dd911ec6jqlmzphwOhz7//HPVq1dPUu5J3UBhsXgbkv452TMuLk4pKSmm4wAAAOWe2L1lyxZ16dJFI0aM0O+//67p06fr119/lSRNnDhR11xzjb78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lK/D75555hlJ0hNPPFGimYBQ4dskITExUVu3bi3x5xs8eLA8Ho/atWtX4s8FwCzf2TTDhw83nCT0sMYiHziZESeze/duVatWTZK0YMEC+6T2U0lMTFS3bt30448/+iMeENQOHjyoZs2aac+ePbIsSwsWLCix5+rRo4d27dqlK664Qj/++KMcDscZD8EEEJyys7MVGRkpidd1JYERiwJg1ALHqlq1qj1tok2bNrrjjjtOe/9zzjlHP/30Ey9YgHwoX768du7cqe3bt8vtduuqq64qsefatWuXJNmlv0yZMiX2XADMGjt2rCSd8WIgCodikQ++xbfXXXed4SQINC6XSy1btlSZMmW0bNmy0973ww8/lGVZ9pUSAGdWvXp19ejRQzt37lSrVq30zjvvFPtzvPXWWxo5cqR9GN/BgweL/TkABAbflORzzjnHcJLQRLHIB98P4dy5cw0nQSBauHChoqOjJeWOXJyKZVmaO3euPB6POnTo4K94QND77rvvFBkZKY/HY19tLE7t2rXThg0bdOjQIQ0aNKjYHx9A4EhOTpYk/fDDD4aThCbWWORDVlaWvZc63y6czK233qrx48fL6/Wqd+/eeuSRR05532uvvVbbtm1jhyiggG6//XaNGzdOCQkJmjJlSpEfb8+ePerZs6c9pTEiIkJpaWn52pABQHBi3WzJYsQiH46dupKfrUURfj788EN5PB5FRkbq+++/P+19L774Ynm9XnvXGwD5M3bsWJUrV65Yzpl46qmndOWVV8rtdmv48OHyeDzKysqiVAAhbObMmZLEgccliGKRT75ycc899xhOgkDmdrvP+Avr7rvvltPp1NNPP+2nVEDouO++++T1ejVy5MhCP8aoUaP022+/aeDAgXK73XriiSd4oQGEgT59+kiSatasaThJ6KJY5NPVV18tSfr000/NBkFAq1q1qlJTU7Vz587T3q969eo6cOCAlixZ4qdkQGj497//re7du2v8+PGFLheffPKJ4uLiilROAAQf3w5wn3/+ueEkoYs1FvnEOgvkl8PhUNmyZfXbb7+d9n4tW7ZUXFxcsUzrAMJNjx499P333+v5559Xt27dCvS5rVq1UuXKle0XGQDCA+srSh4jFvl07DqLjIwMg0kQ6KpXr65Dhw6d8X4NGjRQSkqKHxIBoWfy5MmqWbOmnnnmGe3YsaNAn+vxeBQR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p1Nutzvs1sXcfvvtWr58OVd8AKCYVKtWTbt37y7Q35PExETFxcXpgw8+4IVyPnB2RWCiBpeQ+Ph4zrQwaMSIEfZVnmnTpuUpFT7PPvusduzYoZdeekkffvihfWjRzTff7Nespo0dO1Z33nmnvF4vxQIATqFFixbq0KGDqlSpYp+P5PPJJ5/YZyRdccUV2r17t66//vp8P/bcuXMl5U7TpVTkD2dXBCaKRQm66667JElvvPGG2SBh5sCBA3ryySflcrm0ePFiRUZG2h/bt2+f2rVrJ0nq2LGjqlevrscff1yWZal9+/a69dZb9ddff5mKboyvhDmdTv3000+G0wBA4Fm+fLnmzJmjvXv36vfff1fFihUVExMjy7J000032Wcm/fTTT2rWrJkee+yxfD+272yKTz/9tESyh6J169ZJkn777TfDSXAspkKVMN+oxd69e1WpUiXDacKDb7vf2bNn51mM7fV61bZtW+Xk5Khv3772L/CuXbvm+cVUunRpTZ061e+5A8Ell1yipKQkde7cOWy/BwBwMsfuyuTbRvZYcXFxmj59eqEe++DBg+rataveeOMN3X///UWJGRbGjRun22+/XRLToAINIxYlLCEhQZLsw1tQsvbu3ausrCzdf//9dql49NFHlZiYqJYtWyonJ0evvvpqnqtCM2bMsK/YJyYmhvUL6t9//13du3fXtGnTVLp0aWVnZ5uOBAABYeTIkfbbl1xyiV599VX7/cWLFxeqVIwfP16dOnVS165dFRUVRanIpzvvvFOSdMEFFxhOguMxYlHC/v77b5111lmSaNX+MHLkSN13332KjIxUTk6OvWbguuuuU69evU46d3Xq1Knq0qWLZs6cqdjYWH9HDkjffPONXnzxRUVHRys9Pd10HAAwzuPxKC4uTunp6Zo/f76cTqfuvvtuDR48WDVr1izw4/3444/697//LYfDoY8++kj9+vUrgdSh59hF2x6Ph/M9AgwjFiWsZs2a9g993759DacJfYMGDdK5554rKXdXjiFDhsjr9eqrr7465YK4iy66SFLu/Fnkuuaaa/TAAw8oIyPDdBQA8JvExERZlqWLL744z+07duyQ0+lUenq6xowZI5fLJcuy9N577xWqVEiyt0J3u92UigKoU6eOJNnrWxBYGLHwgxdeeEGDBw+WxKhFIMrJyVFERITGjx+vpk2bmo4TMLxer1q2bKmoqCgtXLjQ3oEDAEJVzZo17RLhcrn0448/qkuXLnI6nfJ6vZo7d669SLsoli1bpgEDBigzM1MbNmwo8CF64cxXJvi+BSZGLPzg6aeftt/etWuXwSQ4ma+++kqSKBXHsSxLL730krKzs9W8eXN17tzZdCQAKFF///23pNxRhMzMTI0aNUrr1q2Tx+MptlIhSRMnTlRmZqYkqUGDBsXymOFgxIgR9tuUisBEsfAT31BpvXr1DCfB8XxTpI4tgMh18cUXa+HChSpbtqxmzJhhOg4AlKi33npLUu5UWim3YLRp00aSiq1USNLQoUPtx33kkUeK7XFD3ZNPPilJ6t27t+EkOBWKhZ9s3bpVkpSRkaGcnByzYWB77rnnFBUVpTJlyujXX381HSdguVwueb1etW/f3nQUACgx9913nyTpiiuukCR9//33Onz4sIYMGVLszzVq1ChJ0ubNm4v9sUPRihUr7Onkn3/+ueE0OBWKhZ84HA77oLbzzjvPcBpIuSecPvvss3K73Tp8+DCnd55G6dKlJeWeDrtw4ULDaQCg+EyfPl1OpzPPi9VbbrlFixcv1vz58zVr1ixdddVVJfb8FIv8adu2rSSpRo0ahpPgdCgWfjRp0iRJ0qpVqwwnCW933323LMtSpUqVZFmWunfvrq5du2rWrFmmowWsL7/8Uj/88IMkqXXr1qpbty5nXAAICdu3b5fH41Hfvn3thcG+C4Eul6tELzo1aNBAy5Yt4/fpGXi9XqWlpUn658RtBCaKhR9169bNfvvBBx80mCS8ffDBB4qOjlabNm3sfcRfeOEF07ECXpUqVewrelu2bFFkZKQsy9KHH35oOBkAFN6GDRvst6tUqaJZs2b5bRtT3wFvTJE+Pd828i6Xi/OmAhzFws98i7TeeOMNs0HCVNmyZeV2u9WxY0eNGjVKlSpVMh0pqDRo0EBTp061TyqXpJ07dxpMBACF07lzZ3322WcaPny4IiMjdd555+k///mP36bF3n777Ro7dqzKly/PVNwz+OuvvyRJs2fPNpwEZ8I5Fgb4roR8/fXXuvbaaw2nCS/x8fFKTU213x8xYoS6dOliMFFweu6557R8+XJt375dK1assK8mAUAwiImJUUZGhlwul3JyclShQgX98ssvfs2QmJioChUqaP/+/X593mDTp08fffHFF5I4CywYMGJhQKtWrSTl/mOBf6WkpMjr9crr9apGjRp6/PHH7e0FfX755RetWLHCUMLg8O9//1vvvPOOJKlZs2ZyOBzyeDyGUwHA6X300UeyLEsZGRmS/pmCdODAAb+8aJ0zZ45atmypxMRESdKNN95Y4s8Z7Hyl4tgzLBC4GLEwwOPxyOl0Sso9MK9q1aqGE4Wv9u3ba+7cuVq8eLH+85//6NNPP7U/dsUVV+i5554zmC6wpaamqnPnzipXrpwOHjwoSXI6nTpy5AhzYAEEjP379+vaa6/VnDlz5Ha7FRERoezsbFWoUEEffPCBrr76aklSbGysZs6cWWI5nn/+eU2ePFmxsbHavHmzKleuXGLPFSref/993XXXXZIYrQgWFAtDKleurH379ik+Pl5HjhwxHSdspaamKj4+3n6/UaNGWrt2rSIiImRZlubNm2cwXfDIzs5Wt27dlJSUJCl3e+XmzZurf//+uvPOO+V0OrVr1y7169dP06ZNM5wWQDipWLGiDhw4IMuyNGvWLEVHR+f5eMuWLeX1ejVo0CDdfPPNxf78s2fP1gMPPCBJuummm/TRRx8V+3OEKqfTKY/Ho+7du2vy5Mmm4yAfKBaGHDlyRAkJCZJyX5S5XC7DicLXK6+8ounTp+v999/X3LlzNXz4cC1dulRvvvkmB8IV0KeffqopU6Zo5cqV9tWlhIQEHT58WLVq1dL27dt1/fXX68svv/R7tjvvvFNLly5VvXr17KF1AKHP4XCobNmy+u2330768W3btmnr1q3q1KlTsT/3119/rREjRiguLk579+5lNLcAVqxYoebNm0titCKYUCwMio6OVmZmpipVqqS9e/eajhP2mjVrppUrV0rKHVH68ccfDScKbm63W998841GjBihyMhIZWVlybIseb1ebdy4UW3atFFGRoYSEhLsfeRLomBnZ2dr//79ql69un3b9u3bVbNmzWJ/LgCBx7IsDR482J7y5C/t27dXZmamXC6X0tLSFBER4dfnD3YRERHKyclRvXr1tHHjRtNxkE8s3jZo27ZtkqR9+/YZTgJJatOmjf12dna2MjIylJKSoldeeYXDiwrB6XTquuuu0x133KGsrCzVqVPHvupUv359HTx4UDk5Odq5c6ecTqciIiIUFRWlunXrqmHDhurVq5fmzJljjy4MGjRIvXr10nXXXadDhw5p2LBhGj58uIYPH37Ccw8YMMDewjEyMtIuFXfddZecTqfOOuss/30jABi3Zs0avzxP+/btlZiYqMTERGVmZmrPnj3Kzs6mVBRQRkaGvbB+/fr1htOgIBixMMw3f7B169aaP3++6TiQ9NRTT530xWqFChX07bffnjA/93TeeustLVu2TOPGjSvOiEGrY8eOSk9PV7Vq1ez5sj/99JNGjx6te+65R88//3yhdpdyOBzq27evPv74Y0n/bOnscDj0wQcfqFmzZvZ9L730Uh06dEiWZSkmJkbp6emaNWuW3n33XY0ZM0aSCvT/GEBgc7lcio6O1owZM0r0eQ4ePKiuXbuqR48eSk9P12effaYKFSqU6HOGqoSEBB05coR1qEGIYmHYDz/8oO7du0tiDmGgSU1NVWZmpsqWLatq1arZ09Wio6M1depURUZGnvJzN2/erN69e9svkhcvXuyXzMGgIFfvcnJy5HQ6lZ6erujoaPtgvuzsbDmdTjkcDq1cuVIDBgxQWlpans891dSHffv2acSIEVqzZo32799/0n93w4YN0+rVq/Xxxx/nOQwQQPBp06aNFixYUOK/h1euXKlbb72Vv+VFlJGRYR8YmJqaqlKlShlOhIKgWAQAh8Mhr9fLqEUQ2Lp1q+rUqWO/37dvXz300EP6448/dNFFF+ntt99WvXr1NGTIEEVERGjevHlKTEykWPiBx+PRmDFjlJKSogcffNDe0vl0Jk2apBdeeEGVKlWypyQ6nU653W5JUs2aNbV9+3ZlZGTI4/EoNjZWS5Ys0ddff61XX31VHo/HfhFRr149bdq0SZGRkdq/f79Kly5dcl8sgHx7+OGH9frrr5f472HfmgpeVhWNb7QiLi5OKSkppuOggCgWAYBRi+CTmpqqBg0aaM+ePSf9uGVZysnJkcPhsKfl3HHHHVqzZo0qVKigIUOG+DMuCsB3cJUk3X777XrzzTcVFxd3wv18FwQuuugitWzZUi+99NIJ9+nUqZOuvvpqdejQIc/jAvAft9stl8ulefPmldhaB980zwMHDqh8+fIl8hzhID093d45KyUl5aS/exHYKBYBglGL4OR2u3X06FHFx8fn+YV4LK/Xq5o1a2rnzp32bV9//XWekQ8EjkceeUTTp0+XJHsNhk9CQoKSk5M1a9Yse6jex1ccHA6Hfv/9d11//fX2wYGWZSklJYUhfcAA36G048ePV9OmTYv1sVNTU3XRRRfJ4/Fo7ty5atu2bbE+frgpU6aMkpOTGa0IYkweDhC+hawLFiwwnAQF4XQ6Vbp0aVmWdcr9yS3L0o4dO5STk6PMzExJuaNUH3zwQZ77paamnnAb/O/VV1/V4sWL9cUXXygzM1OxsbHq37+/Pv74Y02ZMkWLFy8+oVRIUrly5STlFouEhAT9+uuvWrx4sX3RIC4uTm+88YZ9/xkzZsjhcGjOnDn++tKAsORwOOR0Ou0TnIvTW2+9JY/Ho86dO1MqiigtLU3JycmSdMrZAAh8jFgEEEYtwkO5cuXsE6qPHbnwzc8tXbq0pk6dajIiCuntt99Wt27dVLt27Ty3ezwedejQQTk5Ofb6jeHDh+upp56SlLtYMSoqyt9xgbDhcrkUERGh2bNnF+vjbty4Ub179+ag22LAaEVoYMQigDBqER4OHTqkv/76S5JUq1Yt+3Zfxz9y5Ig9soHgcu+9955QKqTciwaffPKJPB6PKlSooKpVq9qlAkDJ69SpkzIyMrRp06ZifVzfJg2n2yUQZ3bsaMXu3bsNp0FRMGIRYBi1CA9ut1sRERGnXKz/7rvvstg3BB3//7RTp0765JNPVKNGDUOJgPBhWZYqVaqkn376qVgft2PHjsrKyrIPdEPBMVoROhixCDDfffedJEYtQp3vpGmf22+/3X7b4XDo/PPPNxELJezzzz/P8/4PP/xAqQD8xOVy2dtKFyfLsuR2u/Xwww8X+2OHg2NHK3bt2mU4DYqKYhFgunfvbm9P2qZNG8NpUJKysrIkSU888YQ++OAD+/+7x+PRwoULTUZDCWnQoIEWL15snwAcHx+v/v37G04FhAff5gnLly8v1sd9/fXX5XA49NZbbxXr44aLatWqSZLi4uIUHx9vOA2KiqlQAej7779Xjx49JHGuRbipUqWK9u7dq/nz57MQMAxcd9112rJlix5++GG9+uqrpuMAIS07O1uRkZEqVaqUXe6Lg2+KY2xsrI4ePVpsjxsO0tLS7G24k5OTOVg0BDBiEYC6d+8uhyP3f03Dhg0Np4G/1KhRQ3v37tW4ceMoFSEsMzNTaWlpkqSePXtKki688EKDiYDwcNZZZ0mSvvrqq2J93IsuukiSKBWFUKFCBUm5oxWUitBAsQhQvjUWGzZssLenROjyeDzauXOnHnroITVr1sx0HJSg9u3b64ILLpDH49FVV10lSbrxxhsNpwJCn+/K+JEjR/Lc7vF47Dn+heHxeIqUK1ytXr3aPoB07969htOguFAsAlRiYqK9r71v/iFCV5kyZSRJ119/vdkgKDETJ07MsyvUe++9p1KlSun1119XcnLySbepBVB83n33XUlSnz59lJiYqFtuuUWS1Lp1a3Xp0kU9e/bUihUrTvsYXq9XaWlpeu6553TllVcqMTFR06dPV9euXUs6fshp3ry5JKlu3bqnPGAWwYf5FgFsx44dqlixovbt26ft27fbw7gILR999JFSUlJ07733MgUqRD3++OOaMmWKIiMjlZmZqYoVK2rs2LFauHChqlatKkl65plnDKcEQlurVq0UHR0tp9Op1q1ba+rUqZo0aZK8Xq+aNGmiNWvW6LbbbtPAgQPt0nG8rl276tChQ3kek10cC27MmDH29rwbN240nAbFicXbAa5ixYo6cOCA/YIEoeX888/Xn3/+KUlavHix4TQoCZs3b9b111+vqKgo/fXXX6pXr54kqV27dpo3b559P34VA/7l24nv2L+vjRs31rp162RZlipXrqwDBw6oadOmuuOOO3TffffJ6/WqatWqmjhxotq2bWsyflDzfe8vuugiTZkyxXAaFCemQgU43wmUWVlZxbqLBczyeDxq166d/vzzT91+++2UihDWp08fOZ1OZWRk2KVCkubOncv6KcAg33lBx24Tu3btWk2ePFlly5bV3r17FRUVpeXLl2vQoEHyer2KiorSli1bKBVFMHjwYPttSkXoYd5FgHO5XGrRooWWLVumzp07s0gsRFSoUEFJSUmSpCVLlhhOg5Iyfvx4ud1uzZw586QfdzgcjFQAhpzqgk737t118OBBP6cJHy+88IIk6b777jOcBCWBEYsgsHTpUkm5UyWefPJJw2lQHLp27SqHwyGXy6Vly5Zp4MCBpiOhmLndbo0cOVL169dXx44dTccBAOPat29vv/3mm28aTIKSQrEIEnfccYck6aWXXjKcBEXl8Xi0du1azZ07V+vWrZMkLVu2zGwoFLsuXbpIyt0yGgDCXWZmpubOnStJmjRpkuE0KCks3g4ivsVObdu2tf9xIvhccMEFmjVrlv1+bGyspk+fbh+KiOC0adMm1alTRw6HQ8nJyerSpYsWLVqUZ4tZAAhXZcuW1eHDh9mMJsRRLILI5MmT7QO1cnJy5HQ6DSdCYSQkJCg1NVWvv/66vvzyS40cOdJ0JBTRnXfeae/u5eNwOFicDQDKXRR/9tlnS5IOHz6shIQEw4lQUigWQSY6OlqZmZkqU6aMvfgXwcU38uQzduxY+6AgBI9XX31V06ZNU1JSkrKysiRJF154oR555BE9+OCD+u233zj0DgAkOZ1OeTwe1a5dW1u2bDEdByWIuRdBZufOnZJyG/+mTZsMp0Fh/P3337r55ptVqlQpSVL//v0NJ0JBDRw4UF988YUOHDhglwopd1rbFVdcofXr11MqAEDSyJEj7R0tN2/ebDgNShrFIsiUL19ederUkSQ1bNjQcBoURo0aNfTf//5Xqamp2rNnjzwej66++mp16NBBl19+uXr16qXs7GzTMXEMj8ejJ598Ujt27FDr1q01f/589evXTzk5OXm2C/b92wQA5PJtK9urV68TRuwRepgKFaR8/zjvvfdejR492nAaFMXtt9+ucePGybKsPPPy77nnHt1+++2G04Wf++67T4sXL9aFF16oyy+/XO3bt1e7du2Uk5MjKXdIf86cOWrdurX9OU2bNlX9+vX13XffmYoNAAGnadOmWr16tSzL4hyuMEGxCFKPPfaYXnnlFUm5++Wzo1Do2LZtm1q2bKn9+/erbdu2mjdvnv2xr7/+mqviJejyyy/X/v37T/qx6OhoXXLJJZo8ebKfUwFA8Dl8+LDKli0rSZo3b57atGljOBH8gWIRxHyLoSpXrqw9e/aYjoNidskll+iPP/5QlSpVtGLFCtWsWVOZmZlasGABO4IVwdGjR7V27Vqdf/759m379+/X5ZdffsJ9t27dqtatW+upp57ilFgAKICIiAjl5OSobNmyOnTokOk48BOKRRDbu3evqlSpIklat24day5CnNvtlsvl0r/+9S+9//77puMEpWeeeUY//fSTJGnx4sWSck+0b9Wq1Qnbw27ZsoUF2ABQCCNHjrQvxng8HtZWhBHmzwSxypUr29NifPtDI3RlZGRIks466yzDSYLLCy+8oMTERF100UX66aef7JGKm266SV6vV23btpXX67VLhWVZcrvdlAoAKCQWbIcvikWQ823d5vF41Lt3b8NpUJJ8e39///33hpMEl0mTJikmJkYZGRlq27atPVKRnp6u9u3b24uyfeuUHn74YdYsAUAh1a1bV1LuRZoJEyYYTgN/YypUCBg1apQGDRokiYXcoczj8ahOnTravn27/eIYp/fCCy9o0qRJOv7XnK9oSLmFwuPx2P/l3xAAFM7mzZtVr149SdLKlSt1zjnnGE4Ef+OvZwgYOHCgoqKiJEnx8fGG06CkOJ1Obd++XX369DEdJWjMmDHjpMPw6enp8nq98nq99sd9WyEeu40sACD/6tevL0mqWbMmpSJMUSxCRGpqqiQpLS1N7733nuE0KAkPPfSQpNxdjZA/1157rbxerypWrHjK++Tk5Mjr9SotLU3r16/XwoUL/ZgQAEJDz5497dHh7du3G04DUygWIcLlcunqq6+WJN19991mw6BENGrUSJLsaW+S9NRTTykxMVGJiYm66667TEULWHfccYf69OmjAwcOaPfu3ae833vvvaekpCQ1aNCAhYYAUEAZGRn69ttvJUlvvfWW2TAwijUWIcbhcMjr9apOnTr2wm6EhmrVqmn37t1yOp1yu9365JNPdOONN6pcuXL2HuGsvTi5xMRERUREKCMj44T1E1FRUcrKyjphu1kAQP5ER0crMzNTsbGxjKqHOUYsQsyff/4pKXcHoalTpxpOg+L0559/qkGDBvbi4htvvFGSVKpUKfs+vtPYkdcdd9yh7Oxs7dmzR127dpVlWbIsSxEREXapiIyMNB0TAILOk08+qczMTEnSwYMHDaeBaRSLENOiRQudd955kqQuXboYToPiVKVKFa1fv14ZGRnasmWLSpcurZkzZ2rLli12yfj5558NpwxMH3zwgaTc815+++03nXPOOerUqZO8Xq8qVKggj8ej/v37G04JAMElKytLL730kiTp/vvvV3R0tOFEMI2pUCHKNyWqVq1a2rp1q+k4KGHx8fFKTU3VhAkTTjjYzbeVarhauXKlbr31Vvv9u+++W3fccYf9vsfjUatWrZSSkqK4uDgTEQEgKPmmQEVFRdlbeCO8he+rjRC3bNkySdK2bds0ZcoUs2FQ4mJiYiRJtWrVynP7I488olatWql9+/bq2rWrunXrpkmTJpmIaIxvNy2Hw6ERI0bkKRWS1LdvX0miVABAATz++OP2FKjDhw+bDYOAwYhFCPvXv/6lpUuXStIJB4QhtHg8HjmdTsXGxqpnz56aOHGiqlWrps2bN6t58+Zavny5LMuyz21YtGjRSR8nJydHDocjZEY4LrjgAqWlpUmSXnrpJV188cV5Pt66dWu53W61aNHC/rcCADi97Oxse13awIEDNXLkSMOJECgoFiHONyWqdu3a2rJli+k4KEFbt25VnTp1JOXudJSZmXnCTkelS5dWamqq5s+fL6fTmefzk5KSdMkll0iSZs6cqdjYWP+FLyE9evTQrl27VLt2bU2YMEFSbslu2bKlXbTS09OZFwwABcAUKJxKaFyWxCktX75cUu6LTqZEhbbatWvbp0lnZGTI6/WesH3qmDFj5PV67dOlMzIy1KpVKyUmJtqlQpJWrFjh1+wl5bvvvtNdd92lzz//3L7Ndy3F6/XqnHPOoVQAQAE8+uijTIHCKTFiEQbOO+88e80F/7shSZZlqVWrVnK73VqyZInmzp2rPn36aNu2berRo4eeeeYZ0xFLjG/EYs+ePapcubLpOAAQNI6dAjVgwACNGjXKcCIEGopFmPBNiapSpcppTyBGeBg2bJiGDBliv+9bo1GpUiX9+OOPBpOVvP79+2vp0qWqWrWqdu3aZToOAASNyMhIZWdnMwUKp8RUqDCxceNGSdKePXvsPf0RvgYPHqzrr79eTZo0kfRP8Xz33XcNJys59957rxITE7V06VJZlqULL7zQdCQACBrXXnutsrOzJTEFCqfGiEUY6dmzp7799ltJubv/HL94F+HHt+Db4XDo6quv1lNPPWU6UolJTExU69attWDBAh09ejQkFqcDgD9s2rRJ9evXlySNHDlSAwcONJwIgYpiEWaioqKUlZWl6Ohopaenm44Dw3xToAYNGqSbb77ZdJwSlZiYKEmqU6eONm/ebDgNAAQP36h2tWrVtHPnTtNxEMCYChVmjh49Kil3N6DevXsbTgPTfLtGLViwwHCSkjdmzBhJ0sMPP2w4CQAEj8aNG9sbv1AqcCYUizDjcrk0YsQISdKXX36pNWvWGE4Ek7p16yZJevvttw0nKXn33nuvnE6nBgwYYDoKAASFUaNGad26dZKkefPmGU6DYECxCEOPPfaYatSoIUn24l2Ep6ysLEnShRdeqKSkJMNpSlZ2drbKly9vOgYABIWcnBwNGjRIknTFFVeoTZs2hhMhGLDGIoxZliVJbEEbxtxutxo1aqTNmzfL6/UqNjZW06ZNC8mF/YmJiTr33HND5vA/AChJvq1lIyIi7ItQwJkwYhHGtm7dKil3C9rhw4ebDQMjnE6nNm7cKI/HoylTpigtLU2tW7dWz549TUcrNg899JDatWsnSfrXv/5lOA0ABL5u3brZW8umpqYaToNgwohFmOvdu7e+/PJLSVJycrJKly5tOBFM2rhxozp16qRdu3apdevWGj16tOlIRdayZUt5vV7VrVtXmzZtMh0HAALa9OnT1blzZ0m5ayxYl4aCoFhA8fHxSk1NlcPhsHcJQnjzbUvs88gjjwTtLmKtWrVSdHS0vSMaAODkvF6vHI7cySxnn322Vq9ebTgRgg1ToaDk5GRJuWca1K5d22wYBITdu3erQYMGeu655+RyufTmm2/q0KFD8ng8pqMV2J133qm0tDTTMQAg4JUqVUpS7rkVlAoUBiMWkCStWrVK5557riRp2LBhevrppw0nQqAoXbq0UlJSJOX+sVm4cKHhRAXTpUsXJScni191AHBql19+uX755RdJuTsGRkREGE6EYMSIBSRJ55xzjj3VZfDgwVq/fr3hRAgUR44ckdfrldPpDLoRiz59+tgjcgCAk/vkk0/sUjFq1ChKBQqNEQvkUblyZe3bt0+SuMKLPHzbE0dGRmr27Nn2PNxA9eSTT+r333/XU089pV69erEjFACchNvtlsvlkiRdcMEFmjFjhuFECGaB/coAfrd37177bQ4Tw7E2bdqk0aNHKysrS6+//ro9PSpQ+dYLvfDCC5QKADiF6OhoSVJERASlAkXGiAVOsHPnTvtk7ksuuUS//fab4UQIJJUqVdL+/fslSYsXLzac5tRWrFih2267jZE3ADiFWrVqafv27ZJYV4HiwYgFTlC9enWNGTNGkvT777/rv//9r9lACCj79u1Tp06dJEnp6emG05xa9erVTUcAgIB166232qViyZIllAoUC4oFTuqOO+7QhRdeKCn3lw9nAOBY06dPlyRt3rzZbJDTWLZsmekIABCQZs6caV80HDJkCNNFUWyYCoXTiouL09GjR2VZVtDtCISSZVmWHA6HvvzyS9WpU8d0nBOMHTtW77zzDlOhAOAYHo9HTqdTktSoUSOtXbvWcCKEEkYscFpHjhyRlLtDFIu5caw///xTHo9H1113nTIyMkzHOQGHOwHAiSIjIyXlLtamVKC4USxwWg6HQzt27JAkHTp0SO3atTOcCIGgQ4cOeYbOX3zxRYNpTm7WrFkqV66c6RgAEDCqVq0qt9stSUxxRomgWOCMqlevrrFjx0qS5s2bp2effdZsIBg3d+5cxcfHa/Hixfr111/1/PPPm450goiICKWmppqOAQABoXv37tqzZ4+k3DVoLNZGSaBYIF9uu+02XXnllZKk5557joWxYSw1NVVer1dnn322pMA976RUqVKsCwIA5a45++GHHyRJzz//vJo3b244EUIVi7dRIDVq1NDOnTslsed1uLruuus0YcIETZ8+XXFxcabjnNJDDz2kmTNnsngbQFg79myqTp062bv6ASWBEQsUyI4dO+RyuST9c1onwktCQoIk2YfkBapzzz3XdAQAMMrj8dilokKFCpQKlDhGLFBgx25VV7p0aSUnJxtOBH+yLCvP+7GxscrOzlZ2drbuvvtu3XHHHYaS/WPHjh26+uqr2UoRQFhzuVxyu91yOBz2om2gJFEsUCi7d+9WtWrVJEmJiYlatGiR4UTwl7S0NE2ePFmzZ8/W7t279eOPP8rhcKhhw4Zavny5nE6nZs+ebXSaXLt27ZSVlcU0KABhq1KlSvbIMlOX4S9MhUKhVK1aVR9++KEkafHixXrkkUcMJ4K/xMbGqnfv3ho1apQmTpyojIwMpaWladmyZVq3bp3cbre6du1qLN9HH32krKwsLVy40FgGADCpdevWdqlYvnw5pQJ+Q7FAod1yyy265557JEmvvfaaXnrpJcOJYFrDhg119tln2wcr+ttnn32mt956S9WqVVPLli2NZAAAk3r27GlfWJk4caKaNWtmOBHCCcUCRfL222+re/fukqQnn3xSf/75p+FEMG3fvn1Gnvftt9/W66+/rooVK9o7lwFAOHn//ff17bffSpKGDh2qa665xmwghB3WWKBYVKtWTbt375Ykpaens2NUGNu2bZtq164tKXcEoWHDhn553tatWys6OppD8QCEpXnz5qldu3aSpDZt2mjevHmGEyEcUSxQbEqVKqW0tDRJLBQLd9u3b1etWrUUExOjWbNm+eU5fX9QMzMz/fJ8ABAofL9zJalBgwZav3694UQIV0yFQrFJTU21t6GNiooynAYmtWjRQpL0wAMP+O05o6KilJWV5bfnA4BAkJqaapeK0qVLUypgFMUCxcayLGVnZ8uyLHm93oA+lRnFq3LlyrIsS9HR0brhhhuUlJSk8uXLq1evXn7L4Cu1HAAFIFx4PB6VLl1aUu6htZwrBdMoFihWlmVp165dkqSjR48qPj7ecCKUNI/Ho3379ql+/frKycnRV199JUn64IMP/JZh165dOnz4sCRpzZo1fnteADDF4/EoIiJCXq9XlmUZ240POBZrLFAi9uzZo6pVq0qS6tatq02bNhlOhOKSkpKi5ORkvfDCC3r33XflcDjk8Xi0aNGiE07l9peLL75Yhw8f5kA8AGEjNjZW6enpsixLmZmZrGtEQGDEAiWiSpUq9j7amzdvVmJiouFEKIqYmBhFR0crLi5OpUuXVs2aNbVs2TJJktfr1XPPPWesVEjS4cOHVaFCBT377LNFehyPx2OPfABAoKpYsaLS09MlSWlpaZQKBAxGLFCiPvroI918882SpPPPP1+LFy82nAgFtXLlSjVr1kwRERHyeDyyLEs5OTmSJIfDERAnXC9atMg+rLFRo0Zau3btSe+3ZcsWXXLJJYqJidGqVatUp04dTZs2TZK0detWXXjhhZKknJwce80GAASSihUr6sCBA5KkVatWqWnTpoYTAf+gWKDEjR8/XrfccoskqX379po9e7bZQCiQunXrasuWLXYpzMjI0KBBg5SWlqZq1arplVdeMZzwHz169NCuXbtOmBKVnZ2tJ598UkuWLLEXd8fHxyslJSXP/eLi4pSamspZLAACUtWqVbVnzx5J0l9//aUmTZoYTgTkRbGAX3z88cf6v//7P0nS4MGDNXToUMOJkF//+c9/9NBDD9lrKXwSEhI0ZcoUv2bJyclRt27dNGjQIPvE92MlJiae9GAoh8Nhl42zzjpL33zzzSmfwzdtr1+/ftqwYYM9ItO5c2dNnTq1uL4UACiQyy+/XL/88oskSgUCF8UCftOzZ099++23kigXweall17SK6+8ovLly6t79+5auXKlfv/9d7300ku6+OKLS/S5X375ZXXq1Enjxo3TkiVL8nysatWqevLJJ+3D8S655BIlJSXlGbEYNmyYhgwZooULF8rhOPWysoyMDEVHR2vlypW6//77T7rDyrp16/x2kjgA+BxbKoYPH64nnnjCcCLg5CgW8KtrrrlGkyZNkkS5CHaWZal06dIlchX/8OHDJy0sF198sVq3bq3hw4erfPny2r9/vyRp8eLF+uqrr/Tyyy+ratWq9pbHvpwREREnjGIca8qUKXr88cftqVCSThih8alXr542btxY1C8RAPLl2FLx4osv6sknnzScCDg1igX8jpGL0GBZluLi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KHAA+mlVLi4u8PHxwZgxY/D222/D39+/nKut2M6ePYuYmBisW7cOaWlpKCgoeOlokoG9vT28vb0xfPhwfP7559KO7kREVD4YLIiIzESj0WDbtm1YtWoVzpw5g7y8vFJ3nDKED6VSibZt26JXr14IDAxEYGBgOVVtHfv27cMff/yBLVu24Pr168jKyoJKpSpxaDCQyWRwd3dHp06d8NlnnyEkJIQBgojIShgsiIgsQKfTIT09HdOnT0dsbCwePnyIp0+fmtTq1tAGFdBP76lWrZr0taCgIHTu3LnIOYMHD4aXl1eZ7pecnIyYmJgix2NiYpCcnCy9zszMlEZxRFE0+Xt0cXFB3bp18frrr2PRokVwcnKCTMZu6UREtobBgojIRuh0OqSlpWHWrFlIS0vD8ePHUVhYiLy8PACoNPttGAKRUqmEnZ0devbsierVq2PhwoUMDUREFRiDBRFRBfTsj26VSoXp06dDpVIBAO7du4cTJ04UCSIqlUp6T1k5OzsbbTwIAHK5HD169JBGTLy8vDBr1iyjgMAF0kRElR+DBRERERERmYzjzUREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhk/w9GtSHWpgpaQwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "#=====cycles through contours=========\n", "\n", @@ -870,54 +912,30 @@ "plot_mask()\n", "\n", "#====EOF====" - ], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 1000 - }, - "id": "OGhJ_fJT6j3t", - "outputId": "c00d4c6e-bdbb-4ca3-e426-ad0c36a70112" - }, - "execution_count": 62, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "(array([ 582, 582, 582, ..., 3207, 3208, 3208]), array([1674, 1675, 1676, ..., 3069, 3067, 3068]))\n", - "(array([2606, 2606, 2606, ..., 2687, 2688, 2688]), array([2408, 2409, 2410, ..., 2347, 2345, 2346]))\n", - "(array([1611, 1611, 1611, ..., 1826, 1826, 1826]), array([2278, 2279, 2280, ..., 2332, 2333, 2334]))\n", - "(array([1268, 1268, 1268, ..., 1551, 1551, 1551]), array([2994, 2995, 2996, ..., 3285, 3286, 3287]))\n", - "(array([ 621, 621, 621, ..., 1790, 1791, 1791]), array([1463, 1464, 1465, ..., 1153, 1151, 1152]))\n", - "(array([582, 582, 582, ..., 934, 935, 935]), array([1674, 1675, 1676, ..., 1729, 1727, 1728]))\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - ":180: RuntimeWarning: divide by zero encountered in log10\n", - " data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9))\n", - ":181: RuntimeWarning: divide by zero encountered in log10\n", - " data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0))\n", - ":182: RuntimeWarning: divide by zero encountered in log10\n", - " data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7))\n", - ":210: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored\n", - " plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2)\n" - ] - }, - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAMWCAYAAABsvhCnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACvpUlEQVR4nOzdZ2CT5cLG8etJ0klL2RvZQ1DAY9kgIiqKgiIOhr5OXIB7Cx4VFHEdFXAheHAPEMU92HvJlL1lr1JaupO8H3rySJmduTP+vy+2aZpcraXN9dzL8nq9XgEAAABAEThMBwAAAAAQ/CgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIrMZToAAKDodu7cKbfbLUnavXu3nnrqKWVlZeW5z/79+7Vt2zZ5vd5CPYfD4VD9+vWVkJCQ5/b4+Hi99tprKlWqlCQpOjpalSpVKtRzAACCl+Ut7F8YAECx83g82rp1q5KSkvTYY4/pyJEjWr16tdxut7Kzs+XxeExHLBKn0ymXy6WIiAidc845Kl++vF599VXFxsbqrLPOMh0PAFAEFAsA8BOv16tNmzZp9OjR+u2337R582ZlZ2fbIw1FYVmWXK5/BqETEhJUrVq1E+536623qm3btoV6jokTJ+rXX3894fZt27YpLS3Nfj8nJ6fQoyLHcjqdioyMVOPGjXXttdeqb9++ql27dpEfFwBQMigWAFCMUlJSNGvWLP373//W6tWrlZ6eXuAX2Q6HQy6XS/Hx8apZs6ZatGihgQMHyuFwqHnz5nI4gnN5XGZmplatWiVJGjp0qLZt26bNmzcrIyND2dnZhfo+lSpVSi1atNCIESP0r3/9S1FRUSURHQCQDxQLACiErVu36oknntCMGTO0f//+fI86OBwORUVFqW7durr66qt1ww03qEmTJnI6nSWcOLhkZGRow4YNGjlypGbMmKFt27YpKysr3+XD6XSqRo0a6tatmwYPHnzS0RsAQPGiWADAaRw5ckQff/yxhg0blu8C4XK5VKlSJXXu3FnPPvus6tev74ek4cXj8Wjt2rV6/PHHNX/+fB0+fFg5OTln/Dyn06maNWvqlVde0ZVXXqno6Gg/pAWA8ECxAID/OXLkiPr376/ffvtNycnJp706blmWYmNj1axZMw0bNkwXXHBBnjUOMCc9PV3ff/+9hg8frvXr1+dZ/3EylmWpfPnyuvbaa/XGG28wnQoAColiASAs5eTk6KmnntKYMWPOWCIiIiJUt25djR49WhdccIEiIiL8mBTFJS0tTd99950ef/xx7d69+7QjHJZlqUKFCnrqqad0//33y7IsPyYFgOBEsQAQFnbs2KErr7xSq1atOu10pujoaHXo0EHvv/++6tSp48eEMGXRokUaMGCAli9ffsLZH8eKiIhQp06d9NVXX6ls2bJ+TAgAwYFiASAkzZ07V71799aOHTtOORrhcrmUmJiozz//nG1MkceiRYt02223ac2aNacsog6HQw0bNtTkyZPVoEEDPycEgMATnHsWAsBx5s6dq7POOksOh0OWZal9+/b6+++/85SKhIQEvfPOO/buQtnZ2Zo3bx6lAido2bKlVq5caZ/JkZ6eriFDhigmJsa+j28BecOGDWVZlhwOh84++2xt2LDBYHIAMIcRCwBBadu2bbr66qu1YsWKU55GXa1aNX399ddq166dn9MhHHz22We67777dPDgwZN+3Ol0qnPnzvriiy9Uvnx5P6cDAP9jxAJA0HjmmWcUHR0ty7JUu3ZtLVu2LE+pqFatmmbNmiWv1yuv16udO3dSKlBi+vbtqwMHDsjr9crj8eizzz5TuXLl7I+73W798ccfqlChgizLUnx8vD744AODiQGgZDFiASBgbd26VZdddpnWr19/0nUSpUuX1ssvv6z+/fsH7WnUCE1ut1vPPPOM3njjjZNud2tZllq1aqUff/yR0QwAIYNiASCgfP3117rrrruUlJR0wsccDocuueQSTZ48WZGRkQbSAYWTlJSkrl27avHixSctyZUrV9Z3332n1q1bG0gHAMWDS3wAjBsyZIgiIiJkWZauv/76PKUiISFB06ZNk9frldvt1i+//EKpQNApW7asFi5cKI/HI4/Ho3feeUexsbH2x/fu3as2bdrIsizFxMQwZQpAUGLEAoARQ4YM0fDhw0+6lWeTJk00depUVa5c2UAywL9Wrlypbt26aceOHSd8LDIyUqNGjVL//v0NJAOAgmHEAoDfDB48WC6XS5ZladiwYXapsCxLffr0kdvtltfr1V9//UWpQNg499xz7a2Rs7OzdcEFF9gfy8rK0p133inLshQVFaX333/fYFIAOD1GLACUqNdff12PPfbYCSMTvjLxySefyLIsQ+mAwOV2u3XRRRdp5syZJ3wsMjJSn332mXr16mUgGQCcHMUCQLGbPn26unXrpvT09Dy3UyaAwnG73ercubNmzZp1wscSEhK0cuVK1axZ00AyAPgHxQJAsTh69KiaNGmi7du3n/Cxnj17auLEiZQJoBi43W61atVKf/755wkfa9GihRYvXiyn02kgGYBwxxoLAEXSp08fORwOxcXF5SkVjRo1UlZWlrxer7755htKBVBMnE6nlixZIq/Xq+TkZFWpUsX+2LJly+RyueR0OjV48GCDKQGEI0YsABTYrFmz1LVr1xOmOsXFxWnx4sVq1KiRoWRA+Pruu+90ww03KDMzM8/tCQkJWrt2bZ4CAgAlgRELAPni8XjUqVMnWZalCy64wC4VDodDw4cPl9frVUpKCqUCMOSqq65SRkaGvF6vbrvtNvv25ORkVa1aVZZl5bkdAIobIxYATmv9+vU677zzlJaWluf2Ro0aaeXKlYqIiDCUDMCZJCUlqUmTJtqzZ0+e2ytWrKj169erTJkyZoIBCEmMWAA4qT59+siyLDVq1MguFU6nU2PHjpXX69XatWspFUCAK1u2rHbv3i2v16shQ4bYa53279+vsmXLyuFw6OmnnzacEkCoYMQCgC0tLU2NGjU64QTgatWqacuWLYqMjDSUDEBx2bFjh5o2baojR47kub158+ZasmQJO0oBKDRGLABo7ty5ioiIUKlSpexSYVmWXn75ZXm9Xu3cuZNSAYSIGjVqKDk5WV6vV7fccot9+/Lly+VyuRQbG6sDBw6YCwggaFEsgDD28ssvy7IstW/fXjk5OZKk2NhYLV++XB6PR48++qjhhABK0ocffiiv16tJkybZUxvT09NVsWJFORwOff/994YTAggmTIUCwlD37t31ww8/5LmtadOmWrVqlaFEAAKB2+1WtWrVtG/fvjy3P/roo3r55ZcNpQIQLCgWQJjweDxq2LChNm3alOf2G264QV988YWhVAACVdu2bTV//vw8t3Xs2FEzZ840lAhAoGMqFBDiUlNTVbp0aTmdTrtUOBwOvffee/J6vZQKACc1b948eb1eDRgwwL5t1qxZsixL1atXl9vtNpgOQCBixAIIUampqapatapSU1Pt21wul5YsWaJmzZoZTAYgGE2aNEm9evXSsS8bqlatqu3bt8vlchlMBiBQUCyAEJOSkqJq1arlKRSlS5fW3r17FR0dbTAZgFCwdetWNWrUSFlZWfZt1apV07Zt2ygYQJijWAAhgkIBwJ+2bNmis88+W5mZmfZtFAwgvFEsgCCXnJysGjVqUCgAGMEIBgAfigUQpE5WKBISErRnzx4KBQC/27Ztmxo2bJinYLAGAwgvFAsgyBw9elRVqlShUAAISFu3blXjxo1PmCK1fft2OZ1Og8kAlDS2mwWChNfrVb169RQXF2eXioSEBB09elSHDx+mVAAICLVr11ZGRoa2bt2qyMhISdKuXbvkcrl04YUXmg0HoERRLIAg0KVLFzkcDm3evFmSFBMTYxeK2NhYw+kA4ES1atVSZmamtm7dao9UzJgxQ5ZlaeDAgYbTASgJFAsggN1///2yLEtTp06VlHsOxV9//aW0tDQKBYCgUKtWLeXk5Oibb76RZVmSpNGjR8uyLI0ZM8ZwOgDFiTUWQAD69ttv1bNnzzy3vf/+++rfv7+hRABQPB588EG98cYb9vsOh0Nr165V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U+3bt1e9evX0+OOPq3///pKkc889V6tWrZLEDAL4H5PgUWgrVqywS8VFF11EqQAAhJU///xTGzdu1Lp16+T1evXmm28W+TE7dOigm266SQ6HQ4mJiae8X1RUlGrUqKHNmzfrzjvv1FdffSWPx6OVK1eqSpUqkqTGjRtzOjf8ihELFMqxB+BVqlRJe/fuNZwIAAD/evjhh+0D7qKjozV79ux8f+7atWt11113qVq1avr888/t25OTk7Vv3748i7ZPZ9++ferWrZv9vm/tRVxcnLKysuRwOOR2u/OdCygKRixQKL4TRp1OJ6UCABCWXnvtNXm9Xg0ZMqTAB9TdeOONSk9P14YNG5SYmKhWrVrpzTffVJcuXdSnTx/16dOnQI933nnnacGCBZKkmJgYezqUx+NRfHx8gR4LKCyKBQqsXLly9mIx35kVAACEq48//liWZRX489xutzIyMuytZT/++GPVqlVLzz//vDZs2KCjR4+e8TEqVaqkxYsXa8yYMXI6nZo0aZLcbrcaNmyoIUOGSJJSU1MLdL4GUFgUCxRI27Zt7ZM+d+zYwVkVAICwNmDAAG3dulXVqlXL9+csWbLEfjsqKkqDBg1SRkaGvF6vtm7daheCa665psB5atSoocWLFysxMVFDhw7VzTffLCl3XeS9995b4McDCoJXhci3oUOH2mdVjB07VtWrVzecCAAAs8aMGSMpd6vX/Nq/f78kadu2bae8z5dffqmDBw/af3cL6t1331Xz5s01fvx4u6C88847+u233wr1eEB+sHgb+bJ8+XK1aNFCktS9e3dNnjzZbCAAAALAJ598optuukm33XbbKUcE9uzZY+/U5NOqVSt5vV7l5OSccvT/rLPO0t9//605c+YoKiqqUPk6d+6slJQU1atXT5s2bZIkpaSkKC4urlCPB5wOxQJndOwOUDVq1NDff/9tOBEAAIHBt7ZixowZKlWqVJ6P9e/fX0uXLpUk1a1bV1999ZUkqV27dqpataq2bdumK664Qj/88MMpH9/hcMjr9Wr06NFq3bp1gfNNmDBBL730khwOhyIjI5WRkXHCwXpAcaFY4IxcLpfcbrciIiJYrA0AwDFq1aql7du3n/Qgu9atW8uyLFmWpezs7JN+/siRIzVw4MDTPofL5VKpUqU0derUfOfKzs5Wnz59tHXrVvu2mJgYpaenS5LKlCljr5kEigtrLHBatWvXtve/LuhWegAAhDrfi3NfcdiwYYM95eiuu+5STk6OlixZou7du+vHH3/U4sWLlZGRoVmzZikjI+OMpULKLRY5OTl5bhswYIDGjx9/ys/p0qWLtm7dqvr169u3paen61//+pck6fDhw+rRo0fBvljgDCgWOKVBgwbZC8tWrVrFDlAAABzn8OHDcjgcateundasWaM+ffrohhtukCQdPHhQknTuuedq8uTJ6tatm84//3xFRUWpQ4cO+V434fF4lJaWpvvvv1+S9Pzzz2vBggUaOXKkVqxYcdLPadeunSSdsFvVn3/+qeuvv16S9P333+vHH38s+BcNnAJToXBSS5YsUWJioqTcgvHWW28ZTgQAQGDyeDxyOp2S/hld6NmzpyZNmqQ6depo8+bNRX4Op9Op0qVL648//tCyZct0xx132B+75557dPvtt5/wOf369dO6devkcrk0b948bd++Xffcc4/27dunxMREe/oWi7lRXCgWOMGxi7Xr1aunjRs3Gk4EAEBg27Jli9asWaMrrrjCvq24SoXb7ZbL5dJ//vMfdezYUZK0a9cu9ejRQ7GxsUpLS1Pv3r31yCOP5OvxfDtSxcbG6ujRoyzmRrFhbgtOEBMTI0mKiIigVAAAkA916tRR6dKlJUmRkZHyer3FUip8jy3JLhVS7hSn1q1bKy0tTZL0xRdfaO3atad8jJtvvlmtWrWSJFWpUkVer9e+iOj1elW2bNliyYrwRrFAHi1atGCxNgAAhdChQwd5vV5lZmYW6+OeanLJ6NGjNWjQIPv9J554wn77iy++UGJiohITE7Vq1SqtWbNGHo9HKSkpGjBggGJiYpSUlGSXlcOHD+uuu+4q1twIPxQL2IYNG6bly5dLkn744QcWawMAEAC++eabPP/12bBhg0aPHm2/f9VVV9lvv/nmm/bbt9xyiz3VqXv37nr66aeVnp6uzp07a9asWerXr58k6f3339f06dNL6stAGOCVIyRJR48e1ZAhQyTl/mI6do4oAAAwp2/fvpJ0wkhInz59FBkZqYsvvliSNG7cOPtjQ4cOtXeduvTSS7Vv3z5dd911ec6jqlmzphwOhz7//HPVq1dPUu5J3UBhsXgbkv452TMuLk4pKSmm4wAAAOWe2L1lyxZ16dJFI0aM0O+//67p06fr119/lSRNnDhR11xzjb78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lK/D75555hlJ0hNPPFGimYBQ4dskITExUVu3bi3x5xs8eLA8Ho/atWtX4s8FwCzf2TTDhw83nCT0sMYiHziZESeze/duVatWTZK0YMEC+6T2U0lMTFS3bt30448/+iMeENQOHjyoZs2aac+ePbIsSwsWLCix5+rRo4d27dqlK664Qj/++KMcDscZD8EEEJyys7MVGRkpidd1JYERiwJg1ALHqlq1qj1tok2bNrrjjjtOe/9zzjlHP/30Ey9YgHwoX768du7cqe3bt8vtduuqq64qsefatWuXJNmlv0yZMiX2XADMGjt2rCSd8WIgCodikQ++xbfXXXed4SQINC6XSy1btlSZMmW0bNmy0973ww8/lGVZ9pUSAGdWvXp19ejRQzt37lSrVq30zjvvFPtzvPXWWxo5cqR9GN/BgweL/TkABAbflORzzjnHcJLQRLHIB98P4dy5cw0nQSBauHChoqOjJeWOXJyKZVmaO3euPB6POnTo4K94QND77rvvFBkZKY/HY19tLE7t2rXThg0bdOjQIQ0aNKjYHx9A4EhOTpYk/fDDD4aThCbWWORDVlaWvZc63y6czK233qrx48fL6/Wqd+/eeuSRR05532uvvVbbtm1jhyiggG6//XaNGzdOCQkJmjJlSpEfb8+ePerZs6c9pTEiIkJpaWn52pABQHBi3WzJYsQiH46dupKfrUURfj788EN5PB5FRkbq+++/P+19L774Ynm9XnvXGwD5M3bsWJUrV65Yzpl46qmndOWVV8rtdmv48OHyeDzKysqiVAAhbObMmZLEgccliGKRT75ycc899xhOgkDmdrvP+Avr7rvvltPp1NNPP+2nVEDouO++++T1ejVy5MhCP8aoUaP022+/aeDAgXK73XriiSd4oQGEgT59+kiSatasaThJ6KJY5NPVV18tSfr000/NBkFAq1q1qlJTU7Vz587T3q969eo6cOCAlixZ4qdkQGj497//re7du2v8+PGFLheffPKJ4uLiilROAAQf3w5wn3/+ueEkoYs1FvnEOgvkl8PhUNmyZfXbb7+d9n4tW7ZUXFxcsUzrAMJNjx499P333+v5559Xt27dCvS5rVq1UuXKle0XGQDCA+srSh4jFvl07DqLjIwMg0kQ6KpXr65Dhw6d8X4NGjRQSkqKHxIBoWfy5MmqWbOmnnnmGe3YsaNAn+vxeBQREVFCyQAEolmzZklifUVJo1gUgK9c3HvvvYaTIJBNnz5d0pmviLz88suSpBo1apR0JCAkbd++XZJ088035/tzMjMz5XK5zjhdEUBo6d27tyT+5pY0ikUBsM4C+eE7UPFMV0Vq1KihmJgY7d692x+xgJAUGRmp5OTkfO3Yd8UVV6h9+/bKycmxr14CCA++qY9ffPGF4SShjWJRAB999JGk3PUWwKm88cYb+b5vdna2PB6PcnJySi4QEMJ8p2SvWLHilPfJyspSYmKi9u7dq4kTJ8rr9apt27b+igjAsGPPjWrXrp3BJKGPYlEAvsXb0j9/zIDj+abKTZw48Yz39Z3mPnTo0BLNBISiG2+8UfHx8ZKkxMTEEz5+8OBBpaamqnv37pJypydec801fs0IwLwxY8ZIyt1cBSWLXaEKKC4uTkePHlXHjh3tg1aA45UpU0bJycm64IIL9Prrr5/2vq1bt1ZERITS09P9lA4Ibh6PR06n035/0aJFJ0w9/OOPP/TEE0/Y748YMUKPPfaY3zICCBy+v8ktWrTQ0qVLTccJaRSLAho+fLieeuopORwOud1u03EQwKKjoxUZGalp06ad9n4PPvigZs2axfZ3QD5lZGQoJiZGNWvW1KRJk/J87JFHHrE3UIiOjqawA7AvPGzdulW1atUynCa0USwKgX2QkR8ul0ulSpXS1KlTT3u/UaNG6b///a8WLVp00ukcAE40efJkXXXVVZKkiIgIxcXFKSkpSZJ09tln6+jRo/rqq6/UunVrkzEBGMY5ZP7FZLNC8BWLH3/80XASmOTxeGRZlizL0rBhw+zbx40bZ49oPfroo2d8nIEDB8qyLHvXMQBn1qNHD3m9XnXv3l0ul0vJycnq16+fvF6vVq9erW3btlEqANhrrI5dJ4uSw4hFITRp0kRr1qxR6dKllZycbDoODGnWrJlWrlyp6OhoZWRkqHv37vrpp5/kdrsVExOj7777TuXKlcvXY11wwQVKT0/Ps3MFAAAoGqfTKY/Ho3//+9969tlnTccJeRSLQjhw4IAqVqwoiWG1cJKamqpOnTpp0aJF9sm9119/vR599FG1bdtWOTk5ioyM1Hvvvadzzz23QI+9b98+devWTeedd57+/PPPEvoKAAAIL0xf9y+X6QDBqEKFCvbbGRkZio6ONpgG/pCammpva9mxY0e1bNlSkuxdZubPn1+kx69UqZKaNWumpUuX6tJLL9Vvv/1WtMAAAIQ530YOZzqwFsWHNRaF5CsTl156qeEkKKj4+HhZlqWIiAh7T+vrrrtOPXr0OOn9faXC5XKpZs2amjt3rt5+++1izzVu3Di1atVKv//+e55tMgEAQMH51i42aNDAbJAwQrEopCFDhkiS5syZYzgJCmLr1q1KTU2VJOXk5Mjr9cqyLE2YMEHff/+9LMuS0+nUN998oxo1aqhixYqKj49XRESE5s+fr0mTJunmm29Wdna2ypYtW+z53n77bZ111lkaMWJEngXhAACgYHzrYH/99VfDScIHayyKwDe05tsdCIFv9+7dqlatmv3+lVdeqVtuuUUVKlRQRESEBg4cqDVr1igjI8O+T4cOHfTGG2/keZzk5GQlJCSUWM42bdrI6/UqJyenxJ4DAIBQdezfe17q+g/FoghcLpfcbrdefPFFPfnkk6bjIB+ys7N1xx136KOPPrJvW7x4scFEJ+dbzF2zZk1t377ddBwAAIJK06ZNtXr1apUtW1aHDh0yHSdsMBWqCC6++GJJ0r///W/DSZBfDz/8sF0qEhMT1bBhQ8OJTq5SpUrq3bu3/v77b5UqVcp0HAAAgsrq1aslSe+//77hJOGFEYsicLvdcrlyN9bi2xg8KlSooIMHDyoxMVHvvvuu6Tin5Ru5cDqdTIsCACAfPB6PnE6nJF6f+RsjFkXg+6GVpBkzZhhMgvx65ZVXdPDgQQ0cODDgS4WUO3IxdepUud1u1apVy3QcAAACXr9+/STJvvgL/6FYFNHZZ58tSbriiisMJ8GZXHPNNfa5E7fccovZMAVQunRpXXHFFdq+fbtmzZplOg4AIACd7Mr8tGnT7O3Vw2nU+8svv5QkPfXUU4aThB+mQhXRkSNH7N2B+FYGrldffVWPPvqoGjZsqJYtW+rBBx80HanA2rVrp+zsbHk8HtNRAAAB5P7779dbb72ltm3bau7cuZJyZ1Uc+/eiTJkySkpKMhXRb7xer31GFa/L/I8RiyIqXbq0/fbMmTMNJsHpvPTSS5JyD6ELxlIhSb///ru8Xq+6detmOgoAwCDfduSPPPKILMuyD22dN2+eYmNjJeWuM7jxxhvt7dKPHDliKq5f+aZBHTtdHf5DsSgGjRs3liRdfvnlhpPgVPbt2ydJeuCBB8wGKYJSpUrJsix7pwsAQPhZsWKFHA6HIiIi9Nprr6lKlSryeDy65pprNHnyZKWnp9tna82ePVsPPPCAHA6H3G634eT+8cUXX0hiGpQpFItisHDhQklSWlqa4SQ4FYfDoc6dOwfkmRUF0aRJE23btk2zZs1iiBcAwoDL5ZJlWbIsSw6HQ82bN1dkZKTmzJmjJk2aaM+ePfJ4PPrmm2/Uo0cPSbkXom6++WZt3bpVl112WdiUCq/Xa/9tfP755w2nCU8Ui2IQHx9vv83uUIFr6tSpkqQ777zTcJLCGz9+vOLi4nTBBRfI4XAoLi5O48aNMx0LAFACunfvLrfbrRkzZqhKlSqyLEvDhw/X3Llz9fbbb2v16tXq27evvF6vDh06pP3796t+/fo6evSoxo8fL0lhdRYS06DMY/F2MTn77LO1du1axcbG6ujRo6bj4BRq1KihXbt2adiwYeratavpOIWWk5Ojvn37atu2bXK73crIyFBUVJTpWACAYmRZlmrXrq0JEybYt3k8HrVv317Z2dlq1qyZli9fnudzUlJSdN555+nAgQPas2ePoqOj/R3bGIfDIa/Xq8GDB2vo0KGm44QlikUxYXeowObxeJSVlaXRo0frkUcekSQlJCRoypQphpMVXWJiotxut70LBgAg+FWrVk27d+/Wb7/9pnLlykmSxo4dq3feeUdS7uYx27ZtU5kyZQymDBzsBhUYeCVSTI7dHWry5MkGk+BkYmNjFRMTo7Fjx+rw4cOSpLJly5oNVQy+/fZbSaJUAECQ2Lx5s0qXLn3anSSPHj2q3bt3y+Fw2Bct169fr3feeUdVq1bV/v37lZycTKk4Rq9evSQxDco0Xo0Uo7Zt20qSrr32WsNJcLzMzExJ0po1a+xfxJ999pnBRMXj4osvliTt3LnTcBIAwMn8/PPPateunb0Au169ekpJSVGnTp3sBdmWZWnMmDH250RERCgiIkIej0evvfaaJGnixImSpO3bt6tChQpGvpZANmnSJEnSyy+/bDhJeGMqVDHyeDx2U+bbGjhmzpypTp06ScodRm7WrJkk2dvxBbvExET77UqVKmnv3r0G0wAAjuX7W1O2bFllZ2crNTVVUu5C4/3792vBggVKS0tTdna2SpUqpWbNmsntdmvhwoWyLEv//e9/1bRpU0m5v++vueYau2QgV1ZWlr3OkNdfZjFiUYx8Vx0k6aGHHjKcBj6XXnqpJKlDhw5q3ry5fdUoVDRv3ly//vqrbrnlFu3bt8++AsaBjQBgRr9+/dS/f3/7/UaNGun333/X9OnTVapUKUVFRemBBx7Qiy++qClTpmju3Llq27atMjIyNG/ePC1cuFDVqlXTokWL7FLx7rvvSpIOHjxo5GsKZL4LhjExMYaTgBGLYnbnnXdqzJgxYXUYTSDLyclRRESE/f4HH3ygFi1amAtUwrxer95//3198cUXSklJUf369TVo0CDdd999pqMBQEgbOnSotm3bpg8++MC+eOX1etW0aVOtXr06zyLsglqzZo3+7//+Ty6XS1lZWcUZOyT4vt/Tpk3ThRdeaDZMmKNYlADfD3h2drZcLpfhNOFt8+bNqlevnizLktfr1UUXXRQ28y8fe+wxTZs2TV6vV5ZlqVatWtqyZYvpWAAQcq666ip74xbf35tSpUopOTlZsbGxysrK0tSpU/Ns9JJfkyZN0gsvvCDLsjRr1iy1b9++uOMHtTVr1qhJkyaSmAYVCCgWJSAqKkpZWVk6//zzg/6k51Dg9Xo1YcIEXX/99XI4HPZJ6eFi3759uvHGG3Xo0CH7NsuytHTpUjVv3txgMgAIDR06dNCcOXNOuN1XMr788kvVq1evwI/br18/rVu3TmXKlFFSUlJxRA058fHxSk1N1VlnnaVt27aZjhP2KBYlYOLEifbOUHx7zfPtbR0dHa2RI0fqvPPOMx3JCI/Ho08//VTp6ekaO3asPB6PPB6P6VgAEFKOX8P33//+V+ecc06BHycrK0vt2rXTzTffrA8//DCk1gYWJ9/3JS0tjTUWAYBiUUJ8P+g7duxQ9erVDacJbxUrVtSBAwfyjB7t379ft956qyZPnmyfAZGRkRE2J5Tu27dP3bp1kyQ98MAD+s9//mM4EQCEBpfLJbfbrbfeekuVKlVS/fr1C/T5nTt3lsPhUHJysiRxAOppjBgxQk888YQsy+JCWYDgJ7WEVKtWTZLUuHFjw0nC2/XXX68DBw6ccPtVV12lPXv22Iu8BgwYoA4dOujRRx/1c0IzKlWqpHnz5snpdOqNN97Q1VdfbToSAISEnJwcRUdH6/777y9QqXjnnXfUqlUrpaSkKDk5WY0bN5bH46FUnMZTTz0lSbrhhhsMJ4EPIxYlJCUlxV6kxbfYDN8UqKioKHXu3FnDhg2zP5aYmKgmTZpo9erV9m1Op1Nutzvs1sXcfvvtWr58OVd8AKCYVKtWTbt37y7Q35PExETFxcXpgw8+4IVyPnB2RWCiBpeQ+Ph4zrQwaMSIEfZVnmnTpuUpFT7PPvusduzYoZdeekkffvihfWjRzTff7Nespo0dO1Z33nmnvF4vxQIATqFFixbq0KGDqlSpYp+P5PPJJ5/YZyRdccUV2r17t66//vp8P/bcuXMl5U7TpVTkD2dXBCaKRQm66667JElvvPGG2SBh5sCBA3ryySflcrm0ePFiRUZG2h/bt2+f2rVrJ0nq2LGjqlevrscff1yWZal9+/a69dZb9ddff5mKboyvhDmdTv3000+G0wBA4Fm+fLnmzJmjvXv36vfff1fFihUVExMjy7J000032Wcm/fTTT2rWrJkee+yxfD+272yKTz/9tESyh6J169ZJkn777TfDSXAspkKVMN+oxd69e1WpUiXDacKDb7vf2bNn51mM7fV61bZtW+Xk5Khv3772L/CuXbvm+cVUunRpTZ061e+5A8Ell1yipKQkde7cOWy/BwBwMsfuyuTbRvZYcXFxmj59eqEe++DBg+rataveeOMN3X///UWJGRbGjRun22+/XRLToAINIxYlLCEhQZLsw1tQsvbu3ausrCzdf//9dql49NFHlZiYqJYtWyonJ0evvvpqnqtCM2bMsK/YJyYmhvUL6t9//13du3fXtGnTVLp0aWVnZ5uOBAABYeTIkfbbl1xyiV599VX7/cWLFxeqVIwfP16dOnVS165dFRUVRanIpzvvvFOSdMEFFxhOguMxYlHC/v77b5111lmSaNX+MHLkSN13332KjIxUTk6OvWbguuuuU69evU46d3Xq1Knq0qWLZs6cqdjYWH9HDkjffPONXnzxRUVHRys9Pd10HAAwzuPxKC4uTunp6Zo/f76cTqfuvvtuDR48WDVr1izw4/3444/697//LYfDoY8++kj9+vUrgdSh59hF2x6Ph/M9AgwjFiWsZs2a9g993759DacJfYMGDdK5554rKXdXjiFDhsjr9eqrr7465YK4iy66SFLu/Fnkuuaaa/TAAw8oIyPDdBQA8JvExERZlqWLL744z+07duyQ0+lUenq6xowZI5fLJcuy9N577xWqVEiyt0J3u92UigKoU6eOJNnrWxBYGLHwgxdeeEGDBw+WxKhFIMrJyVFERITGjx+vpk2bmo4TMLxer1q2bKmoqCgtXLjQ3oEDAEJVzZo17RLhcrn0448/qkuXLnI6nfJ6vZo7d669SLsoli1bpgEDBigzM1MbNmwo8CF64cxXJvi+BSZGLPzg6aeftt/etWuXwSQ4ma+++kqSKBXHsSxLL730krKzs9W8eXN17tzZdCQAKFF///23pNxRhMzMTI0aNUrr1q2Tx+MptlIhSRMnTlRmZqYkqUGDBsXymOFgxIgR9tuUisBEsfAT31BpvXr1DCfB8XxTpI4tgMh18cUXa+HChSpbtqxmzJhhOg4AlKi33npLUu5UWim3YLRp00aSiq1USNLQoUPtx33kkUeK7XFD3ZNPPilJ6t27t+EkOBWKhZ9s3bpVkpSRkaGcnByzYWB77rnnFBUVpTJlyujXX381HSdguVwueb1etW/f3nQUACgx9913nyTpiiuukCR9//33Onz4sIYMGVLszzVq1ChJ0ubNm4v9sUPRihUr7Onkn3/+ueE0OBWKhZ84HA77oLbzzjvPcBpIuSecPvvss3K73Tp8+DCnd55G6dKlJeWeDrtw4ULDaQCg+EyfPl1OpzPPi9VbbrlFixcv1vz58zVr1ixdddVVJfb8FIv8adu2rSSpRo0ahpPgdCgWfjRp0iRJ0qpVqwwnCW933323LMtSpUqVZFmWunfvrq5du2rWrFmmowWsL7/8Uj/88IMkqXXr1qpbty5nXAAICdu3b5fH41Hfvn3thcG+C4Eul6tELzo1aNBAy5Yt4/fpGXi9XqWlpUn658RtBCaKhR9169bNfvvBBx80mCS8ffDBB4qOjlabNm3sfcRfeOEF07ECXpUqVewrelu2bFFkZKQsy9KHH35oOBkAFN6GDRvst6tUqaJZs2b5bRtT3wFvTJE+Pd828i6Xi/OmAhzFws98i7TeeOMNs0HCVNmyZeV2u9WxY0eNGjVKlSpVMh0pqDRo0EBTp061TyqXpJ07dxpMBACF07lzZ3322WcaPny4IiMjdd555+k///mP36bF3n777Ro7dqzKly/PVNwz+OuvvyRJs2fPNpwEZ8I5Fgb4roR8/fXXuvbaaw2nCS/x8fFKTU213x8xYoS6dOliMFFweu6557R8+XJt375dK1assK8mAUAwiImJUUZGhlwul3JyclShQgX98ssvfs2QmJioChUqaP/+/X593mDTp08fffHFF5I4CywYMGJhQKtWrSTl/mOBf6WkpMjr9crr9apGjRp6/PHH7e0FfX755RetWLHCUMLg8O9//1vvvPOOJKlZs2ZyOBzyeDyGUwHA6X300UeyLEsZGRmS/pmCdODAAb+8aJ0zZ45atmypxMRESdKNN95Y4s8Z7Hyl4tgzLBC4GLEwwOPxyOl0Sso9MK9q1aqGE4Wv9u3ba+7cuVq8eLH+85//6NNPP7U/dsUVV+i5554zmC6wpaamqnPnzipXrpwOHjwoSXI6nTpy5AhzYAEEjP379+vaa6/VnDlz5Ha7FRERoezsbFWoUEEffPCBrr76aklSbGysZs6cWWI5nn/+eU2ePFmxsbHavHmzKleuXGLPFSref/993XXXXZIYrQgWFAtDKleurH379ik+Pl5HjhwxHSdspaamKj4+3n6/UaNGWrt2rSIiImRZlubNm2cwXfDIzs5Wt27dlJSUJCl3e+XmzZurf//+uvPOO+V0OrVr1y7169dP06ZNM5wWQDipWLGiDhw4IMuyNGvWLEVHR+f5eMuWLeX1ejVo0CDdfPPNxf78s2fP1gMPPCBJuummm/TRRx8V+3OEKqfTKY/Ho+7du2vy5Mmm4yAfKBaGHDlyRAkJCZJyX5S5XC7DicLXK6+8ounTp+v999/X3LlzNXz4cC1dulRvvvkmB8IV0KeffqopU6Zo5cqV9tWlhIQEHT58WLVq1dL27dt1/fXX68svv/R7tjvvvFNLly5VvXr17KF1AKHP4XCobNmy+u2330768W3btmnr1q3q1KlTsT/3119/rREjRiguLk579+5lNLcAVqxYoebNm0titCKYUCwMio6OVmZmpipVqqS9e/eajhP2mjVrppUrV0rKHVH68ccfDScKbm63W998841GjBihyMhIZWVlybIseb1ebdy4UW3atFFGRoYSEhLsfeRLomBnZ2dr//79ql69un3b9u3bVbNmzWJ/LgCBx7IsDR482J7y5C/t27dXZmamXC6X0tLSFBER4dfnD3YRERHKyclRvXr1tHHjRtNxkE8s3jZo27ZtkqR9+/YZTgJJatOmjf12dna2MjIylJKSoldeeYXDiwrB6XTquuuu0x133KGsrCzVqVPHvupUv359HTx4UDk5Odq5c6ecTqciIiIUFRWlunXrqmHDhurVq5fmzJljjy4MGjRIvXr10nXXXadDhw5p2LBhGj58uIYP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LpDH49FVV10lSbrxxhsNpwJCn+/K+JEjR/Lc7vF47Dn+heHxeIqUK1ytXr3aPoB07969htOguFAsAlRiYqK9r71v/iFCV5kyZSRJ119/vdkgKDETJ07MsyvUe++9p1KlSun1119XcnLySbepBVB83n33XUlSnz59lJiYqFtuuUWS1Lp1a3Xp0kU9e/bUihUrTvsYXq9XaWlpeu6553TllVcqMTFR06dPV9euXUs6fshp3ry5JKlu3bqnPGAWwYf5FgFsx44dqlixovbt26ft27fbw7gILR999JFSUlJ07733MgUqRD3++OOaMmWKIiMjlZmZqYoVK2rs2LFauHChqlatKkl65plnDKcEQlurVq0UHR0tp9Op1q1ba+rUqZo0aZK8Xq+aNGmiNWvW6LbbbtPAgQPt0nG8rl276tChQ3kek10cC27MmDH29rwbN240nAbFicXbAa5ixYo6cOCA/YIEoeX888/Xn3/+KUlavHix4TQoCZs3b9b111+vqKgo/fXXX6pXr54kqV27dpo3b559P34VA/7l24nv2L+vjRs31rp162RZlipXrqwDBw6oadOmuuOOO3TffffJ6/WqatWqmjhxotq2bWsyflDzfe8vuugiTZkyxXAaFCemQgU43wmUWVlZxbqLBczyeDxq166d/vzzT91+++2UihDWp08fOZ1OZWRk2KVCkubOncv6KcAg33lBx24Tu3btWk2ePFlly5bV3r17FRUVpeXLl2vQoEHyer2KiorSli1bKBVFMHjwYPttSkXoYd5FgHO5XGrRooWWLVumzp07s0gsRFSoUEFJSUmSpCVLlhhOg5Iyfvx4ud1uzZw586QfdzgcjFQAhpzqgk737t118OBBP6cJHy+88IIk6b777jOcBCWBEYsgsHTpUkm5UyWefPJJw2lQHLp27SqHwyGXy6Vly5Zp4MCBpiOhmLndbo0cOVL169dXx44dTccBAOPat29vv/3mm28aTIKSQrEIEnfccYck6aWXXjKcBEXl8Xi0du1azZ07V+vWrZMkLVu2zGwoFLsuXbpIyt0yGgDCXWZmpubOnStJmjRpkuE0KCks3g4ivsVObdu2tf9xIvhccMEFmjVrlv1+bGyspk+fbh+KiOC0adMm1alTRw6HQ8nJyerSpYsWLVqUZ4tZAAhXZcuW1eHDh9mMJsRRLILI5MmT7QO1cnJy5HQ6DSdCYSQkJCg1NVWvv/66vvzyS40cOdJ0JBTRnXfeae/u5eNwOFicDQDKXRR/9tlnS5IOHz6shIQEw4lQUigWQSY6OlqZmZkqU6aMvfgXwcU38uQzduxY+6AgBI9XX31V06ZNU1JSkrKysiRJF154oR555BE9+OCD+u233zj0DgAkOZ1OeTwe1a5dW1u2bDEdByWIuRdBZufOnZJyG/+mTZsMp0Fh/P3337r55ptVqlQpSVL//v0NJ0JBDRw4UF988YUOHDhglwopd1rbFVdcofXr11MqAEDSyJEj7R0tN2/ebDgNShrFIsiUL19ederUkSQ1bNjQcBoURo0aNfTf//5Xqamp2rNnjzwej66++mp16NBBl19+uXr16qXs7GzTMXEMj8ejJ598Ujt27FDr1q01f/589evXTzk5OXm2C/b92wQA5PJtK9urV68TRuwRepgKFaR8/zjvvfdejR492nAaFMXtt9+ucePGybKsPPPy77nnHt1+++2G04Wf++67T4sXL9aFF16oyy+/XO3bt1e7du2Uk5MjKXdIf86cOWrdurX9OU2bNlX9+vX13XffmYoNAAGnadOmWr16tSzL4hyuMEGxCFKPPfaYXnnlFUm5++Wzo1Do2LZtm1q2bKn9+/erbdu2mjdvnv2xr7/+mqviJejyyy/X/v37T/qx6OhoXXLJJZo8ebKfUwFA8Dl8+LDKli0rSZo3b57atGljOBH8gWIRxHyLoSpXrqw9e/aYjoNidskll+iPP/5QlSpVtGLFCtWsWVOZmZlasGABO4IVwdGjR7V27Vqdf/759m379+/X5ZdffsJ9t27dqtatW+upp57ilFgAKICIiAjl5OSobNmyOnTokOk48BOKRRDbu3evqlSpIklat24day5CnNvtlsvl0r/+9S+9//77puMEpWeeeUY//fSTJGnx4sWSck+0b9Wq1Qnbw27ZsoUF2ABQCCNHjrQvxng8HtZWhBHmzwSxypUr29NifPtDI3RlZGRIks466yzDSYLLCy+8oMTERF100UX66aef7JGKm266SV6vV23btpXX67VLhWVZcrvdlAoAKCQWbIcvikWQ823d5vF41Lt3b8NpUJJ8e39///33hpMEl0mTJikmJkYZGRlq27atPVKRnp6u9u3b24uyfeuUHn74YdYsAUAh1a1bV1LuRZoJEyYYTgN/YypUCBg1apQGDRokiYXcoczj8ahOnTravn27/eIYp/fCCy9o0qRJOv7XnK9oSLmFwuPx2P/l3xAAFM7mzZtVr149SdLKlSt1zjnnGE4Ef+OvZwgYOHCgoqKiJEnx8fGG06CkOJ1Obd++XX369DEdJWjMmDHjpMPw6enp8nq98nq99sd9WyEeu40sACD/6tevL0mqWbMmpSJMUSxCRGpqqiQpLS1N7733nuE0KAkPPfSQpNxdjZA/1157rbxerypWrHjK++Tk5Mjr9SotLU3r16/XwoUL/ZgQAEJDz5497dHh7du3G04DUygWIcLlcunqq6+WJN19991mw6BENGrUSJLsaW+S9NRTTykxMVGJiYm66667TEULWHfccYf69OmjAwcOaPfu3ae833vvvaekpCQ1aNCAhYYAUEAZGRn69ttvJUlvvfWW2TAwijUWIcbhcMjr9apOnTr2wm6EhmrVqmn37t1yOp1yu9365JNPdOONN6pcuXL2HuGsvTi5xMRERUREKCMj44T1E1FRUcrKyjphu1kAQP5ER0crMzNTsbGxjKqHOUYsQsyff/4pKXcHoalTpxpOg+L0559/qkGDBvbi4htvvFGSVKpUKfs+vtPYkdcdd9yh7Oxs7dmzR127dpVlWbIsSxEREXapiIyMNB0TAILOk08+qczMTEnSwYMHDaeBaRSLENOiRQudd955kqQuXboYToPiVKVKFa1fv14ZGRnasmWLSpcurZkzZ2rLli12yfj5558NpwxMH3zwgaTc815+++03nXPOOerUqZO8Xq8qVKggj8ej/v37G04JAMElKytLL730kiTp/vvvV3R0tOFEMI2pUCHKNyWqVq1a2rp1q+k4KGHx8fFKTU3VhAkTTjjYzbeVarhauXKlbr31Vvv9u+++W3fccYf9vsfjUatWrZSSkqK4uDgTEQEgKPmmQEVFRdlbeCO8he+rjRC3bNkySdK2bds0ZcoUs2FQ4mJiYiRJtWrVynP7I488olatWql9+/bq2rWrunXrpkmTJpmIaIxvNy2Hw6ERI0bkKRWS1LdvX0miVABAATz++OP2FKjDhw+bDYOAwYhFCPvXv/6lpUuXStIJB4QhtHg8HjmdTsXGxqpnz56aOHGiqlWrps2bN6t58+Zavny5LMuyz21YtGjRSR8nJydHDocjZEY4LrjgAqWlpUmSXnrpJV188cV5Pt66dWu53W61aNHC/rcCADi97Oxse13awIEDNXLkSMOJECgoFiHONyWqdu3a2rJli+k4KEFbt25VnTp1JOXudJSZmXnCTkelS5dWamqq5s+fL6fTmefzk5KSdMkll0iSZs6cqdjYWP+FLyE9evTQrl27VLt2bU2YMEFSbslu2bKlXbTS09OZFwwABcAUKJxKaFyWxCktX75cUu6LTqZEhbbatWvbp0lnZGTI6/WesH3qmDFj5PV67dOlMzIy1KpVKyUmJtqlQpJWrFjh1+wl5bvvvtNdd92lzz//3L7Ndy3F6/XqnHPOoVQAQAE8+uijTIHCKTFiEQbOO+88e80F/7shSZZlqVWrVnK73VqyZInmzp2rPn36aNu2berRo4eeeeYZ0xFLjG/EYs+ePapcubLpOAAQNI6dAjVgwACNGjXKcCIEGopFmPBNiapSpcppTyBGeBg2bJiGDBliv+9bo1GpUiX9+OOPBpOVvP79+2vp0qWqWrWqdu3aZToOAASNyMhIZWdnMwUKp8RUqDCxceNGSdKePXvsPf0RvgYPHqzrr79eTZo0kfRP8Xz33XcNJys59957rxITE7V06VJZlqULL7zQdCQACBrXXnutsrOzJTEFCqfGiEUY6dmzp7799ltJubv/HL94F+HHt+Db4XDo6quv1lNPPWU6UolJTExU69attWDBAh09ejQkFqcDgD9s2rRJ9evXlySNHDlSAwcONJwIgYpiEWaioqKUlZWl6Ohopaenm44Dw3xToAYNGqSbb77ZdJwSlZiYKEmqU6eONm/ebDgNAAQP36h2tWrVtHPnTtNxEMCYChVmjh49Kil3N6DevXsbTgPTfLtGLViwwHCSkjdmzBhJ0sMPP2w4CQAEj8aNG9sbv1AqcCYUizDjcrk0YsQISdKXX36pNWvWGE4Ek7p16yZJevvttw0nKXn33nuvnE6nBgwYYDoKAASFUaNGad26dZKkefPmGU6DYECxCEOPPfaYatSoIUn24l2Ep6ysLEnShRdeqKSkJMNpSlZ2drbKly9vOgYABIWcnBwNGjRIknTFFVeoTZs2hhMhGLDGIoxZliVJbEEbxtxutxo1aqTNmzfL6/UqNjZW06ZNC8mF/YmJiTr33HND5vA/AChJvq1lIyIi7ItQwJkwYhHGtm7dKil3C9rhw4ebDQMjnE6nNm7cKI/HoylTpigtLU2tW7dWz549TUcrNg899JDatWsnSfrXv/5lOA0ABL5u3brZW8umpqYaToNgwohFmOvdu7e+/PJLSVJycrJKly5tOBFM2rhxozp16qRdu3apdevWGj16tOlIRdayZUt5vV7VrVtXmzZtMh0HAALa9OnT1blzZ0m5ayxYl4aCoFhA8fHxSk1NlcPhsHcJQnjzbUvs88gjjwTtLmKtWrVSdHS0vSMaAODkvF6vHI7cySxnn322Vq9ebTgRgg1ToaDk5GRJuWca1K5d22wYBITdu3erQYMGeu655+RyufTmm2/q0KFD8ng8pqMV2J133qm0tDTTMQAg4JUqVUpS7rkVlAoUBiMWkCStWrVK5557riRp2LBhevrppw0nQqAoXbq0UlJSJOX+sVm4cKHhRAXTpUsXJScni191AHBql19+uX755RdJuTsGRkREGE6EYMSIBSRJ55xzjj3VZfDgwVq/fr3hRAgUR44ckdfrldPpDLoRiz59+tgjcgCAk/vkk0/sUjFq1ChKBQqNEQvkUblyZe3bt0+SuMKLPHzbE0dGRmr27Nn2PNxA9eSTT+r333/XU089pV69erEjFACchNvtlsvlkiRdcMEFmjFjhuFECGaB/coAfrd37177bQ4Tw7E2bdqk0aNHKysrS6+//ro9PSpQ+dYLvfDCC5QKADiF6OhoSVJERASlAkXGiAVOsHPnTvtk7ksuuUS//fab4UQIJJUqVdL+/fslSYsXLzac5tRWrFih2267jZE3ADiFWrVqafv27ZJYV4HiwYgFTlC9enWNGTNGkvT777/rv//9r9lACCj79u1Tp06dJEnp6emG05xa9erVTUcAgIB166232qViyZIllAoUC4oFTuqOO+7QhRdeKCn3lw9nAOBY06dPlyRt3rzZbJDTWLZsmekIABCQZs6caV80HDJkCNNFUWyYCoXTiouL09GjR2VZVtDtCISSZVmWHA6HvvzyS9WpU8d0nBOMHTtW77zzDlOhAOAYHo9HTqdTktSoUSOtXbvWcCKEEkYscFpHjhyRlLtDFIu5caw///xTHo9H1113nTIyMkzHOQGHOwHAiSIjIyXlLtamVKC4USxwWg6HQzt27JAkHTp0SO3atTOcCIGgQ4cOeYbOX3zxRYNpTm7WrFkqV66c6RgAEDCqVq0qt9stSUxxRomgWOCMqlevrrFjx0qS5s2bp2effdZsIBg3d+5cxcfHa/Hixfr111/1/PPPm450goiICKWmppqOAQABoXv37tqzZ4+k3DVoLNZGSaBYIF9uu+02XXnllZKk5557joWxYSw1NVVer1dnn322pMA976RUqVKsCwIA5a45++GHHyRJzz//vJo3b244EUIVi7dRIDVq1NDOnTslsed1uLruuus0YcIETZ8+XXFxcabjnNJDDz2kmTNnsngbQFg79myqTp062bv6ASWBEQsUyI4dO+RyuST9c1onwktCQoIk2YfkBapzzz3XdAQAMMrj8dilokKFCpQKlDhGLFBgx25VV7p0aSUnJxtOBH+yLCvP+7GxscrOzlZ2drbuvvtu3XHHHYaS/WPHjh26+uqr2UoRQFhzuVxyu91yOBz2om2gJFEsUCi7d+9WtWrVJEmJiYlatGiR4UTwl7S0NE2ePFmzZ8/W7t279eOPP8rhcKhhw4Zavny5nE6nZs+ebXSaXLt27ZSVlcU0KABhq1KlSvbIMlOX4S9MhUKhVK1aVR9++KEkafHixXrkkUcMJ4K/xMbGqnfv3ho1apQmTpyojIwMpaWladmyZVq3bp3cbre6du1qLN9HH32krKwsLVy40FgGADCpdevWdqlYvnw5pQJ+Q7FAod1yyy265557JEmvvfaaXnrpJcOJYFrDhg119tln2wcr+ttnn32mt956S9WqVVPLli2NZAAAk3r27GlfWJk4caKaNWtmOBHCCcUCRfL222+re/fukqQnn3xSf/75p+FEMG3fvn1Gnvftt9/W66+/rooVK9o7lwFAOHn//ff17bffSpKGDh2qa665xmwghB3WWKBYVKtWTbt375Ykpaens2NUGNu2bZtq164tKXcEoWHDhn553tatWys6OppD8QCEpXnz5qldu3aSpDZt2mjevHmGEyEcUSxQbEqVKqW0tDRJLBQLd9u3b1etWrUUExOjWbNm+eU5fX9QMzMz/fJ8ABAofL9zJalBgwZav3694UQIV0yFQrFJTU21t6GNiooynAYmtWjRQpL0wAMP+O05o6KilJWV5bfnA4BAkJqaapeK0qVLUypgFMUCxcayLGVnZ8uyLHm93oA+lRnFq3LlyrIsS9HR0brhhhuUlJSk8uXLq1evXn7L4Cu1HAAFIFx4PB6VLl1aUu6htZwrBdMoFihWlmVp165dkqSjR48qPj7ecCKUNI/Ho3379ql+/frKycnRV199JUn64IMP/JZh165dOnz4sCRpzZo1fnteADDF4/EoIiJCXq9XlmUZ240POBZrLFAi9uzZo6pVq0qS6tatq02bNhlOhOKSkpKi5ORkvfDCC3r33XflcDjk8Xi0aNGiE07l9peLL75Yhw8f5kA8AGEjNjZW6enpsixLmZmZrGtEQGDEAiWiSpUq9j7amzdvVmJiouFEKIqYmBhFR0crLi5OpUuXVs2aNbVs2TJJktfr1XPPPWesVEjS4cOHVaFCBT377LNFehyPx2OPfABAoKpYsaLS09MlSWlpaZQKBAxGLFCiPvroI918882SpPPPP1+LFy82nAgFtXLlSjVr1kwRERHyeDyyLEs5OTmSJIfDERAnXC9atMg+rLFRo0Zau3btSe+3ZcsWXXLJJYqJidGqVatUp04dTZs2TZK0detWXXjhhZKknJwce80GAASSihUr6sCBA5KkVatWqWnTpoYTAf+gWKDEjR8/XrfccoskqX379po9e7bZQCiQunXrasuWLXYpzMjI0KBBg5SWlqZq1arplVdeMZzwHz169NCuXbtOmBKVnZ2tJ598UkuWLLEXd8fHxyslJSXP/eLi4pSamspZLAACUtWqVbVnzx5J0l9//aUmTZoYTgTkRbGAX3z88cf6v//7P0nS4MGDNXToUMOJkF//+c9/9NBDD9lrKXwSEhI0ZcoUv2bJyclRt27dNGjQIPvE92MlJiae9GAoh8Nhl42zzjpL33zzzSmfwzdtr1+/ftqwYYM9ItO5c2dNnTq1uL4UACiQyy+/XL/88oskSgUCF8UCftOzZ099++23kigXweall17SK6+8ovLly6t79+5auXKlfv/9d7300ku6+OKLS/S5X375ZXXq1Enjxo3TkiVL8nysatWqevLJJ+3D8S655BIlJSXlGbEYNmyYhgwZooULF8rhOPWysoyMDEVHR2vlypW6//77T7rDyrp16/x2kjgA+BxbKoYPH64nnnjCcCLg5CgW8KtrrrlGkyZNkkS5CHaWZal06dIlchX/8OHDJy0sF198sVq3bq3hw4erfPny2r9/vyRp8eLF+uqrr/Tyyy+ratWq9pbHvpwREREnjGIca8qUKXr88cftqVCSThih8alXr542btxY1C8RAPLl2FLx4osv6sknnzScCDg1igX8jpGL0GBZluLi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KHAA+mlVLi4u8PHxwZgxY/D222/D39+/nKut2M6ePYuYmBisW7cOaWlpKCgoeOlokoG9vT28vb0xfPhwfP7559KO7kREVD4YLIiIzESj0WDbtm1YtWoVzpw5g7y8vFJ3nDKED6VSibZt26JXr14IDAxEYGBgOVVtHfv27cMff/yBLVu24Pr168jKyoJKpSpxaDCQyWRwd3dHp06d8NlnnyEkJIQBgojIShgsiIgsQKfTIT09HdOnT0dsbCwePnyIp0+fmtTq1tAGFdBP76lWrZr0taCgIHTu3LnIOYMHD4aXl1eZ7pecnIyYmJgix2NiYpCcnCy9zszMlEZxRFE0+Xt0cXFB3bp18frrr2PRokVwcnKCTMZu6UREtobBgojIRuh0OqSlpWHWrFlIS0vD8ePHUVhYiLy8PACoNPttGAKRUqmEnZ0devbsierVq2PhwoUMDUREFRiDBRFRBfTsj26VSoXp06dDpVIBAO7du4cTJ04UCSIqlUp6T1k5OzsbbTwIAHK5HD169JBGTLy8vDBr1iyjgMAF0kRElR+DBRERERERmYzjzUREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhk/w9GtSHWpgpaQwAAAABJRU5ErkJggg==\n" - }, - "metadata": {} - } ] } - ] -} \ No newline at end of file + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "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.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/CHIMERA_V2.py b/CHIMERA_V2.py index 40822e3..ae40a50 100644 --- a/CHIMERA_V2.py +++ b/CHIMERA_V2.py @@ -1,3 +1,4 @@ +#import required libraries import astropy from astropy import wcs from astropy.io import fits @@ -16,8 +17,6 @@ import sunpy.map import sys from scipy.interpolate import interp2d, RectBivariateSpline -import sunpy.data.sample - plt.style.use(astropy_mpl_style) @@ -33,15 +32,14 @@ if im171 == [] or im193 == [] or im211 == [] or imhmi == []: print("Not all required files present") sys.exit() - + #Two functions that rescale the aia and hmi images from any original size to any final size #didn't normalize by exposure time for hmi because it was equal to 0 def rescale_aia(image: np.array, orig_res: int, desired_res: int): - hdu_number = 0 - hed = fits.getheader(image[0],hdu_number) - dat= fits.getdata(image[0], ext=0)/(hed["EXPTIME"]) + hed =fits.getheader(image[0], 0) + dat = fits.getdata(image[0], 0)/(hed["EXPTIME"]) if desired_res > orig_res: scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) @@ -89,72 +87,55 @@ def rescale_hmi(image: np.array, orig_res: int, desired_res: int): datc = rescale_aia(im211, 1024, 4096) datm = rescale_hmi(imhmi, 1024, 4096) -heda=fits.getheader(im171[0],0) -hedb=fits.getheader(im193[0],0) -hedc=fits.getheader(im211[0],0) -hedm=fits.getheader(imhmi[0],0) - -#filter_all: rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 -#filter_b: ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively -#filter_hmi: rotates array if 'crota1' is greater than 90 degrees - -def filter_all(aiaa: np.array, aiab: np.array, aiac: np.array): - hdu_number = 0 - heda = fits.getheader(aiaa[0],hdu_number) - hedb = fits.getheader(aiab[0],hdu_number) - hedc = fits.getheader(aiac[0],hdu_number) +#rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 +#ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively +#rotates array if 'crota1' is greater than 90 degrees +def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): + global heda, hedb, hedc, hedm + heda = fits.getheader(aiaa[0],0) + hedb = fits.getheader(aiab[0],0) + hedc = fits.getheader(aiac[0],0) + hedm = fits.getheader(aiam[0],0) + if hedb["ctype1"] != 'solar_x ': + hedb["ctype1"]='solar_x ' + hedb["ctype2"]='solar_y ' if heda['cdelt1'] > 1: heda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0 hedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0 hedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0 - -def filter_b(aiab: np.array): - hdu_number = 0 - hedb = fits.getheader(aiab[0],hdu_number) - if hedb["ctype1"] != 'solar_x ': - hedb["ctype1"]='solar_x ' - hedb["ctype2"]='solar_y ' - -def filter_hmi(aiac: np.array): - hdu_number = 0 - hedm=fits.getheader(imhmi[0],hdu_number) if hedm['crota1'] > 90: datm=np.rot90(np.rot90(datm)) -filter_all(im171, im193, im211) -filter_hmi(imhmi) -filter_b(im193) - +filter(im171, im193, im211, imhmi) #removes negative values from an array def remove_neg(aiaa: np.array, aiab:np.array, aiac: np.array): + global data, datb, datc data[np.where(data <= 0)] = 0 datb[np.where(datb <= 0)] = 0 datc[np.where(datc <= 0)] = 0 + if len(data[data < 0]) != 0: + print("data contains negative") + if len(datb[datb < 0]) != 0: + print("data contains negative") + if len(datc[datc < 0]) != 0: + print("datc contains negative") +remove_neg(im171, im193, im211) -remove_neg(data, datb, datc) +#defines the shape (length) of the array as "s" and the solar radius as "rs" +s=np.shape(data) +rs=heda['rsun'] -#defines shape of the array and the solar radius -def define_shape(aia: np.array): - hdu_number = 0 - return np.shape(aia) +def pix_arc(aia: np.array): + global dattoarc + dattoarc=heda['cdelt1'] + global conver + conver=((s[0])/2)*dattoarc/hedm['cdelt1']-(s[1]/2) + global convermul + convermul=dattoarc/hedm['cdelt1'] -def define_radius(image: np.array): - hdu_number = 0 - hed = fits.getheader(image[0],hdu_number) - return hed['rsun'] - -#defining important variables -s = define_shape(data) -rs = define_radius(im171) -print(s) -print(rs) - -#converting pixel values to arcsec -dattoarc = fits.getheader(im171[0],hdu_number)['cdelt1'] -conver=(s[0]/2)*dattoarc/hedm['cdelt1']-(s[1]/2) -convermul = dattoarc/hedm['cdelt1'] +pix_arc(im171) #converts to the Heliographic Stonyhurst coordinate system @@ -162,59 +143,369 @@ def to_helio(image: np.array): aia = sunpy.map.Map(image) adj = 4096/aia.dimensions[0].value x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj + global hpc hpc = aia.pixel_to_world(x, y) - return hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + global hg + hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + global csys + csys=wcs.WCS(hedb) -hpc = to_helio(im171) -csys=wcs.WCS(hedb) +to_helio(im171) #setting up arrays to be used in later processing -#only difference between iarr and bmcool is integer vs. float? +#only difference between iarr and bmcool is integer vs. float ident = 1 iarr = np.zeros((s[0],s[1]),dtype=np.byte) bmcool=np.zeros((s[0],s[1]),dtype=np.float32) - offarr,slate=np.array(iarr),np.array(iarr) cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool) - -#define the locations of the magnetic cutoffs -def cutoff_loc(size: int): - r = (size[1]/2.0)-450 - xgrid,ygrid=np.meshgrid(np.arange(size[0]),np.arange(size[1])) - center=[int(size[1]/2.0),int(size[1]/2.0)] - return np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2) - -#create 2D gaussian array for mag cutoffs -def create_gauss(size: int): - y,x=np.mgrid[0:4096,0:4096] - return Gaussian2D(1,size[0]/2,size[1]/2,2000/2.3548,2000/2.3548)(x,y) - -w = cutoff_loc(s) -garr = create_gauss(s) -garr[w] = 1.0 +circ=np.zeros((s[0],s[1]),dtype=int) + +#creation of a 2d gaussian for magnetic cut offs +r = (s[1]/2.0)-450 +xgrid,ygrid=np.meshgrid(np.arange(s[0]),np.arange(s[1])) +center=[int(s[1]/2.0),int(s[1]/2.0)] +w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2) +y,x=np.mgrid[0:4096,0:4096] +garr=Gaussian2D(1,s[0]/2,s[1]/2,2000/2.3548,2000/2.3548)(x,y) +#plt.plot(garr) +garr[w]=1.0 #creates sub-arrays of props to isolate column of index 0 and column of index 1 #what is props?? props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','','' props[:,1]='num','"','"','H°','"','"','"','"','"','"','"','"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx' - #define threshold values in log s -def set_thresh(dat: np.array, b_val: float, u_val: float): - with np.errstate(divide = 'ignore'): - t = np.log10(dat) - t[np.where(t < b_val)] = b_val - t[np.where(t > u_val)] = u_val - return np.array(((t - b_val)/(u_val - b_val))*255,dtype=np.float32) - -t0 = set_thresh(datc, .8, 2.7) -t1 = set_thresh(datb, 1.4, 3.0) -t2 = set_thresh(data, 1.2, 3.9) - -#ignores division and invalid erros in the following conditions to create 3 segmented bitmasks -with np.errstate(divide = 'ignore',invalid='ignore'): - bmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1 - bmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1 - bmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1 - -print(bmcool) \ No newline at end of file + +with np.errstate(divide = 'ignore'): + t0=np.log10(datc) + t1=np.log10(datb) + t2=np.log10(data) + +class Bounds: + def __init__(self, upper, lower, slope): + self.upper = upper + self.lower = lower + self.slope = slope + def new_u(self, new_upper): + self.upper = new_upper + def new_l(self, new_lower): + self.lower = new_lower + def new_s(self, new_slope): + self.slope = new_slope + +t0b = Bounds(.8, 2.7, 255) +t1b = Bounds(1.4, 3.0, 255) +t2b = Bounds(1.2, 3.9, 255) + +def threshold(tval: np.array): + global t0, t1, t2 + if tval.all() == t0.all(): + t0[np.where(t0 < t0b.upper)] = t0b.upper + t0[np.where(t0 > t0b.lower)] = t0b.lower + if tval.all() == t1.all(): + t1[np.where(t1 < t1b.upper)] = t1b.upper + t1[np.where(t1 > t1b.lower)] = t2b.lower + if tval.all() == t2.all(): + t2[np.where(t2 < t2b.upper)] = t2b.upper + t2[np.where(t2 > t2b.lower)] = t2b.lower + + +threshold(t0) +threshold(t1) +threshold(t2) + +def set_contour(tval: np.array): + global t0, t1, t2 + if tval.all() == t0.all(): + t0 = np.array(((t0-t0b.upper)/(t0b.lower-t0b.upper))*t0b.slope,dtype=np.float32) + elif tval.all() == t1.all(): + t1 = np.array(((t1-t1b.upper)/(t1b.lower-t1b.upper))*t1b.slope,dtype=np.float32) + elif tval.all() == t2.all(): + t2 = np.array(((t2-t2b.upper)/(t2b.lower-t2b.upper))*t2b.slope,dtype=np.float32) + +set_contour(t0) +set_contour(t1) +set_contour(t2) + +def create_mask(): + global t0, t1, t2, bmmix, bmhot, bmcool + with np.errstate(divide = 'ignore',invalid='ignore'): + bmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1 + bmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1 + bmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1 + +create_mask() + +def conjunction(): + global bmhot, bmcool, bmmix, cand + cand = bmcool*bmmix*bmhot + +conjunction() + +def misid(): + global s, r, w, circ, cand + r = (s[1]/2.0) - 100 + w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) + circ[w]=1.0 + cand=cand*circ + +misid() + +def on_off(): + global circ, cand + circ[:]=0 + r=(rs/dattoarc)-10 + inside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) + circ[inside]=1.0 + r=(rs/dattoarc)+40 + outside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 >= r**2) + circ[outside]=1.0 + cand=cand*circ + +on_off() + +def contours(): + global cand, cont, heir + cand=np.array(cand,dtype=np.uint8) + cont,heir=cv2.findContours(cand,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + +contours() + +def sort(): + global sizes, reord, tmp, cont + sizes=[] + for i in range(len(cont)): + sizes=np.append(sizes,len(cont[i])) + reord=sizes.ravel().argsort()[::-1] + tmp=list(cont) + for i in range(len(cont)): + tmp[i]=cont[reord[i]] + cont=list(tmp) + +sort() + + + +#=====cycles through contours========= + +for i in range(len(cont)): + + x=np.append(x,len(cont[i])) + +#=====only takes values of minimum surface length and calculates area====== + + if len(cont[i]) <= 100: + continue + area=0.5*np.abs(np.dot(cont[i][:,0,0],np.roll(cont[i][:,0,1],1))-np.dot(cont[i][:,0,1],np.roll(cont[i][:,0,0],1))) + arcar=(area*(dattoarc**2)) + if arcar > 1000: + +#=====finds centroid======= + + chpts=len(cont[i]) + cent=[np.mean(cont[i][:,0,0]),np.mean(cont[i][:,0,1])] + +#===remove quiet sun regions encompassed by coronal holes====== + + if (cand[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0) and (iarr[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) + iarr[np.where(slate == 1)]=0 + slate[:]=0 + + else: + +#====create a simple centre point====== + + arccent=csys.all_pix2world(cent[0],cent[1],0) + +#====classifies off limb CH regions======== + + if (((arccent[0]**2)+(arccent[1]**2)) > (rs**2)) or (np.sum(np.array(csys.all_pix2world(cont[i][0,0,0],cont[i][0,0,1],0))**2) > (rs**2)): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),offarr) + else: + +#=====classifies on disk coronal holes======= + + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) + poslin=np.where(slate == 1) + slate[:]=0 + print(poslin) + +#====create an array for magnetic polarity======== + + pos=np.zeros((len(poslin[0]),2),dtype=np.uint) + pos[:,0]=np.array((poslin[0]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) + pos[:,1]=np.array((poslin[1]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) + npix=list(np.histogram(datm[pos[:,0],pos[:,1]],bins=np.arange(np.round(np.min(datm[pos[:,0],pos[:,1]]))-0.5,np.round(np.max(datm[pos[:,0],pos[:,1]]))+0.6,1))) + npix[0][np.where(npix[0]==0)]=1 + npix[1]=npix[1][:-1]+0.5 + + wh1=np.where(npix[1] > 0) + wh2=np.where(npix[1] < 0) + +#=====magnetic cut offs dependant on area========= + + if np.absolute((np.sum(npix[0][wh1])-np.sum(npix[0][wh2]))/np.sqrt(np.sum(npix[0]))) <= 10 and arcar < 9000: + continue + if np.absolute(np.mean(datm[pos[:,0],pos[:,1]])) < garr[int(cent[0]),int(cent[1])] and arcar < 40000: + continue + iarr[poslin]=ident + +#====create an accurate center point======= + + ypos=np.sum((poslin[0])*np.absolute(hg.lat[poslin]))/np.sum(np.absolute(hg.lat[poslin])) + xpos=np.sum((poslin[1])*np.absolute(hg.lon[poslin]))/np.sum(np.absolute(hg.lon[poslin])) + + arccent=csys.all_pix2world(xpos,ypos,0) + +#======calculate average angle coronal hole is subjected to====== + + dist=np.sqrt((arccent[0]**2)+(arccent[1]**2)) + ang=np.arcsin(dist/rs) + +#=====calculate area of CH with minimal projection effects====== + + trupixar=abs(area/np.cos(ang)) + truarcar=trupixar*(dattoarc**2) + trummar=truarcar*((6.96e+08/rs)**2) + + +#====find CH extent in lattitude and longitude======== + + maxxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] + maxxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] + maxylat=hg.lat[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] + maxylon=hg.lon[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] + minxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] + minxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] + minylat=hg.lat[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] + minylon=hg.lon[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] + +#=====CH centroid in lat/lon======= + + centlat=hg.lat[int(ypos),int(xpos)] + centlon=hg.lon[int(ypos),int(xpos)] + +#====caluclate the mean magnetic field===== + + mB=np.mean(datm[pos[:,0],pos[:,1]]) + mBpos=np.sum(npix[0][wh1]*npix[1][wh1])/np.sum(npix[0][wh1]) + mBneg=np.sum(npix[0][wh2]*npix[1][wh2])/np.sum(npix[0][wh2]) + +#=====finds coordinates of CH boundaries======= + + Ywb,Xwb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0]),0) + Yeb,Xeb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0]),0) + Ynb,Xnb=csys.all_pix2world(np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0],0) + Ysb,Xsb=csys.all_pix2world(np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0],0) + + width=round(maxxlon.value)-round(minxlon.value) + + if minxlon.value >= 0.0 : eastl='W'+str(int(np.round(minxlon.value))) + else : eastl='E'+str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0 : westl='W'+str(int(np.round(maxxlon.value))) + else : westl='E'+str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0 : centlat='N'+str(int(np.round(centlat.value))) + else : centlat='S'+str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0 : centlon='W'+str(int(np.round(centlon.value))) + else : centlon='E'+str(np.absolute(int(np.round(centlon.value)))) + +#====insertions of CH properties into property array===== + + props[0,ident+1]=str(ident) + props[1,ident+1]=str(np.round(arccent[0])) + props[2,ident+1]=str(np.round(arccent[1])) + props[3,ident+1]=str(centlon+centlat) + props[4,ident+1]=str(np.round(Xeb)) + props[5,ident+1]=str(np.round(Yeb)) + props[6,ident+1]=str(np.round(Xwb)) + props[7,ident+1]=str(np.round(Ywb)) + props[8,ident+1]=str(np.round(Xnb)) + props[9,ident+1]=str(np.round(Ynb)) + props[10,ident+1]=str(np.round(Xsb)) + props[11,ident+1]=str(np.round(Ysb)) + props[12,ident+1]=str(eastl+'-'+westl) + props[13,ident+1]=str(width) + props[14,ident+1]='{:.1e}'.format(trummar/1e+12) + props[15,ident+1]=str(np.round((arcar*100/(np.pi*(rs**2))),1)) + props[16,ident+1]=str(np.round(mB,1)) + props[17,ident+1]=str(np.round(mBpos,1)) + props[18,ident+1]=str(np.round(mBneg,1)) + props[19,ident+1]=str(np.round(np.max(npix[1]),1)) + props[20,ident+1]=str(np.round(np.min(npix[1]),1)) + tbpos= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] > 0)]) + props[21,ident+1]='{:.1e}'.format(tbpos) + tbneg= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] < 0)]) + props[22,ident+1]='{:.1e}'.format(tbneg) + props[23,ident+1]='{:.1e}'.format(mB*trummar*1e+16) + props[24,ident+1]='{:.1e}'.format(mBpos*trummar*1e+16) + props[25,ident+1]='{:.1e}'.format(mBneg*trummar*1e+16) + +#=====sets up code for next possible coronal hole===== + + ident=ident+1 + +#=====sets ident back to max value of iarr====== + +ident=ident-1 +np.savetxt('ch_summary.txt', props, fmt = '%s') + + +from skimage.util import img_as_ubyte + +def rescale01(arr, cmin=None, cmax=None, a=0, b=1): + if cmin or cmax: + arr = np.clip(arr, cmin, cmax) + return (b-a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + + +def plot_tricolor(): + tricolorarray = np.zeros((4096, 4096, 3)) + + data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9)) + data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0)) + data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7)) + + tricolorarray[..., 0] = data_c/np.max(data_c) + tricolorarray[..., 1] = data_b/np.max(data_b) + tricolorarray[..., 2] = data_a/np.max(data_a) + + + fig, ax = plt.subplots(figsize = (10, 10)) + + plt.imshow(tricolorarray, origin = 'lower')#, extent = ) + cs=plt.contour(xgrid,ygrid,slate,colors='white',linewidths=0.5) + plt.savefig('tricolor.png') + plt.close() + +def plot_mask(slate=slate): + chs=np.where(iarr > 0) + slate[chs]=1 + slate=np.array(slate,dtype=np.uint8) + cont,heir=cv2.findContours(slate,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + + circ[:]=0 + r=(rs/dattoarc) + w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) + circ[w]=1.0 + + plt.figure(figsize=(10,10)) + plt.xlim(143,4014) + plt.ylim(143,4014) + plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2) + plt.gca().set_aspect('equal', adjustable='box') + plt.axis('off') + cs=plt.contour(xgrid,ygrid,slate,colors='black',linewidths=0.5) + cs=plt.contour(xgrid,ygrid,circ,colors='black',linewidths=1.0) + + plt.savefig('CH_mask_'+hedb["DATE"]+'.png',transparent=True) + #plt.close() +#====stores all CH properties in a text file===== + +plot_tricolor() +plot_mask() + +#====EOF==== diff --git a/CHIMERA_V3.py b/CHIMERA_V3.py new file mode 100644 index 0000000..ae40a50 --- /dev/null +++ b/CHIMERA_V3.py @@ -0,0 +1,511 @@ +#import required libraries +import astropy +from astropy import wcs +from astropy.io import fits +from astropy.modeling.models import Gaussian2D +import astropy.units as u +from astropy.utils.data import download_file +from astropy.visualization import astropy_mpl_style +import cv2 +import glob +import mahotas +import matplotlib.pyplot as plt +import numpy as np +import scipy +import scipy.interpolate +import sunpy +import sunpy.map +import sys +from scipy.interpolate import interp2d, RectBivariateSpline + +plt.style.use(astropy_mpl_style) + +# loading in the images as fits files + +im171 = glob.glob('171.fts') +im193 = glob.glob('193.fts') +im211 = glob.glob('211.fts') +imhmi = glob.glob('hmi.fts') + +#ensure that all images are present + +if im171 == [] or im193 == [] or im211 == [] or imhmi == []: + print("Not all required files present") + sys.exit() + +#Two functions that rescale the aia and hmi images from any original size to any final size + +#didn't normalize by exposure time for hmi because it was equal to 0 + +def rescale_aia(image: np.array, orig_res: int, desired_res: int): + hed =fits.getheader(image[0], 0) + dat = fits.getdata(image[0], 0)/(hed["EXPTIME"]) + if desired_res > orig_res: + scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + elif desired_res < orig_res: + scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + + +def rescale_hmi(image: np.array, orig_res: int, desired_res: int): + hdu_number = 0 + hed = fits.getheader(image[0],hdu_number) + dat= fits.getdata(image[0], ext=0) + if desired_res > orig_res: + scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + elif desired_res < orig_res: + scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) + dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) + if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + +#defining data and headers which are used in later steps +hdu_number = 0 + +data = rescale_aia(im171, 1024, 4096) +datb = rescale_aia(im193, 1024, 4096) +datc = rescale_aia(im211, 1024, 4096) +datm = rescale_hmi(imhmi, 1024, 4096) + +#rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 +#ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively +#rotates array if 'crota1' is greater than 90 degrees +def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): + global heda, hedb, hedc, hedm + heda = fits.getheader(aiaa[0],0) + hedb = fits.getheader(aiab[0],0) + hedc = fits.getheader(aiac[0],0) + hedm = fits.getheader(aiam[0],0) + if hedb["ctype1"] != 'solar_x ': + hedb["ctype1"]='solar_x ' + hedb["ctype2"]='solar_y ' + if heda['cdelt1'] > 1: + heda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0 + hedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0 + hedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0 + if hedm['crota1'] > 90: + datm=np.rot90(np.rot90(datm)) + +filter(im171, im193, im211, imhmi) + +#removes negative values from an array +def remove_neg(aiaa: np.array, aiab:np.array, aiac: np.array): + global data, datb, datc + data[np.where(data <= 0)] = 0 + datb[np.where(datb <= 0)] = 0 + datc[np.where(datc <= 0)] = 0 + if len(data[data < 0]) != 0: + print("data contains negative") + if len(datb[datb < 0]) != 0: + print("data contains negative") + if len(datc[datc < 0]) != 0: + print("datc contains negative") + +remove_neg(im171, im193, im211) + +#defines the shape (length) of the array as "s" and the solar radius as "rs" +s=np.shape(data) +rs=heda['rsun'] + +def pix_arc(aia: np.array): + global dattoarc + dattoarc=heda['cdelt1'] + global conver + conver=((s[0])/2)*dattoarc/hedm['cdelt1']-(s[1]/2) + global convermul + convermul=dattoarc/hedm['cdelt1'] + +pix_arc(im171) + +#converts to the Heliographic Stonyhurst coordinate system + +def to_helio(image: np.array): + aia = sunpy.map.Map(image) + adj = 4096/aia.dimensions[0].value + x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj + global hpc + hpc = aia.pixel_to_world(x, y) + global hg + hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + global csys + csys=wcs.WCS(hedb) + +to_helio(im171) + +#setting up arrays to be used in later processing +#only difference between iarr and bmcool is integer vs. float +ident = 1 +iarr = np.zeros((s[0],s[1]),dtype=np.byte) +bmcool=np.zeros((s[0],s[1]),dtype=np.float32) +offarr,slate=np.array(iarr),np.array(iarr) +cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool) +circ=np.zeros((s[0],s[1]),dtype=int) + +#creation of a 2d gaussian for magnetic cut offs +r = (s[1]/2.0)-450 +xgrid,ygrid=np.meshgrid(np.arange(s[0]),np.arange(s[1])) +center=[int(s[1]/2.0),int(s[1]/2.0)] +w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2) +y,x=np.mgrid[0:4096,0:4096] +garr=Gaussian2D(1,s[0]/2,s[1]/2,2000/2.3548,2000/2.3548)(x,y) +#plt.plot(garr) +garr[w]=1.0 + +#creates sub-arrays of props to isolate column of index 0 and column of index 1 +#what is props?? +props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','','' +props[:,1]='num','"','"','H°','"','"','"','"','"','"','"','"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx' +#define threshold values in log s + +with np.errstate(divide = 'ignore'): + t0=np.log10(datc) + t1=np.log10(datb) + t2=np.log10(data) + +class Bounds: + def __init__(self, upper, lower, slope): + self.upper = upper + self.lower = lower + self.slope = slope + def new_u(self, new_upper): + self.upper = new_upper + def new_l(self, new_lower): + self.lower = new_lower + def new_s(self, new_slope): + self.slope = new_slope + +t0b = Bounds(.8, 2.7, 255) +t1b = Bounds(1.4, 3.0, 255) +t2b = Bounds(1.2, 3.9, 255) + +def threshold(tval: np.array): + global t0, t1, t2 + if tval.all() == t0.all(): + t0[np.where(t0 < t0b.upper)] = t0b.upper + t0[np.where(t0 > t0b.lower)] = t0b.lower + if tval.all() == t1.all(): + t1[np.where(t1 < t1b.upper)] = t1b.upper + t1[np.where(t1 > t1b.lower)] = t2b.lower + if tval.all() == t2.all(): + t2[np.where(t2 < t2b.upper)] = t2b.upper + t2[np.where(t2 > t2b.lower)] = t2b.lower + + +threshold(t0) +threshold(t1) +threshold(t2) + +def set_contour(tval: np.array): + global t0, t1, t2 + if tval.all() == t0.all(): + t0 = np.array(((t0-t0b.upper)/(t0b.lower-t0b.upper))*t0b.slope,dtype=np.float32) + elif tval.all() == t1.all(): + t1 = np.array(((t1-t1b.upper)/(t1b.lower-t1b.upper))*t1b.slope,dtype=np.float32) + elif tval.all() == t2.all(): + t2 = np.array(((t2-t2b.upper)/(t2b.lower-t2b.upper))*t2b.slope,dtype=np.float32) + +set_contour(t0) +set_contour(t1) +set_contour(t2) + +def create_mask(): + global t0, t1, t2, bmmix, bmhot, bmcool + with np.errstate(divide = 'ignore',invalid='ignore'): + bmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1 + bmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1 + bmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1 + +create_mask() + +def conjunction(): + global bmhot, bmcool, bmmix, cand + cand = bmcool*bmmix*bmhot + +conjunction() + +def misid(): + global s, r, w, circ, cand + r = (s[1]/2.0) - 100 + w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) + circ[w]=1.0 + cand=cand*circ + +misid() + +def on_off(): + global circ, cand + circ[:]=0 + r=(rs/dattoarc)-10 + inside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) + circ[inside]=1.0 + r=(rs/dattoarc)+40 + outside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 >= r**2) + circ[outside]=1.0 + cand=cand*circ + +on_off() + +def contours(): + global cand, cont, heir + cand=np.array(cand,dtype=np.uint8) + cont,heir=cv2.findContours(cand,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + +contours() + +def sort(): + global sizes, reord, tmp, cont + sizes=[] + for i in range(len(cont)): + sizes=np.append(sizes,len(cont[i])) + reord=sizes.ravel().argsort()[::-1] + tmp=list(cont) + for i in range(len(cont)): + tmp[i]=cont[reord[i]] + cont=list(tmp) + +sort() + + + +#=====cycles through contours========= + +for i in range(len(cont)): + + x=np.append(x,len(cont[i])) + +#=====only takes values of minimum surface length and calculates area====== + + if len(cont[i]) <= 100: + continue + area=0.5*np.abs(np.dot(cont[i][:,0,0],np.roll(cont[i][:,0,1],1))-np.dot(cont[i][:,0,1],np.roll(cont[i][:,0,0],1))) + arcar=(area*(dattoarc**2)) + if arcar > 1000: + +#=====finds centroid======= + + chpts=len(cont[i]) + cent=[np.mean(cont[i][:,0,0]),np.mean(cont[i][:,0,1])] + +#===remove quiet sun regions encompassed by coronal holes====== + + if (cand[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0) and (iarr[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) + iarr[np.where(slate == 1)]=0 + slate[:]=0 + + else: + +#====create a simple centre point====== + + arccent=csys.all_pix2world(cent[0],cent[1],0) + +#====classifies off limb CH regions======== + + if (((arccent[0]**2)+(arccent[1]**2)) > (rs**2)) or (np.sum(np.array(csys.all_pix2world(cont[i][0,0,0],cont[i][0,0,1],0))**2) > (rs**2)): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),offarr) + else: + +#=====classifies on disk coronal holes======= + + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) + poslin=np.where(slate == 1) + slate[:]=0 + print(poslin) + +#====create an array for magnetic polarity======== + + pos=np.zeros((len(poslin[0]),2),dtype=np.uint) + pos[:,0]=np.array((poslin[0]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) + pos[:,1]=np.array((poslin[1]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) + npix=list(np.histogram(datm[pos[:,0],pos[:,1]],bins=np.arange(np.round(np.min(datm[pos[:,0],pos[:,1]]))-0.5,np.round(np.max(datm[pos[:,0],pos[:,1]]))+0.6,1))) + npix[0][np.where(npix[0]==0)]=1 + npix[1]=npix[1][:-1]+0.5 + + wh1=np.where(npix[1] > 0) + wh2=np.where(npix[1] < 0) + +#=====magnetic cut offs dependant on area========= + + if np.absolute((np.sum(npix[0][wh1])-np.sum(npix[0][wh2]))/np.sqrt(np.sum(npix[0]))) <= 10 and arcar < 9000: + continue + if np.absolute(np.mean(datm[pos[:,0],pos[:,1]])) < garr[int(cent[0]),int(cent[1])] and arcar < 40000: + continue + iarr[poslin]=ident + +#====create an accurate center point======= + + ypos=np.sum((poslin[0])*np.absolute(hg.lat[poslin]))/np.sum(np.absolute(hg.lat[poslin])) + xpos=np.sum((poslin[1])*np.absolute(hg.lon[poslin]))/np.sum(np.absolute(hg.lon[poslin])) + + arccent=csys.all_pix2world(xpos,ypos,0) + +#======calculate average angle coronal hole is subjected to====== + + dist=np.sqrt((arccent[0]**2)+(arccent[1]**2)) + ang=np.arcsin(dist/rs) + +#=====calculate area of CH with minimal projection effects====== + + trupixar=abs(area/np.cos(ang)) + truarcar=trupixar*(dattoarc**2) + trummar=truarcar*((6.96e+08/rs)**2) + + +#====find CH extent in lattitude and longitude======== + + maxxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] + maxxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] + maxylat=hg.lat[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] + maxylon=hg.lon[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] + minxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] + minxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] + minylat=hg.lat[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] + minylon=hg.lon[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] + +#=====CH centroid in lat/lon======= + + centlat=hg.lat[int(ypos),int(xpos)] + centlon=hg.lon[int(ypos),int(xpos)] + +#====caluclate the mean magnetic field===== + + mB=np.mean(datm[pos[:,0],pos[:,1]]) + mBpos=np.sum(npix[0][wh1]*npix[1][wh1])/np.sum(npix[0][wh1]) + mBneg=np.sum(npix[0][wh2]*npix[1][wh2])/np.sum(npix[0][wh2]) + +#=====finds coordinates of CH boundaries======= + + Ywb,Xwb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0]),0) + Yeb,Xeb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0]),0) + Ynb,Xnb=csys.all_pix2world(np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0],0) + Ysb,Xsb=csys.all_pix2world(np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0],0) + + width=round(maxxlon.value)-round(minxlon.value) + + if minxlon.value >= 0.0 : eastl='W'+str(int(np.round(minxlon.value))) + else : eastl='E'+str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0 : westl='W'+str(int(np.round(maxxlon.value))) + else : westl='E'+str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0 : centlat='N'+str(int(np.round(centlat.value))) + else : centlat='S'+str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0 : centlon='W'+str(int(np.round(centlon.value))) + else : centlon='E'+str(np.absolute(int(np.round(centlon.value)))) + +#====insertions of CH properties into property array===== + + props[0,ident+1]=str(ident) + props[1,ident+1]=str(np.round(arccent[0])) + props[2,ident+1]=str(np.round(arccent[1])) + props[3,ident+1]=str(centlon+centlat) + props[4,ident+1]=str(np.round(Xeb)) + props[5,ident+1]=str(np.round(Yeb)) + props[6,ident+1]=str(np.round(Xwb)) + props[7,ident+1]=str(np.round(Ywb)) + props[8,ident+1]=str(np.round(Xnb)) + props[9,ident+1]=str(np.round(Ynb)) + props[10,ident+1]=str(np.round(Xsb)) + props[11,ident+1]=str(np.round(Ysb)) + props[12,ident+1]=str(eastl+'-'+westl) + props[13,ident+1]=str(width) + props[14,ident+1]='{:.1e}'.format(trummar/1e+12) + props[15,ident+1]=str(np.round((arcar*100/(np.pi*(rs**2))),1)) + props[16,ident+1]=str(np.round(mB,1)) + props[17,ident+1]=str(np.round(mBpos,1)) + props[18,ident+1]=str(np.round(mBneg,1)) + props[19,ident+1]=str(np.round(np.max(npix[1]),1)) + props[20,ident+1]=str(np.round(np.min(npix[1]),1)) + tbpos= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] > 0)]) + props[21,ident+1]='{:.1e}'.format(tbpos) + tbneg= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] < 0)]) + props[22,ident+1]='{:.1e}'.format(tbneg) + props[23,ident+1]='{:.1e}'.format(mB*trummar*1e+16) + props[24,ident+1]='{:.1e}'.format(mBpos*trummar*1e+16) + props[25,ident+1]='{:.1e}'.format(mBneg*trummar*1e+16) + +#=====sets up code for next possible coronal hole===== + + ident=ident+1 + +#=====sets ident back to max value of iarr====== + +ident=ident-1 +np.savetxt('ch_summary.txt', props, fmt = '%s') + + +from skimage.util import img_as_ubyte + +def rescale01(arr, cmin=None, cmax=None, a=0, b=1): + if cmin or cmax: + arr = np.clip(arr, cmin, cmax) + return (b-a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + + +def plot_tricolor(): + tricolorarray = np.zeros((4096, 4096, 3)) + + data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9)) + data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0)) + data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7)) + + tricolorarray[..., 0] = data_c/np.max(data_c) + tricolorarray[..., 1] = data_b/np.max(data_b) + tricolorarray[..., 2] = data_a/np.max(data_a) + + + fig, ax = plt.subplots(figsize = (10, 10)) + + plt.imshow(tricolorarray, origin = 'lower')#, extent = ) + cs=plt.contour(xgrid,ygrid,slate,colors='white',linewidths=0.5) + plt.savefig('tricolor.png') + plt.close() + +def plot_mask(slate=slate): + chs=np.where(iarr > 0) + slate[chs]=1 + slate=np.array(slate,dtype=np.uint8) + cont,heir=cv2.findContours(slate,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + + circ[:]=0 + r=(rs/dattoarc) + w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) + circ[w]=1.0 + + plt.figure(figsize=(10,10)) + plt.xlim(143,4014) + plt.ylim(143,4014) + plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2) + plt.gca().set_aspect('equal', adjustable='box') + plt.axis('off') + cs=plt.contour(xgrid,ygrid,slate,colors='black',linewidths=0.5) + cs=plt.contour(xgrid,ygrid,circ,colors='black',linewidths=1.0) + + plt.savefig('CH_mask_'+hedb["DATE"]+'.png',transparent=True) + #plt.close() +#====stores all CH properties in a text file===== + +plot_tricolor() +plot_mask() + +#====EOF==== From e0ced29aeb0b08fc0d5c8da9ceb169ffcf3ebf97 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:22:10 +0000 Subject: [PATCH 04/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- CHIMERA_V2.py | 887 ++++++++++++++++++++++++++++++-------------------- CHIMERA_V3.py | 887 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 1062 insertions(+), 712 deletions(-) diff --git a/CHIMERA_V2.py b/CHIMERA_V2.py index ae40a50..48b0111 100644 --- a/CHIMERA_V2.py +++ b/CHIMERA_V2.py @@ -1,13 +1,9 @@ -#import required libraries -import astropy -from astropy import wcs -from astropy.io import fits -from astropy.modeling.models import Gaussian2D +# import required libraries +import glob +import sys + import astropy.units as u -from astropy.utils.data import download_file -from astropy.visualization import astropy_mpl_style import cv2 -import glob import mahotas import matplotlib.pyplot as plt import numpy as np @@ -15,71 +11,75 @@ import scipy.interpolate import sunpy import sunpy.map -import sys -from scipy.interpolate import interp2d, RectBivariateSpline +from astropy import wcs +from astropy.io import fits +from astropy.modeling.models import Gaussian2D +from astropy.visualization import astropy_mpl_style plt.style.use(astropy_mpl_style) # loading in the images as fits files -im171 = glob.glob('171.fts') -im193 = glob.glob('193.fts') -im211 = glob.glob('211.fts') -imhmi = glob.glob('hmi.fts') +im171 = glob.glob("171.fts") +im193 = glob.glob("193.fts") +im211 = glob.glob("211.fts") +imhmi = glob.glob("hmi.fts") -#ensure that all images are present +# ensure that all images are present if im171 == [] or im193 == [] or im211 == [] or imhmi == []: - print("Not all required files present") - sys.exit() - -#Two functions that rescale the aia and hmi images from any original size to any final size + print("Not all required files present") + sys.exit() + +# Two functions that rescale the aia and hmi images from any original size to any final size + +# didn't normalize by exposure time for hmi because it was equal to 0 -#didn't normalize by exposure time for hmi because it was equal to 0 def rescale_aia(image: np.array, orig_res: int, desired_res: int): - hed =fits.getheader(image[0], 0) - dat = fits.getdata(image[0], 0)/(hed["EXPTIME"]) - if desired_res > orig_res: - scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) - elif desired_res < orig_res: - scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + hed = fits.getheader(image[0], 0) + dat = fits.getdata(image[0], 0) / (hed["EXPTIME"]) + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) def rescale_hmi(image: np.array, orig_res: int, desired_res: int): - hdu_number = 0 - hed = fits.getheader(image[0],hdu_number) - dat= fits.getdata(image[0], ext=0) - if desired_res > orig_res: - scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) - elif desired_res < orig_res: - scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) - -#defining data and headers which are used in later steps + hdu_number = 0 + hed = fits.getheader(image[0], hdu_number) + dat = fits.getdata(image[0], ext=0) + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + + +# defining data and headers which are used in later steps hdu_number = 0 data = rescale_aia(im171, 1024, 4096) @@ -87,118 +87,201 @@ def rescale_hmi(image: np.array, orig_res: int, desired_res: int): datc = rescale_aia(im211, 1024, 4096) datm = rescale_hmi(imhmi, 1024, 4096) -#rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 -#ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively -#rotates array if 'crota1' is greater than 90 degrees + +# rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 +# ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively +# rotates array if 'crota1' is greater than 90 degrees def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): - global heda, hedb, hedc, hedm - heda = fits.getheader(aiaa[0],0) - hedb = fits.getheader(aiab[0],0) - hedc = fits.getheader(aiac[0],0) - hedm = fits.getheader(aiam[0],0) - if hedb["ctype1"] != 'solar_x ': - hedb["ctype1"]='solar_x ' - hedb["ctype2"]='solar_y ' - if heda['cdelt1'] > 1: - heda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0 - hedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0 - hedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0 - if hedm['crota1'] > 90: - datm=np.rot90(np.rot90(datm)) + global heda, hedb, hedc, hedm + heda = fits.getheader(aiaa[0], 0) + hedb = fits.getheader(aiab[0], 0) + hedc = fits.getheader(aiac[0], 0) + hedm = fits.getheader(aiam[0], 0) + if hedb["ctype1"] != "solar_x ": + hedb["ctype1"] = "solar_x " + hedb["ctype2"] = "solar_y " + if heda["cdelt1"] > 1: + heda["cdelt1"], heda["cdelt2"], heda["crpix1"], heda["crpix2"] = ( + heda["cdelt1"] / 4.0, + heda["cdelt2"] / 4.0, + heda["crpix1"] * 4.0, + heda["crpix2"] * 4.0, + ) + hedb["cdelt1"], hedb["cdelt2"], hedb["crpix1"], hedb["crpix2"] = ( + hedb["cdelt1"] / 4.0, + hedb["cdelt2"] / 4.0, + hedb["crpix1"] * 4.0, + hedb["crpix2"] * 4.0, + ) + hedc["cdelt1"], hedc["cdelt2"], hedc["crpix1"], hedc["crpix2"] = ( + hedc["cdelt1"] / 4.0, + hedc["cdelt2"] / 4.0, + hedc["crpix1"] * 4.0, + hedc["crpix2"] * 4.0, + ) + if hedm["crota1"] > 90: + datm = np.rot90(np.rot90(datm)) + filter(im171, im193, im211, imhmi) -#removes negative values from an array -def remove_neg(aiaa: np.array, aiab:np.array, aiac: np.array): - global data, datb, datc - data[np.where(data <= 0)] = 0 - datb[np.where(datb <= 0)] = 0 - datc[np.where(datc <= 0)] = 0 - if len(data[data < 0]) != 0: - print("data contains negative") - if len(datb[datb < 0]) != 0: - print("data contains negative") - if len(datc[datc < 0]) != 0: - print("datc contains negative") + +# removes negative values from an array +def remove_neg(aiaa: np.array, aiab: np.array, aiac: np.array): + global data, datb, datc + data[np.where(data <= 0)] = 0 + datb[np.where(datb <= 0)] = 0 + datc[np.where(datc <= 0)] = 0 + if len(data[data < 0]) != 0: + print("data contains negative") + if len(datb[datb < 0]) != 0: + print("data contains negative") + if len(datc[datc < 0]) != 0: + print("datc contains negative") + remove_neg(im171, im193, im211) -#defines the shape (length) of the array as "s" and the solar radius as "rs" -s=np.shape(data) -rs=heda['rsun'] +# defines the shape (length) of the array as "s" and the solar radius as "rs" +s = np.shape(data) +rs = heda["rsun"] + def pix_arc(aia: np.array): global dattoarc - dattoarc=heda['cdelt1'] + dattoarc = heda["cdelt1"] global conver - conver=((s[0])/2)*dattoarc/hedm['cdelt1']-(s[1]/2) + conver = ((s[0]) / 2) * dattoarc / hedm["cdelt1"] - (s[1] / 2) global convermul - convermul=dattoarc/hedm['cdelt1'] + convermul = dattoarc / hedm["cdelt1"] + pix_arc(im171) -#converts to the Heliographic Stonyhurst coordinate system +# converts to the Heliographic Stonyhurst coordinate system + def to_helio(image: np.array): aia = sunpy.map.Map(image) - adj = 4096/aia.dimensions[0].value - x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj + adj = 4096 / aia.dimensions[0].value + x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj global hpc hpc = aia.pixel_to_world(x, y) global hg hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) global csys - csys=wcs.WCS(hedb) + csys = wcs.WCS(hedb) + to_helio(im171) -#setting up arrays to be used in later processing -#only difference between iarr and bmcool is integer vs. float +# setting up arrays to be used in later processing +# only difference between iarr and bmcool is integer vs. float ident = 1 -iarr = np.zeros((s[0],s[1]),dtype=np.byte) -bmcool=np.zeros((s[0],s[1]),dtype=np.float32) -offarr,slate=np.array(iarr),np.array(iarr) -cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool) -circ=np.zeros((s[0],s[1]),dtype=int) - -#creation of a 2d gaussian for magnetic cut offs -r = (s[1]/2.0)-450 -xgrid,ygrid=np.meshgrid(np.arange(s[0]),np.arange(s[1])) -center=[int(s[1]/2.0),int(s[1]/2.0)] -w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2) -y,x=np.mgrid[0:4096,0:4096] -garr=Gaussian2D(1,s[0]/2,s[1]/2,2000/2.3548,2000/2.3548)(x,y) -#plt.plot(garr) -garr[w]=1.0 - -#creates sub-arrays of props to isolate column of index 0 and column of index 1 -#what is props?? -props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','','' -props[:,1]='num','"','"','H°','"','"','"','"','"','"','"','"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx' -#define threshold values in log s - -with np.errstate(divide = 'ignore'): - t0=np.log10(datc) - t1=np.log10(datb) - t2=np.log10(data) +iarr = np.zeros((s[0], s[1]), dtype=np.byte) +bmcool = np.zeros((s[0], s[1]), dtype=np.float32) +offarr, slate = np.array(iarr), np.array(iarr) +cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) +circ = np.zeros((s[0], s[1]), dtype=int) + +# creation of a 2d gaussian for magnetic cut offs +r = (s[1] / 2.0) - 450 +xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) +center = [int(s[1] / 2.0), int(s[1] / 2.0)] +w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) +y, x = np.mgrid[0:4096, 0:4096] +garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) +# plt.plot(garr) +garr[w] = 1.0 + +# creates sub-arrays of props to isolate column of index 0 and column of index 1 +# what is props?? +props = np.zeros((26, 30), dtype="", + "", + "", + "BMAX", + "BMIN", + "TOT_B+", + "TOT_B-", + "", + "", + "", +) +props[:, 1] = ( + "num", + '"', + '"', + "H°", + '"', + '"', + '"', + '"', + '"', + '"', + '"', + '"', + "H°", + "°", + "Mm^2", + "%", + "G", + "G", + "G", + "G", + "G", + "G", + "G", + "Mx", + "Mx", + "Mx", +) +# define threshold values in log s + +with np.errstate(divide="ignore"): + t0 = np.log10(datc) + t1 = np.log10(datb) + t2 = np.log10(data) + class Bounds: def __init__(self, upper, lower, slope): self.upper = upper self.lower = lower self.slope = slope + def new_u(self, new_upper): self.upper = new_upper + def new_l(self, new_lower): self.lower = new_lower + def new_s(self, new_slope): self.slope = new_slope -t0b = Bounds(.8, 2.7, 255) + +t0b = Bounds(0.8, 2.7, 255) t1b = Bounds(1.4, 3.0, 255) t2b = Bounds(1.2, 3.9, 255) + def threshold(tval: np.array): global t0, t1, t2 if tval.all() == t0.all(): @@ -216,296 +299,388 @@ def threshold(tval: np.array): threshold(t1) threshold(t2) + def set_contour(tval: np.array): global t0, t1, t2 if tval.all() == t0.all(): - t0 = np.array(((t0-t0b.upper)/(t0b.lower-t0b.upper))*t0b.slope,dtype=np.float32) + t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) elif tval.all() == t1.all(): - t1 = np.array(((t1-t1b.upper)/(t1b.lower-t1b.upper))*t1b.slope,dtype=np.float32) + t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) elif tval.all() == t2.all(): - t2 = np.array(((t2-t2b.upper)/(t2b.lower-t2b.upper))*t2b.slope,dtype=np.float32) + t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) + set_contour(t0) set_contour(t1) set_contour(t2) + def create_mask(): global t0, t1, t2, bmmix, bmhot, bmcool - with np.errstate(divide = 'ignore',invalid='ignore'): - bmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1 - bmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1 - bmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1 + with np.errstate(divide="ignore", invalid="ignore"): + bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 + bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 + bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 + create_mask() + def conjunction(): global bmhot, bmcool, bmmix, cand - cand = bmcool*bmmix*bmhot + cand = bmcool * bmmix * bmhot + conjunction() + def misid(): - global s, r, w, circ, cand - r = (s[1]/2.0) - 100 - w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) - circ[w]=1.0 - cand=cand*circ + global s, r, w, circ, cand + r = (s[1] / 2.0) - 100 + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + cand = cand * circ + misid() + def on_off(): global circ, cand - circ[:]=0 - r=(rs/dattoarc)-10 - inside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) - circ[inside]=1.0 - r=(rs/dattoarc)+40 - outside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 >= r**2) - circ[outside]=1.0 - cand=cand*circ + circ[:] = 0 + r = (rs / dattoarc) - 10 + inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[inside] = 1.0 + r = (rs / dattoarc) + 40 + outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) + circ[outside] = 1.0 + cand = cand * circ + on_off() + def contours(): global cand, cont, heir - cand=np.array(cand,dtype=np.uint8) - cont,heir=cv2.findContours(cand,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + cand = np.array(cand, dtype=np.uint8) + cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + contours() + def sort(): global sizes, reord, tmp, cont - sizes=[] + sizes = [] for i in range(len(cont)): - sizes=np.append(sizes,len(cont[i])) - reord=sizes.ravel().argsort()[::-1] - tmp=list(cont) + sizes = np.append(sizes, len(cont[i])) + reord = sizes.ravel().argsort()[::-1] + tmp = list(cont) for i in range(len(cont)): - tmp[i]=cont[reord[i]] - cont=list(tmp) + tmp[i] = cont[reord[i]] + cont = list(tmp) -sort() +sort() -#=====cycles through contours========= +# =====cycles through contours========= for i in range(len(cont)): - - x=np.append(x,len(cont[i])) - -#=====only takes values of minimum surface length and calculates area====== - - if len(cont[i]) <= 100: - continue - area=0.5*np.abs(np.dot(cont[i][:,0,0],np.roll(cont[i][:,0,1],1))-np.dot(cont[i][:,0,1],np.roll(cont[i][:,0,0],1))) - arcar=(area*(dattoarc**2)) - if arcar > 1000: - -#=====finds centroid======= - - chpts=len(cont[i]) - cent=[np.mean(cont[i][:,0,0]),np.mean(cont[i][:,0,1])] - -#===remove quiet sun regions encompassed by coronal holes====== - - if (cand[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0) and (iarr[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) - iarr[np.where(slate == 1)]=0 - slate[:]=0 - - else: - -#====create a simple centre point====== - - arccent=csys.all_pix2world(cent[0],cent[1],0) - -#====classifies off limb CH regions======== - - if (((arccent[0]**2)+(arccent[1]**2)) > (rs**2)) or (np.sum(np.array(csys.all_pix2world(cont[i][0,0,0],cont[i][0,0,1],0))**2) > (rs**2)): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),offarr) - else: - -#=====classifies on disk coronal holes======= - - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) - poslin=np.where(slate == 1) - slate[:]=0 - print(poslin) - -#====create an array for magnetic polarity======== - - pos=np.zeros((len(poslin[0]),2),dtype=np.uint) - pos[:,0]=np.array((poslin[0]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) - pos[:,1]=np.array((poslin[1]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) - npix=list(np.histogram(datm[pos[:,0],pos[:,1]],bins=np.arange(np.round(np.min(datm[pos[:,0],pos[:,1]]))-0.5,np.round(np.max(datm[pos[:,0],pos[:,1]]))+0.6,1))) - npix[0][np.where(npix[0]==0)]=1 - npix[1]=npix[1][:-1]+0.5 - - wh1=np.where(npix[1] > 0) - wh2=np.where(npix[1] < 0) - -#=====magnetic cut offs dependant on area========= - - if np.absolute((np.sum(npix[0][wh1])-np.sum(npix[0][wh2]))/np.sqrt(np.sum(npix[0]))) <= 10 and arcar < 9000: - continue - if np.absolute(np.mean(datm[pos[:,0],pos[:,1]])) < garr[int(cent[0]),int(cent[1])] and arcar < 40000: - continue - iarr[poslin]=ident - -#====create an accurate center point======= - - ypos=np.sum((poslin[0])*np.absolute(hg.lat[poslin]))/np.sum(np.absolute(hg.lat[poslin])) - xpos=np.sum((poslin[1])*np.absolute(hg.lon[poslin]))/np.sum(np.absolute(hg.lon[poslin])) - - arccent=csys.all_pix2world(xpos,ypos,0) - -#======calculate average angle coronal hole is subjected to====== - - dist=np.sqrt((arccent[0]**2)+(arccent[1]**2)) - ang=np.arcsin(dist/rs) - -#=====calculate area of CH with minimal projection effects====== - - trupixar=abs(area/np.cos(ang)) - truarcar=trupixar*(dattoarc**2) - trummar=truarcar*((6.96e+08/rs)**2) - - -#====find CH extent in lattitude and longitude======== - - maxxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] - maxxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] - maxylat=hg.lat[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] - maxylon=hg.lon[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] - minxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] - minxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] - minylat=hg.lat[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] - minylon=hg.lon[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] - -#=====CH centroid in lat/lon======= - - centlat=hg.lat[int(ypos),int(xpos)] - centlon=hg.lon[int(ypos),int(xpos)] - -#====caluclate the mean magnetic field===== - - mB=np.mean(datm[pos[:,0],pos[:,1]]) - mBpos=np.sum(npix[0][wh1]*npix[1][wh1])/np.sum(npix[0][wh1]) - mBneg=np.sum(npix[0][wh2]*npix[1][wh2])/np.sum(npix[0][wh2]) - -#=====finds coordinates of CH boundaries======= - - Ywb,Xwb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0]),0) - Yeb,Xeb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0]),0) - Ynb,Xnb=csys.all_pix2world(np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0],0) - Ysb,Xsb=csys.all_pix2world(np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0],0) - - width=round(maxxlon.value)-round(minxlon.value) - - if minxlon.value >= 0.0 : eastl='W'+str(int(np.round(minxlon.value))) - else : eastl='E'+str(np.absolute(int(np.round(minxlon.value)))) - if maxxlon.value >= 0.0 : westl='W'+str(int(np.round(maxxlon.value))) - else : westl='E'+str(np.absolute(int(np.round(maxxlon.value)))) - - if centlat >= 0.0 : centlat='N'+str(int(np.round(centlat.value))) - else : centlat='S'+str(np.absolute(int(np.round(centlat.value)))) - if centlon >= 0.0 : centlon='W'+str(int(np.round(centlon.value))) - else : centlon='E'+str(np.absolute(int(np.round(centlon.value)))) - -#====insertions of CH properties into property array===== - - props[0,ident+1]=str(ident) - props[1,ident+1]=str(np.round(arccent[0])) - props[2,ident+1]=str(np.round(arccent[1])) - props[3,ident+1]=str(centlon+centlat) - props[4,ident+1]=str(np.round(Xeb)) - props[5,ident+1]=str(np.round(Yeb)) - props[6,ident+1]=str(np.round(Xwb)) - props[7,ident+1]=str(np.round(Ywb)) - props[8,ident+1]=str(np.round(Xnb)) - props[9,ident+1]=str(np.round(Ynb)) - props[10,ident+1]=str(np.round(Xsb)) - props[11,ident+1]=str(np.round(Ysb)) - props[12,ident+1]=str(eastl+'-'+westl) - props[13,ident+1]=str(width) - props[14,ident+1]='{:.1e}'.format(trummar/1e+12) - props[15,ident+1]=str(np.round((arcar*100/(np.pi*(rs**2))),1)) - props[16,ident+1]=str(np.round(mB,1)) - props[17,ident+1]=str(np.round(mBpos,1)) - props[18,ident+1]=str(np.round(mBneg,1)) - props[19,ident+1]=str(np.round(np.max(npix[1]),1)) - props[20,ident+1]=str(np.round(np.min(npix[1]),1)) - tbpos= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] > 0)]) - props[21,ident+1]='{:.1e}'.format(tbpos) - tbneg= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] < 0)]) - props[22,ident+1]='{:.1e}'.format(tbneg) - props[23,ident+1]='{:.1e}'.format(mB*trummar*1e+16) - props[24,ident+1]='{:.1e}'.format(mBpos*trummar*1e+16) - props[25,ident+1]='{:.1e}'.format(mBneg*trummar*1e+16) - -#=====sets up code for next possible coronal hole===== - - ident=ident+1 - -#=====sets ident back to max value of iarr====== - -ident=ident-1 -np.savetxt('ch_summary.txt', props, fmt = '%s') + x = np.append(x, len(cont[i])) + + # =====only takes values of minimum surface length and calculates area====== + + if len(cont[i]) <= 100: + continue + area = 0.5 * np.abs( + np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) + - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) + ) + arcar = area * (dattoarc**2) + if arcar > 1000: + # =====finds centroid======= + + chpts = len(cont[i]) + cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] + + # ===remove quiet sun regions encompassed by coronal holes====== + + if ( + cand[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 + + else: + # ====create a simple centre point====== + + arccent = csys.all_pix2world(cent[0], cent[1], 0) + + # ====classifies off limb CH regions======== + + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + else: + # =====classifies on disk coronal holes======= + + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + poslin = np.where(slate == 1) + slate[:] = 0 + print(poslin) + + # ====create an array for magnetic polarity======== + + pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) + pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + npix = list( + np.histogram( + datm[pos[:, 0], pos[:, 1]], + bins=np.arange( + np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, + np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, + 1, + ), + ) + ) + npix[0][np.where(npix[0] == 0)] = 1 + npix[1] = npix[1][:-1] + 0.5 + + wh1 = np.where(npix[1] > 0) + wh2 = np.where(npix[1] < 0) + + # =====magnetic cut offs dependant on area========= + + if ( + np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) + <= 10 + and arcar < 9000 + ): + continue + if ( + np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] + and arcar < 40000 + ): + continue + iarr[poslin] = ident + + # ====create an accurate center point======= + + ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) + xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) + + arccent = csys.all_pix2world(xpos, ypos, 0) + + # ======calculate average angle coronal hole is subjected to====== + + dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) + ang = np.arcsin(dist / rs) + + # =====calculate area of CH with minimal projection effects====== + + trupixar = abs(area / np.cos(ang)) + truarcar = trupixar * (dattoarc**2) + trummar = truarcar * ((6.96e08 / rs) ** 2) + + # ====find CH extent in lattitude and longitude======== + + maxxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxylat = hg.lat[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + maxylon = hg.lon[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minylat = hg.lat[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minylon = hg.lon[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + + # =====CH centroid in lat/lon======= + + centlat = hg.lat[int(ypos), int(xpos)] + centlon = hg.lon[int(ypos), int(xpos)] + + # ====caluclate the mean magnetic field===== + + mB = np.mean(datm[pos[:, 0], pos[:, 1]]) + mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) + mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) + + # =====finds coordinates of CH boundaries======= + + Ywb, Xwb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + 0, + ) + Yeb, Xeb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + 0, + ) + Ynb, Xnb = csys.all_pix2world( + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + Ysb, Xsb = csys.all_pix2world( + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + + width = round(maxxlon.value) - round(minxlon.value) + + if minxlon.value >= 0.0: + eastl = "W" + str(int(np.round(minxlon.value))) + else: + eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0: + westl = "W" + str(int(np.round(maxxlon.value))) + else: + westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0: + centlat = "N" + str(int(np.round(centlat.value))) + else: + centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0: + centlon = "W" + str(int(np.round(centlon.value))) + else: + centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) + + # ====insertions of CH properties into property array===== + + props[0, ident + 1] = str(ident) + props[1, ident + 1] = str(np.round(arccent[0])) + props[2, ident + 1] = str(np.round(arccent[1])) + props[3, ident + 1] = str(centlon + centlat) + props[4, ident + 1] = str(np.round(Xeb)) + props[5, ident + 1] = str(np.round(Yeb)) + props[6, ident + 1] = str(np.round(Xwb)) + props[7, ident + 1] = str(np.round(Ywb)) + props[8, ident + 1] = str(np.round(Xnb)) + props[9, ident + 1] = str(np.round(Ynb)) + props[10, ident + 1] = str(np.round(Xsb)) + props[11, ident + 1] = str(np.round(Ysb)) + props[12, ident + 1] = str(eastl + "-" + westl) + props[13, ident + 1] = str(width) + props[14, ident + 1] = f"{trummar/1e+12:.1e}" + props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) + props[16, ident + 1] = str(np.round(mB, 1)) + props[17, ident + 1] = str(np.round(mBpos, 1)) + props[18, ident + 1] = str(np.round(mBneg, 1)) + props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) + props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) + tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) + props[21, ident + 1] = f"{tbpos:.1e}" + tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) + props[22, ident + 1] = f"{tbneg:.1e}" + props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" + props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" + props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" + + # =====sets up code for next possible coronal hole===== + + ident = ident + 1 + +# =====sets ident back to max value of iarr====== + +ident = ident - 1 +np.savetxt("ch_summary.txt", props, fmt="%s") from skimage.util import img_as_ubyte + def rescale01(arr, cmin=None, cmax=None, a=0, b=1): if cmin or cmax: arr = np.clip(arr, cmin, cmax) - return (b-a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a def plot_tricolor(): - tricolorarray = np.zeros((4096, 4096, 3)) + tricolorarray = np.zeros((4096, 4096, 3)) - data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9)) - data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0)) - data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7)) + data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) + data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) + data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) - tricolorarray[..., 0] = data_c/np.max(data_c) - tricolorarray[..., 1] = data_b/np.max(data_b) - tricolorarray[..., 2] = data_a/np.max(data_a) + tricolorarray[..., 0] = data_c / np.max(data_c) + tricolorarray[..., 1] = data_b / np.max(data_b) + tricolorarray[..., 2] = data_a / np.max(data_a) + fig, ax = plt.subplots(figsize=(10, 10)) - fig, ax = plt.subplots(figsize = (10, 10)) + plt.imshow(tricolorarray, origin="lower") # , extent = ) + cs = plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) + plt.savefig("tricolor.png") + plt.close() - plt.imshow(tricolorarray, origin = 'lower')#, extent = ) - cs=plt.contour(xgrid,ygrid,slate,colors='white',linewidths=0.5) - plt.savefig('tricolor.png') - plt.close() def plot_mask(slate=slate): - chs=np.where(iarr > 0) - slate[chs]=1 - slate=np.array(slate,dtype=np.uint8) - cont,heir=cv2.findContours(slate,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - - circ[:]=0 - r=(rs/dattoarc) - w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) - circ[w]=1.0 - - plt.figure(figsize=(10,10)) - plt.xlim(143,4014) - plt.ylim(143,4014) - plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2) - plt.gca().set_aspect('equal', adjustable='box') - plt.axis('off') - cs=plt.contour(xgrid,ygrid,slate,colors='black',linewidths=0.5) - cs=plt.contour(xgrid,ygrid,circ,colors='black',linewidths=1.0) - - plt.savefig('CH_mask_'+hedb["DATE"]+'.png',transparent=True) - #plt.close() -#====stores all CH properties in a text file===== + chs = np.where(iarr > 0) + slate[chs] = 1 + slate = np.array(slate, dtype=np.uint8) + cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + circ[:] = 0 + r = rs / dattoarc + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + + plt.figure(figsize=(10, 10)) + plt.xlim(143, 4014) + plt.ylim(143, 4014) + plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) + plt.gca().set_aspect("equal", adjustable="box") + plt.axis("off") + cs = plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) + cs = plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) + + plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) + # plt.close() + + +# ====stores all CH properties in a text file===== plot_tricolor() plot_mask() -#====EOF==== +# ====EOF==== diff --git a/CHIMERA_V3.py b/CHIMERA_V3.py index ae40a50..48b0111 100644 --- a/CHIMERA_V3.py +++ b/CHIMERA_V3.py @@ -1,13 +1,9 @@ -#import required libraries -import astropy -from astropy import wcs -from astropy.io import fits -from astropy.modeling.models import Gaussian2D +# import required libraries +import glob +import sys + import astropy.units as u -from astropy.utils.data import download_file -from astropy.visualization import astropy_mpl_style import cv2 -import glob import mahotas import matplotlib.pyplot as plt import numpy as np @@ -15,71 +11,75 @@ import scipy.interpolate import sunpy import sunpy.map -import sys -from scipy.interpolate import interp2d, RectBivariateSpline +from astropy import wcs +from astropy.io import fits +from astropy.modeling.models import Gaussian2D +from astropy.visualization import astropy_mpl_style plt.style.use(astropy_mpl_style) # loading in the images as fits files -im171 = glob.glob('171.fts') -im193 = glob.glob('193.fts') -im211 = glob.glob('211.fts') -imhmi = glob.glob('hmi.fts') +im171 = glob.glob("171.fts") +im193 = glob.glob("193.fts") +im211 = glob.glob("211.fts") +imhmi = glob.glob("hmi.fts") -#ensure that all images are present +# ensure that all images are present if im171 == [] or im193 == [] or im211 == [] or imhmi == []: - print("Not all required files present") - sys.exit() - -#Two functions that rescale the aia and hmi images from any original size to any final size + print("Not all required files present") + sys.exit() + +# Two functions that rescale the aia and hmi images from any original size to any final size + +# didn't normalize by exposure time for hmi because it was equal to 0 -#didn't normalize by exposure time for hmi because it was equal to 0 def rescale_aia(image: np.array, orig_res: int, desired_res: int): - hed =fits.getheader(image[0], 0) - dat = fits.getdata(image[0], 0)/(hed["EXPTIME"]) - if desired_res > orig_res: - scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) - elif desired_res < orig_res: - scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) + hed = fits.getheader(image[0], 0) + dat = fits.getdata(image[0], 0) / (hed["EXPTIME"]) + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) def rescale_hmi(image: np.array, orig_res: int, desired_res: int): - hdu_number = 0 - hed = fits.getheader(image[0],hdu_number) - dat= fits.getdata(image[0], ext=0) - if desired_res > orig_res: - scaled_array=np.linspace(start = 0, stop = desired_res, num = orig_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) - elif desired_res < orig_res: - scaled_array=np.linspace(start = 0, stop = orig_res, num = desired_res) - dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,dat) - if len(dn(np.arange(0, desired_res),np.arange(0,desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0,desired_res),np.arange(0,desired_res)) - -#defining data and headers which are used in later steps + hdu_number = 0 + hed = fits.getheader(image[0], hdu_number) + dat = fits.getdata(image[0], ext=0) + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) + if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: + print("Incorrect image resolution") + sys.exit() + else: + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + + +# defining data and headers which are used in later steps hdu_number = 0 data = rescale_aia(im171, 1024, 4096) @@ -87,118 +87,201 @@ def rescale_hmi(image: np.array, orig_res: int, desired_res: int): datc = rescale_aia(im211, 1024, 4096) datm = rescale_hmi(imhmi, 1024, 4096) -#rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 -#ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively -#rotates array if 'crota1' is greater than 90 degrees + +# rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 +# ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively +# rotates array if 'crota1' is greater than 90 degrees def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): - global heda, hedb, hedc, hedm - heda = fits.getheader(aiaa[0],0) - hedb = fits.getheader(aiab[0],0) - hedc = fits.getheader(aiac[0],0) - hedm = fits.getheader(aiam[0],0) - if hedb["ctype1"] != 'solar_x ': - hedb["ctype1"]='solar_x ' - hedb["ctype2"]='solar_y ' - if heda['cdelt1'] > 1: - heda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0 - hedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0 - hedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0 - if hedm['crota1'] > 90: - datm=np.rot90(np.rot90(datm)) + global heda, hedb, hedc, hedm + heda = fits.getheader(aiaa[0], 0) + hedb = fits.getheader(aiab[0], 0) + hedc = fits.getheader(aiac[0], 0) + hedm = fits.getheader(aiam[0], 0) + if hedb["ctype1"] != "solar_x ": + hedb["ctype1"] = "solar_x " + hedb["ctype2"] = "solar_y " + if heda["cdelt1"] > 1: + heda["cdelt1"], heda["cdelt2"], heda["crpix1"], heda["crpix2"] = ( + heda["cdelt1"] / 4.0, + heda["cdelt2"] / 4.0, + heda["crpix1"] * 4.0, + heda["crpix2"] * 4.0, + ) + hedb["cdelt1"], hedb["cdelt2"], hedb["crpix1"], hedb["crpix2"] = ( + hedb["cdelt1"] / 4.0, + hedb["cdelt2"] / 4.0, + hedb["crpix1"] * 4.0, + hedb["crpix2"] * 4.0, + ) + hedc["cdelt1"], hedc["cdelt2"], hedc["crpix1"], hedc["crpix2"] = ( + hedc["cdelt1"] / 4.0, + hedc["cdelt2"] / 4.0, + hedc["crpix1"] * 4.0, + hedc["crpix2"] * 4.0, + ) + if hedm["crota1"] > 90: + datm = np.rot90(np.rot90(datm)) + filter(im171, im193, im211, imhmi) -#removes negative values from an array -def remove_neg(aiaa: np.array, aiab:np.array, aiac: np.array): - global data, datb, datc - data[np.where(data <= 0)] = 0 - datb[np.where(datb <= 0)] = 0 - datc[np.where(datc <= 0)] = 0 - if len(data[data < 0]) != 0: - print("data contains negative") - if len(datb[datb < 0]) != 0: - print("data contains negative") - if len(datc[datc < 0]) != 0: - print("datc contains negative") + +# removes negative values from an array +def remove_neg(aiaa: np.array, aiab: np.array, aiac: np.array): + global data, datb, datc + data[np.where(data <= 0)] = 0 + datb[np.where(datb <= 0)] = 0 + datc[np.where(datc <= 0)] = 0 + if len(data[data < 0]) != 0: + print("data contains negative") + if len(datb[datb < 0]) != 0: + print("data contains negative") + if len(datc[datc < 0]) != 0: + print("datc contains negative") + remove_neg(im171, im193, im211) -#defines the shape (length) of the array as "s" and the solar radius as "rs" -s=np.shape(data) -rs=heda['rsun'] +# defines the shape (length) of the array as "s" and the solar radius as "rs" +s = np.shape(data) +rs = heda["rsun"] + def pix_arc(aia: np.array): global dattoarc - dattoarc=heda['cdelt1'] + dattoarc = heda["cdelt1"] global conver - conver=((s[0])/2)*dattoarc/hedm['cdelt1']-(s[1]/2) + conver = ((s[0]) / 2) * dattoarc / hedm["cdelt1"] - (s[1] / 2) global convermul - convermul=dattoarc/hedm['cdelt1'] + convermul = dattoarc / hedm["cdelt1"] + pix_arc(im171) -#converts to the Heliographic Stonyhurst coordinate system +# converts to the Heliographic Stonyhurst coordinate system + def to_helio(image: np.array): aia = sunpy.map.Map(image) - adj = 4096/aia.dimensions[0].value - x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj + adj = 4096 / aia.dimensions[0].value + x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj global hpc hpc = aia.pixel_to_world(x, y) global hg hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) global csys - csys=wcs.WCS(hedb) + csys = wcs.WCS(hedb) + to_helio(im171) -#setting up arrays to be used in later processing -#only difference between iarr and bmcool is integer vs. float +# setting up arrays to be used in later processing +# only difference between iarr and bmcool is integer vs. float ident = 1 -iarr = np.zeros((s[0],s[1]),dtype=np.byte) -bmcool=np.zeros((s[0],s[1]),dtype=np.float32) -offarr,slate=np.array(iarr),np.array(iarr) -cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool) -circ=np.zeros((s[0],s[1]),dtype=int) - -#creation of a 2d gaussian for magnetic cut offs -r = (s[1]/2.0)-450 -xgrid,ygrid=np.meshgrid(np.arange(s[0]),np.arange(s[1])) -center=[int(s[1]/2.0),int(s[1]/2.0)] -w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2) -y,x=np.mgrid[0:4096,0:4096] -garr=Gaussian2D(1,s[0]/2,s[1]/2,2000/2.3548,2000/2.3548)(x,y) -#plt.plot(garr) -garr[w]=1.0 - -#creates sub-arrays of props to isolate column of index 0 and column of index 1 -#what is props?? -props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','','' -props[:,1]='num','"','"','H°','"','"','"','"','"','"','"','"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx' -#define threshold values in log s - -with np.errstate(divide = 'ignore'): - t0=np.log10(datc) - t1=np.log10(datb) - t2=np.log10(data) +iarr = np.zeros((s[0], s[1]), dtype=np.byte) +bmcool = np.zeros((s[0], s[1]), dtype=np.float32) +offarr, slate = np.array(iarr), np.array(iarr) +cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) +circ = np.zeros((s[0], s[1]), dtype=int) + +# creation of a 2d gaussian for magnetic cut offs +r = (s[1] / 2.0) - 450 +xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) +center = [int(s[1] / 2.0), int(s[1] / 2.0)] +w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) +y, x = np.mgrid[0:4096, 0:4096] +garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) +# plt.plot(garr) +garr[w] = 1.0 + +# creates sub-arrays of props to isolate column of index 0 and column of index 1 +# what is props?? +props = np.zeros((26, 30), dtype="", + "", + "", + "BMAX", + "BMIN", + "TOT_B+", + "TOT_B-", + "", + "", + "", +) +props[:, 1] = ( + "num", + '"', + '"', + "H°", + '"', + '"', + '"', + '"', + '"', + '"', + '"', + '"', + "H°", + "°", + "Mm^2", + "%", + "G", + "G", + "G", + "G", + "G", + "G", + "G", + "Mx", + "Mx", + "Mx", +) +# define threshold values in log s + +with np.errstate(divide="ignore"): + t0 = np.log10(datc) + t1 = np.log10(datb) + t2 = np.log10(data) + class Bounds: def __init__(self, upper, lower, slope): self.upper = upper self.lower = lower self.slope = slope + def new_u(self, new_upper): self.upper = new_upper + def new_l(self, new_lower): self.lower = new_lower + def new_s(self, new_slope): self.slope = new_slope -t0b = Bounds(.8, 2.7, 255) + +t0b = Bounds(0.8, 2.7, 255) t1b = Bounds(1.4, 3.0, 255) t2b = Bounds(1.2, 3.9, 255) + def threshold(tval: np.array): global t0, t1, t2 if tval.all() == t0.all(): @@ -216,296 +299,388 @@ def threshold(tval: np.array): threshold(t1) threshold(t2) + def set_contour(tval: np.array): global t0, t1, t2 if tval.all() == t0.all(): - t0 = np.array(((t0-t0b.upper)/(t0b.lower-t0b.upper))*t0b.slope,dtype=np.float32) + t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) elif tval.all() == t1.all(): - t1 = np.array(((t1-t1b.upper)/(t1b.lower-t1b.upper))*t1b.slope,dtype=np.float32) + t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) elif tval.all() == t2.all(): - t2 = np.array(((t2-t2b.upper)/(t2b.lower-t2b.upper))*t2b.slope,dtype=np.float32) + t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) + set_contour(t0) set_contour(t1) set_contour(t2) + def create_mask(): global t0, t1, t2, bmmix, bmhot, bmcool - with np.errstate(divide = 'ignore',invalid='ignore'): - bmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1 - bmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1 - bmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1 + with np.errstate(divide="ignore", invalid="ignore"): + bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 + bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 + bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 + create_mask() + def conjunction(): global bmhot, bmcool, bmmix, cand - cand = bmcool*bmmix*bmhot + cand = bmcool * bmmix * bmhot + conjunction() + def misid(): - global s, r, w, circ, cand - r = (s[1]/2.0) - 100 - w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) - circ[w]=1.0 - cand=cand*circ + global s, r, w, circ, cand + r = (s[1] / 2.0) - 100 + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + cand = cand * circ + misid() + def on_off(): global circ, cand - circ[:]=0 - r=(rs/dattoarc)-10 - inside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) - circ[inside]=1.0 - r=(rs/dattoarc)+40 - outside=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 >= r**2) - circ[outside]=1.0 - cand=cand*circ + circ[:] = 0 + r = (rs / dattoarc) - 10 + inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[inside] = 1.0 + r = (rs / dattoarc) + 40 + outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) + circ[outside] = 1.0 + cand = cand * circ + on_off() + def contours(): global cand, cont, heir - cand=np.array(cand,dtype=np.uint8) - cont,heir=cv2.findContours(cand,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + cand = np.array(cand, dtype=np.uint8) + cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + contours() + def sort(): global sizes, reord, tmp, cont - sizes=[] + sizes = [] for i in range(len(cont)): - sizes=np.append(sizes,len(cont[i])) - reord=sizes.ravel().argsort()[::-1] - tmp=list(cont) + sizes = np.append(sizes, len(cont[i])) + reord = sizes.ravel().argsort()[::-1] + tmp = list(cont) for i in range(len(cont)): - tmp[i]=cont[reord[i]] - cont=list(tmp) + tmp[i] = cont[reord[i]] + cont = list(tmp) -sort() +sort() -#=====cycles through contours========= +# =====cycles through contours========= for i in range(len(cont)): - - x=np.append(x,len(cont[i])) - -#=====only takes values of minimum surface length and calculates area====== - - if len(cont[i]) <= 100: - continue - area=0.5*np.abs(np.dot(cont[i][:,0,0],np.roll(cont[i][:,0,1],1))-np.dot(cont[i][:,0,1],np.roll(cont[i][:,0,0],1))) - arcar=(area*(dattoarc**2)) - if arcar > 1000: - -#=====finds centroid======= - - chpts=len(cont[i]) - cent=[np.mean(cont[i][:,0,0]),np.mean(cont[i][:,0,1])] - -#===remove quiet sun regions encompassed by coronal holes====== - - if (cand[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0) and (iarr[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) - iarr[np.where(slate == 1)]=0 - slate[:]=0 - - else: - -#====create a simple centre point====== - - arccent=csys.all_pix2world(cent[0],cent[1],0) - -#====classifies off limb CH regions======== - - if (((arccent[0]**2)+(arccent[1]**2)) > (rs**2)) or (np.sum(np.array(csys.all_pix2world(cont[i][0,0,0],cont[i][0,0,1],0))**2) > (rs**2)): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),offarr) - else: - -#=====classifies on disk coronal holes======= - - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate) - poslin=np.where(slate == 1) - slate[:]=0 - print(poslin) - -#====create an array for magnetic polarity======== - - pos=np.zeros((len(poslin[0]),2),dtype=np.uint) - pos[:,0]=np.array((poslin[0]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) - pos[:,1]=np.array((poslin[1]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint) - npix=list(np.histogram(datm[pos[:,0],pos[:,1]],bins=np.arange(np.round(np.min(datm[pos[:,0],pos[:,1]]))-0.5,np.round(np.max(datm[pos[:,0],pos[:,1]]))+0.6,1))) - npix[0][np.where(npix[0]==0)]=1 - npix[1]=npix[1][:-1]+0.5 - - wh1=np.where(npix[1] > 0) - wh2=np.where(npix[1] < 0) - -#=====magnetic cut offs dependant on area========= - - if np.absolute((np.sum(npix[0][wh1])-np.sum(npix[0][wh2]))/np.sqrt(np.sum(npix[0]))) <= 10 and arcar < 9000: - continue - if np.absolute(np.mean(datm[pos[:,0],pos[:,1]])) < garr[int(cent[0]),int(cent[1])] and arcar < 40000: - continue - iarr[poslin]=ident - -#====create an accurate center point======= - - ypos=np.sum((poslin[0])*np.absolute(hg.lat[poslin]))/np.sum(np.absolute(hg.lat[poslin])) - xpos=np.sum((poslin[1])*np.absolute(hg.lon[poslin]))/np.sum(np.absolute(hg.lon[poslin])) - - arccent=csys.all_pix2world(xpos,ypos,0) - -#======calculate average angle coronal hole is subjected to====== - - dist=np.sqrt((arccent[0]**2)+(arccent[1]**2)) - ang=np.arcsin(dist/rs) - -#=====calculate area of CH with minimal projection effects====== - - trupixar=abs(area/np.cos(ang)) - truarcar=trupixar*(dattoarc**2) - trummar=truarcar*((6.96e+08/rs)**2) - - -#====find CH extent in lattitude and longitude======== - - maxxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] - maxxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])] - maxylat=hg.lat[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] - maxylon=hg.lon[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]] - minxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] - minxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])] - minylat=hg.lat[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] - minylon=hg.lon[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]] - -#=====CH centroid in lat/lon======= - - centlat=hg.lat[int(ypos),int(xpos)] - centlon=hg.lon[int(ypos),int(xpos)] - -#====caluclate the mean magnetic field===== - - mB=np.mean(datm[pos[:,0],pos[:,1]]) - mBpos=np.sum(npix[0][wh1]*npix[1][wh1])/np.sum(npix[0][wh1]) - mBneg=np.sum(npix[0][wh2]*npix[1][wh2])/np.sum(npix[0][wh2]) - -#=====finds coordinates of CH boundaries======= - - Ywb,Xwb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0]),0) - Yeb,Xeb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0]),0) - Ynb,Xnb=csys.all_pix2world(np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0],0) - Ysb,Xsb=csys.all_pix2world(np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0],0) - - width=round(maxxlon.value)-round(minxlon.value) - - if minxlon.value >= 0.0 : eastl='W'+str(int(np.round(minxlon.value))) - else : eastl='E'+str(np.absolute(int(np.round(minxlon.value)))) - if maxxlon.value >= 0.0 : westl='W'+str(int(np.round(maxxlon.value))) - else : westl='E'+str(np.absolute(int(np.round(maxxlon.value)))) - - if centlat >= 0.0 : centlat='N'+str(int(np.round(centlat.value))) - else : centlat='S'+str(np.absolute(int(np.round(centlat.value)))) - if centlon >= 0.0 : centlon='W'+str(int(np.round(centlon.value))) - else : centlon='E'+str(np.absolute(int(np.round(centlon.value)))) - -#====insertions of CH properties into property array===== - - props[0,ident+1]=str(ident) - props[1,ident+1]=str(np.round(arccent[0])) - props[2,ident+1]=str(np.round(arccent[1])) - props[3,ident+1]=str(centlon+centlat) - props[4,ident+1]=str(np.round(Xeb)) - props[5,ident+1]=str(np.round(Yeb)) - props[6,ident+1]=str(np.round(Xwb)) - props[7,ident+1]=str(np.round(Ywb)) - props[8,ident+1]=str(np.round(Xnb)) - props[9,ident+1]=str(np.round(Ynb)) - props[10,ident+1]=str(np.round(Xsb)) - props[11,ident+1]=str(np.round(Ysb)) - props[12,ident+1]=str(eastl+'-'+westl) - props[13,ident+1]=str(width) - props[14,ident+1]='{:.1e}'.format(trummar/1e+12) - props[15,ident+1]=str(np.round((arcar*100/(np.pi*(rs**2))),1)) - props[16,ident+1]=str(np.round(mB,1)) - props[17,ident+1]=str(np.round(mBpos,1)) - props[18,ident+1]=str(np.round(mBneg,1)) - props[19,ident+1]=str(np.round(np.max(npix[1]),1)) - props[20,ident+1]=str(np.round(np.min(npix[1]),1)) - tbpos= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] > 0)]) - props[21,ident+1]='{:.1e}'.format(tbpos) - tbneg= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] < 0)]) - props[22,ident+1]='{:.1e}'.format(tbneg) - props[23,ident+1]='{:.1e}'.format(mB*trummar*1e+16) - props[24,ident+1]='{:.1e}'.format(mBpos*trummar*1e+16) - props[25,ident+1]='{:.1e}'.format(mBneg*trummar*1e+16) - -#=====sets up code for next possible coronal hole===== - - ident=ident+1 - -#=====sets ident back to max value of iarr====== - -ident=ident-1 -np.savetxt('ch_summary.txt', props, fmt = '%s') + x = np.append(x, len(cont[i])) + + # =====only takes values of minimum surface length and calculates area====== + + if len(cont[i]) <= 100: + continue + area = 0.5 * np.abs( + np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) + - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) + ) + arcar = area * (dattoarc**2) + if arcar > 1000: + # =====finds centroid======= + + chpts = len(cont[i]) + cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] + + # ===remove quiet sun regions encompassed by coronal holes====== + + if ( + cand[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 + + else: + # ====create a simple centre point====== + + arccent = csys.all_pix2world(cent[0], cent[1], 0) + + # ====classifies off limb CH regions======== + + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + else: + # =====classifies on disk coronal holes======= + + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + poslin = np.where(slate == 1) + slate[:] = 0 + print(poslin) + + # ====create an array for magnetic polarity======== + + pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) + pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + npix = list( + np.histogram( + datm[pos[:, 0], pos[:, 1]], + bins=np.arange( + np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, + np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, + 1, + ), + ) + ) + npix[0][np.where(npix[0] == 0)] = 1 + npix[1] = npix[1][:-1] + 0.5 + + wh1 = np.where(npix[1] > 0) + wh2 = np.where(npix[1] < 0) + + # =====magnetic cut offs dependant on area========= + + if ( + np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) + <= 10 + and arcar < 9000 + ): + continue + if ( + np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] + and arcar < 40000 + ): + continue + iarr[poslin] = ident + + # ====create an accurate center point======= + + ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) + xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) + + arccent = csys.all_pix2world(xpos, ypos, 0) + + # ======calculate average angle coronal hole is subjected to====== + + dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) + ang = np.arcsin(dist / rs) + + # =====calculate area of CH with minimal projection effects====== + + trupixar = abs(area / np.cos(ang)) + truarcar = trupixar * (dattoarc**2) + trummar = truarcar * ((6.96e08 / rs) ** 2) + + # ====find CH extent in lattitude and longitude======== + + maxxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxylat = hg.lat[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + maxylon = hg.lon[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minylat = hg.lat[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minylon = hg.lon[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + + # =====CH centroid in lat/lon======= + + centlat = hg.lat[int(ypos), int(xpos)] + centlon = hg.lon[int(ypos), int(xpos)] + + # ====caluclate the mean magnetic field===== + + mB = np.mean(datm[pos[:, 0], pos[:, 1]]) + mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) + mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) + + # =====finds coordinates of CH boundaries======= + + Ywb, Xwb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + 0, + ) + Yeb, Xeb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + 0, + ) + Ynb, Xnb = csys.all_pix2world( + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + Ysb, Xsb = csys.all_pix2world( + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + + width = round(maxxlon.value) - round(minxlon.value) + + if minxlon.value >= 0.0: + eastl = "W" + str(int(np.round(minxlon.value))) + else: + eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0: + westl = "W" + str(int(np.round(maxxlon.value))) + else: + westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0: + centlat = "N" + str(int(np.round(centlat.value))) + else: + centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0: + centlon = "W" + str(int(np.round(centlon.value))) + else: + centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) + + # ====insertions of CH properties into property array===== + + props[0, ident + 1] = str(ident) + props[1, ident + 1] = str(np.round(arccent[0])) + props[2, ident + 1] = str(np.round(arccent[1])) + props[3, ident + 1] = str(centlon + centlat) + props[4, ident + 1] = str(np.round(Xeb)) + props[5, ident + 1] = str(np.round(Yeb)) + props[6, ident + 1] = str(np.round(Xwb)) + props[7, ident + 1] = str(np.round(Ywb)) + props[8, ident + 1] = str(np.round(Xnb)) + props[9, ident + 1] = str(np.round(Ynb)) + props[10, ident + 1] = str(np.round(Xsb)) + props[11, ident + 1] = str(np.round(Ysb)) + props[12, ident + 1] = str(eastl + "-" + westl) + props[13, ident + 1] = str(width) + props[14, ident + 1] = f"{trummar/1e+12:.1e}" + props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) + props[16, ident + 1] = str(np.round(mB, 1)) + props[17, ident + 1] = str(np.round(mBpos, 1)) + props[18, ident + 1] = str(np.round(mBneg, 1)) + props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) + props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) + tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) + props[21, ident + 1] = f"{tbpos:.1e}" + tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) + props[22, ident + 1] = f"{tbneg:.1e}" + props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" + props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" + props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" + + # =====sets up code for next possible coronal hole===== + + ident = ident + 1 + +# =====sets ident back to max value of iarr====== + +ident = ident - 1 +np.savetxt("ch_summary.txt", props, fmt="%s") from skimage.util import img_as_ubyte + def rescale01(arr, cmin=None, cmax=None, a=0, b=1): if cmin or cmax: arr = np.clip(arr, cmin, cmax) - return (b-a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a def plot_tricolor(): - tricolorarray = np.zeros((4096, 4096, 3)) + tricolorarray = np.zeros((4096, 4096, 3)) - data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9)) - data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0)) - data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7)) + data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) + data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) + data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) - tricolorarray[..., 0] = data_c/np.max(data_c) - tricolorarray[..., 1] = data_b/np.max(data_b) - tricolorarray[..., 2] = data_a/np.max(data_a) + tricolorarray[..., 0] = data_c / np.max(data_c) + tricolorarray[..., 1] = data_b / np.max(data_b) + tricolorarray[..., 2] = data_a / np.max(data_a) + fig, ax = plt.subplots(figsize=(10, 10)) - fig, ax = plt.subplots(figsize = (10, 10)) + plt.imshow(tricolorarray, origin="lower") # , extent = ) + cs = plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) + plt.savefig("tricolor.png") + plt.close() - plt.imshow(tricolorarray, origin = 'lower')#, extent = ) - cs=plt.contour(xgrid,ygrid,slate,colors='white',linewidths=0.5) - plt.savefig('tricolor.png') - plt.close() def plot_mask(slate=slate): - chs=np.where(iarr > 0) - slate[chs]=1 - slate=np.array(slate,dtype=np.uint8) - cont,heir=cv2.findContours(slate,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - - circ[:]=0 - r=(rs/dattoarc) - w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2) - circ[w]=1.0 - - plt.figure(figsize=(10,10)) - plt.xlim(143,4014) - plt.ylim(143,4014) - plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2) - plt.gca().set_aspect('equal', adjustable='box') - plt.axis('off') - cs=plt.contour(xgrid,ygrid,slate,colors='black',linewidths=0.5) - cs=plt.contour(xgrid,ygrid,circ,colors='black',linewidths=1.0) - - plt.savefig('CH_mask_'+hedb["DATE"]+'.png',transparent=True) - #plt.close() -#====stores all CH properties in a text file===== + chs = np.where(iarr > 0) + slate[chs] = 1 + slate = np.array(slate, dtype=np.uint8) + cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + circ[:] = 0 + r = rs / dattoarc + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + + plt.figure(figsize=(10, 10)) + plt.xlim(143, 4014) + plt.ylim(143, 4014) + plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) + plt.gca().set_aspect("equal", adjustable="box") + plt.axis("off") + cs = plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) + cs = plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) + + plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) + # plt.close() + + +# ====stores all CH properties in a text file===== plot_tricolor() plot_mask() -#====EOF==== +# ====EOF==== From bd1f3eaad0e0294065f94751a34faf0b87ee7e31 Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:57:31 -0400 Subject: [PATCH 05/10] adding new chimera file --- chimera.py | 994 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 994 insertions(+) create mode 100644 chimera.py diff --git a/chimera.py b/chimera.py new file mode 100644 index 0000000..646a6e9 --- /dev/null +++ b/chimera.py @@ -0,0 +1,994 @@ +"""Package for Coronal Hole Identification Algorithm""" +import glob +import sys + +import astropy.units as u +import cv2 +import mahotas +import matplotlib.pyplot as plt +import numpy as np +import scipy +import scipy.interpolate +import sunpy +import sunpy.map +from astropy import wcs +from astropy.io import fits +from astropy.modeling.models import Gaussian2D +from astropy.visualization import astropy_mpl_style +from skimage.util import img_as_ubyte + +plt.style.use(astropy_mpl_style) + +"""loading in the images as fits files""" + +file_path = "./" + +im171 = glob.glob(file_path + "*171*.fts") +im193 = glob.glob(file_path + "*193*.fts") +im211 = glob.glob(file_path + "*211*.fts") +imhmi = glob.glob(file_path + "*hmi*.fts") + +"""ensure that all images are present""" + +if im171 == [] or im193 == [] or im211 == [] or imhmi == []: + print("Not all required files present") + sys.exit() + + +def rescale_aia(image: np.array, orig_res: int, desired_res: int): + """ + Rescale the input aia image dimensions. + + Parameters + ---------- + image: 'np.array' + orig_res: 'int' + desired_res: 'int + + Returns + ------- + 'np.array' + """ + + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline( + scaled_array, scaled_array, fits.getdata(image[0], 0) / (fits.getheader(image[0], 0)["EXPTIME"]) + ) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], 0)) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + + +def rescale_hmi(image: np.array, orig_res: int, desired_res: int): + """ + Rescale the input hmi image dimensions. + + Parameters + ---------- + image: 'np.array' + orig_res: 'int' + desired_res: 'int + + Returns + ------- + 'np.array' + """ + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], ext=0)) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], ext=0)) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + + +"""defining data arrays which are used in later steps""" + +data = rescale_aia(im171, 1024, 4096) +datb = rescale_aia(im193, 1024, 4096) +datc = rescale_aia(im211, 1024, 4096) +datm = rescale_hmi(imhmi, 1024, 4096) + + +def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): + """ + Defines headers and filters aia arrays to meet header requirements + + Parameters + ---------- + aiaa: 'np.array' + aiab: 'np.array' + aiac: 'np.array' + aiam: 'np.array' + + Returns + ------- + 'np.array' + + """ + global heda, hedb, hedc, hedm, datm + heda = fits.getheader(aiaa[0], 0) + hedb = fits.getheader(aiab[0], 0) + hedc = fits.getheader(aiac[0], 0) + hedm = fits.getheader(aiam[0], 0) + if hedb["ctype1"] != "solar_x ": + hedb["ctype1"] = "solar_x " + hedb["ctype2"] = "solar_y " + if heda["cdelt1"] > 1: + heda["cdelt1"], heda["cdelt2"], heda["crpix1"], heda["crpix2"] = ( + heda["cdelt1"] / 4.0, + heda["cdelt2"] / 4.0, + heda["crpix1"] * 4.0, + heda["crpix2"] * 4.0, + ) + hedb["cdelt1"], hedb["cdelt2"], hedb["crpix1"], hedb["crpix2"] = ( + hedb["cdelt1"] / 4.0, + hedb["cdelt2"] / 4.0, + hedb["crpix1"] * 4.0, + hedb["crpix2"] * 4.0, + ) + hedc["cdelt1"], hedc["cdelt2"], hedc["crpix1"], hedc["crpix2"] = ( + hedc["cdelt1"] / 4.0, + hedc["cdelt2"] / 4.0, + hedc["crpix1"] * 4.0, + hedc["crpix2"] * 4.0, + ) + if hedm["crota1"] > 90: + datm = np.rot90(np.rot90(datm)) + + +filter(im171, im193, im211, imhmi) + + +def remove_neg(aiaa: np.array, aiab: np.array, aiac: np.array): + """ + Removes negative values from arrays + + Parameters + ---------- + aiaa: 'np.array' + aiab: 'np.array' + aiac: 'np.array' + + Returns + ------- + 'np.array' + + """ + global data, datb, datc + data[np.where(data <= 0)] = 0 + datb[np.where(datb <= 0)] = 0 + datc[np.where(datc <= 0)] = 0 + + +remove_neg(im171, im193, im211) + +"""defines the shape of the arrays as "s" and "rs" as the solar radius""" +s = np.shape(data) +rs = heda["rsun"] + + +def pix_arc(aia: np.array): + global dattoarc + dattoarc = heda["cdelt1"] + global conver + conver = ((s[0]) / 2) * dattoarc / hedm["cdelt1"] - (s[1] / 2) + global convermul + convermul = dattoarc / hedm["cdelt1"] + + +pix_arc(im171) + + +def to_helio(image: np.array): + """ + Converts arrays to the Heliographic Stonyhurst coordinate system + + Parameters + ---------- + image: 'np.array' + + Returns + ------- + 'np.array' + + """ + aia = sunpy.map.Map(image) + adj = 4096 / aia.dimensions[0].value + x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj + global hpc + hpc = aia.pixel_to_world(x, y) + global hg + hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + global csys + csys = wcs.WCS(hedb) + print(csys) + + +to_helio(im171) + +"""Setting up arrays to be used in later processing""" +ident = 1 +iarr = np.zeros((s[0], s[1]), dtype=np.byte) +bmcool = np.zeros((s[0], s[1]), dtype=np.float32) +offarr, slate = np.array(iarr), np.array(iarr) +cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) +circ = np.zeros((s[0], s[1]), dtype=int) + +"""creation of a 2d gaussian for magnetic cut offs""" +r = (s[1] / 2.0) - 450 +xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) +center = [int(s[1] / 2.0), int(s[1] / 2.0)] +w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) +y, x = np.mgrid[0:4096, 0:4096] +garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) +garr[w] = 1.0 + +"""creates sub-arrays of props to isolate column of index 0 and column of index 1""" +props = np.zeros((26, 30), dtype="", + "", + "", + "BMAX", + "BMIN", + "TOT_B+", + "TOT_B-", + "", + "", + "", +) +props[:, 1] = ( + "num", + '"', + '"', + "H°", + '"', + '"', + '"', + '"', + '"', + '"', + '"', + '"', + "H°", + "°", + "Mm^2", + "%", + "G", + "G", + "G", + "G", + "G", + "G", + "G", + "Mx", + "Mx", + "Mx", +) + +"""define threshold values in log space""" + +with np.errstate(divide="ignore"): + t0 = np.log10(datc) + t1 = np.log10(datb) + t2 = np.log10(data) + + +class Bounds: + """Mixin to change and define array boundaries and slopes""" + + def __init__(self, upper, lower, slope): + self.upper = upper + self.lower = lower + self.slope = slope + + def new_u(self, new_upper): + self.upper = new_upper + + def new_l(self, new_lower): + self.lower = new_lower + + def new_s(self, new_slope): + self.slope = new_slope + + +t0b = Bounds(0.8, 2.7, 255) +t1b = Bounds(1.4, 3.0, 255) +t2b = Bounds(1.2, 3.9, 255) + + +def threshold(tval: np.array): + """ + Threshold arrays based on desired boundaries + + Parameters + ---------- + tval: 'np.array' + + Returns + ------- + 'np.array' + + """ + global t0, t1, t2 + if tval.all() == t0.all(): + t0[np.where(t0 < t0b.upper)] = t0b.upper + t0[np.where(t0 > t0b.lower)] = t0b.lower + if tval.all() == t1.all(): + t1[np.where(t1 < t1b.upper)] = t1b.upper + t1[np.where(t1 > t1b.lower)] = t2b.lower + if tval.all() == t2.all(): + t2[np.where(t2 < t2b.upper)] = t2b.upper + t2[np.where(t2 > t2b.lower)] = t2b.lower + else: + print("Must input valid logarithmic arrays") + + +threshold(t0) +threshold(t1) +threshold(t2) + + +def set_contour(tval: np.array): + """Sets contour values for bounded arrays + + Parameters + ---------- + tval: 'np.array' + + Returns + ------- + 'np.array' + + """ + global t0, t1, t2 + if tval.all() == t0.all(): + t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) + elif tval.all() == t1.all(): + t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) + elif tval.all() == t2.all(): + t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) + + +set_contour(t0) +set_contour(t1) +set_contour(t2) + + +def create_mask(): + """ + Creates 3 segmented bitmasks + + Returns + ------- + 'np.array' + + """ + + global t0, t1, t2, bmmix, bmhot, bmcool + with np.errstate(divide="ignore", invalid="ignore"): + bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 + bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 + bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 + + +create_mask() + + +def conjunction(): + """ + Creates a conjunction of 3 segmentations + + Returns + ------- + 'np.array' + + """ + global bmhot, bmcool, bmmix, cand + cand = bmcool * bmmix * bmhot + + +conjunction() + + +def misid(): + """ + Removes off-detector mis-identification + + Returns + ------- + 'np.array' + + """ + global s, r, w, circ, cand + r = (s[1] / 2.0) - 100 + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + cand = cand * circ + + +misid() + + +def on_off(): + """ + Seperates on-disk and off-limb coronal holes + + Returns + ------- + 'np.array' + + """ + global circ, cand + circ[:] = 0 + r = (rs / dattoarc) - 10 + inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[inside] = 1.0 + r = (rs / dattoarc) + 40 + outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) + circ[outside] = 1.0 + cand = cand * circ + + +on_off() + + +def contours(): + """ + Contours the identified datapoints + + Returns + ------- + cand: 'np.array' + cont: 'tuple' + heir: 'np.array' + + """ + global cand, cont, heir + cand = np.array(cand, dtype=np.uint8) + cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + +contours() + + +def sort(): + """ + Sorts the contours by size + + Returns + ------- + reord: 'list' + tmp: 'list' + cont: 'list' + + """ + global sizes, reord, tmp, cont + sizes = [] + for i in range(len(cont)): + sizes = np.append(sizes, len(cont[i])) + reord = sizes.ravel().argsort()[::-1] + tmp = list(cont) + for i in range(len(cont)): + tmp[i] = cont[reord[i]] + cont = list(tmp) + + +sort() + + +# =====cycles through contours========= + + +def extent(i, ypos, xpos, hg, cont): + """ + Finds coronal hole extent in latitude and longitude + + Parameters + ---------- + i: 'int' + ypos: 'astropy.units.quantity.Quantity' + xpos: 'astropy.units.quantity.Quantity' + hg: 'astropy.coordinates.sky_coordinate.SkyCoord' + cont: 'list' + + Returns + ------- + maxxlon: 'astropy.coordinates.angles.core.Longitude' + minxlon: 'astropy.coordinates.angles.core.Longitude' + centlat: 'astropy.coordinates.angles.core.Latitude' + centlon: 'astropy.coordinates.angles.core.Longitude' + + """ + global maxxlat, maxxlon, maxylat, maxylon, minxlon, minylat, minylon, minxlat + maxxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxylat = hg.lat[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + maxylon = hg.lon[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minylat = hg.lat[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minylon = hg.lon[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + + # =====CH centroid in lat/lon======= + + centlat = hg.lat[int(ypos), int(xpos)] + centlon = hg.lon[int(ypos), int(xpos)] + return maxxlon, minxlon, centlat, centlon + + +def coords(i, csys, cont): + """ + Finds coordinates of CH boundaries + + Parameters + ---------- + i: 'int' + csys: 'astropy.wcs.wcs.WCS' + cont: 'list' + + Returns + ------- + Ywb: 'np.array' + Xwb: 'np.array' + Yeb: 'np.array' + Xeb: 'np.array' + Ynb: 'np.array' + Xnb: 'np.array' + Ysb: 'np.array' + Xsb: 'np.array' + """ + global Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb + Ywb, Xwb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + 0, + ) + Yeb, Xeb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + 0, + ) + Ynb, Xnb = csys.all_pix2world( + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + Ysb, Xsb = csys.all_pix2world( + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + + return Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb + + +def ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, +): + """ + Insertion of CH properties into property array + + Parameters + ---------- + datm: 'np.array' + rs: 'float' + ident: 'int' + props: 'np.array' + arcar: 'np.float64' + arccent: 'list' + pos: 'np.array' + npix: 'list' + trummar: 'np.float64' + centlat: 'str' + centlon: 'str' + mB: 'np.float64' + mBpos: 'np.float64' + mBneg: 'np.float64' + + Returns + ------- + props[0, ident + 1]: 'str' + props[1, ident + 1]: 'str' + props[2, ident + 1]: 'str' + props[3, ident + 1]: 'str' + props[4, ident + 1]: 'str' + props[5, ident + 1]: 'str' + props[6, ident + 1]: 'str' + props[7, ident + 1]: 'str' + props[8, ident + 1]: 'str' + props[9, ident + 1]: 'str' + props[10, ident + 1]: 'str' + props[11, ident + 1]: 'str' + props[12, ident + 1]: 'str' + props[13, ident + 1]: 'str' + props[14, ident + 1]: 'str' + props[15, ident + 1]: 'str' + props[16, ident + 1]: 'str' + props[17, ident + 1]: 'str' + props[18, ident + 1]: 'str' + props[19, ident + 1]: 'str' + props[20, ident + 1]: 'str' + tbpos: 'np.float64' + props[21, ident + 1]: 'str' + tbneg: 'np.float64' + props[22, ident + 1]: 'str' + props[23, ident + 1]: 'str' + props[24, ident + 1]: 'str' + props[25, ident + 1]: 'str' + + """ + props[0, ident + 1] = str(ident) + props[1, ident + 1] = str(np.round(arccent[0])) + props[2, ident + 1] = str(np.round(arccent[1])) + props[3, ident + 1] = str(centlon + centlat) + props[4, ident + 1] = str(np.round(Xeb)) + props[5, ident + 1] = str(np.round(Yeb)) + props[6, ident + 1] = str(np.round(Xwb)) + props[7, ident + 1] = str(np.round(Ywb)) + props[8, ident + 1] = str(np.round(Xnb)) + props[9, ident + 1] = str(np.round(Ynb)) + props[10, ident + 1] = str(np.round(Xsb)) + props[11, ident + 1] = str(np.round(Ysb)) + props[12, ident + 1] = str(eastl + "-" + westl) + props[13, ident + 1] = str(width) + props[14, ident + 1] = f"{trummar/1e+12:.1e}" + props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) + props[16, ident + 1] = str(np.round(mB, 1)) + props[17, ident + 1] = str(np.round(mBpos, 1)) + props[18, ident + 1] = str(np.round(mBneg, 1)) + props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) + props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) + tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) + props[21, ident + 1] = f"{tbpos:.1e}" + tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) + props[22, ident + 1] = f"{tbneg:.1e}" + props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" + props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" + props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" + + +"""Cycles through contours""" + +for i in range(len(cont)): + x = np.append(x, len(cont[i])) + + """only takes values of minimum surface length and calculates area""" + + if len(cont[i]) <= 100: + continue + area = 0.5 * np.abs( + np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) + - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) + ) + arcar = area * (dattoarc**2) + if arcar > 1000: + """finds centroid""" + + chpts = len(cont[i]) + cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] + + """remove quiet sun regions encompassed by coronal holes""" + if ( + cand[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + print(slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 + + else: + """Create a simple centre point if coronal hole regions is not quiet""" + + arccent = csys.all_pix2world(cent[0], cent[1], 0) + + """classifies off limb CH regions""" + + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + else: + """classifies on disk coronal holes""" + + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + poslin = np.where(slate == 1) + slate[:] = 0 + print(poslin) + + """create an array for magnetic polarity""" + + pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) + pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + npix = list( + np.histogram( + datm[pos[:, 0], pos[:, 1]], + bins=np.arange( + np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, + np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, + 1, + ), + ) + ) + npix[0][np.where(npix[0] == 0)] = 1 + npix[1] = npix[1][:-1] + 0.5 + + wh1 = np.where(npix[1] > 0) + wh2 = np.where(npix[1] < 0) + + """Filters magnetic cutoff values by area""" + + if ( + np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) + <= 10 + and arcar < 9000 + ): + continue + if ( + np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] + and arcar < 40000 + ): + continue + iarr[poslin] = ident + + """create an accurate center point""" + + ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) + xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) + + arccent = csys.all_pix2world(xpos, ypos, 0) + + """calculate average angle coronal hole is subjected to""" + + dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) + ang = np.arcsin(dist / rs) + + """calculate area of CH with minimal projection effects""" + + trupixar = abs(area / np.cos(ang)) + truarcar = trupixar * (dattoarc**2) + trummar = truarcar * ((6.96e08 / rs) ** 2) + + """find CH extent in lattitude and longitude""" + + maxxlon, minxlon, centlat, centlon = extent(i, ypos, xpos, hg, cont) + + """caluclate the mean magnetic field""" + + mB = np.mean(datm[pos[:, 0], pos[:, 1]]) + mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) + mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) + + """finds coordinates of CH boundaries""" + + Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb = coords(i, csys, cont) + + width = round(maxxlon.value) - round(minxlon.value) + + if minxlon.value >= 0.0: + eastl = "W" + str(int(np.round(minxlon.value))) + else: + eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0: + westl = "W" + str(int(np.round(maxxlon.value))) + else: + westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0: + centlat = "N" + str(int(np.round(centlat.value))) + else: + centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0: + centlon = "W" + str(int(np.round(centlon.value))) + else: + centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) + + """insertions of CH properties into property array""" + + ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, + ) + """sets up code for next possible coronal hole""" + + ident = ident + 1 + +"""sets ident back to max value of iarr""" + +ident = ident - 1 + +"""stores all CH properties in a text file""" +np.savetxt("ch_summary.txt", props, fmt="%s") + + +def rescale01(arr, cmin=None, cmax=None, a=0, b=1): + """ + Rescales array + + Parameters + ---------- + arr: 'np.arr' + cmin: 'np.float' + cmax: 'np.float' + a: 'int' + b: 'int' + + Returns + ------- + np.array + + """ + if cmin or cmax: + arr = np.clip(arr, cmin, cmax) + return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + + +def plot_tricolor(): + """ + Plots a tricolor mask of image data + + Returns + ------- + plot: 'matplotlib.image.AxesImage' + + """ + + tricolorarray = np.zeros((4096, 4096, 3)) + + data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) + data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) + data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) + + tricolorarray[..., 0] = data_c / np.max(data_c) + tricolorarray[..., 1] = data_b / np.max(data_b) + tricolorarray[..., 2] = data_a / np.max(data_a) + + fig, ax = plt.subplots(figsize=(10, 10)) + + plt.imshow(tricolorarray, origin="lower") + plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) + plt.savefig("tricolor.png") + plt.close() + + +def plot_mask(slate=slate): + """ + Plots the contour mask + + Parameters + ---------- + slate: 'np.array' + + Returns + ------- + plot: 'matplotlib.image.AxesImage' + + """ + + chs = np.where(iarr > 0) + slate[chs] = 1 + slate = np.array(slate, dtype=np.uint8) + cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + circ[:] = 0 + r = rs / dattoarc + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + + plt.figure(figsize=(10, 10)) + plt.xlim(143, 4014) + plt.ylim(143, 4014) + plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) + plt.gca().set_aspect("equal", adjustable="box") + plt.axis("off") + plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) + plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) + + plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) + + +plot_tricolor() +plot_mask() From 9aeafef647fe95d8d916fab3db7724f02cc51ba8 Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Tue, 9 Jul 2024 09:18:05 -0400 Subject: [PATCH 06/10] Update for code review --- .DS_Store | Bin 6148 -> 0 bytes CHIMERA_V1.ipynb | 941 ------------------ chimera.py | 994 ------------------- chimerapy/chimera.py | 1263 ++++++++++++++++-------- chimerapy/chimera_legacy.py | 521 ++++++++++ chimerapy/tests/test_chimera.py | 435 +++++++- chimerapy/tests/test_chimera_legacy.py | 29 + 7 files changed, 1833 insertions(+), 2350 deletions(-) delete mode 100644 .DS_Store delete mode 100644 CHIMERA_V1.ipynb delete mode 100644 chimera.py create mode 100644 chimerapy/chimera_legacy.py create mode 100644 chimerapy/tests/test_chimera_legacy.py diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index f23f47c8aa6559a33596e0f23a54d830f83d66f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy-LJD5dKC}oY5WaX*u=yVPJ+MMzxGAqaAJAP{BYj5DWwZ!N89)z?!WxJ9Z2m3K{04~N*!%#(NiXkH{Y9#WY*Bsqo- z27-aF3~295DYgE${FEk}d~*pE3sT5t7gg5<=Xn~oLXxuj#~~D_3L#);oM3w l(MmBN^d31equVQ;)1ET+j+#Z+Eu0uX0!B!vVBi-Rcn6BoHueAj diff --git a/CHIMERA_V1.ipynb b/CHIMERA_V1.ipynb deleted file mode 100644 index 084ac4d..0000000 --- a/CHIMERA_V1.ipynb +++ /dev/null @@ -1,941 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 30, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "aMdifgamAEpp", - "outputId": "9894abba-dd40-4480-f47a-6b3296220a41" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: sunpy in /usr/local/lib/python3.10/dist-packages (5.1.1)\n", - "Requirement already satisfied: astropy!=5.1.0,>=5.0.6 in /usr/local/lib/python3.10/dist-packages (from sunpy) (5.3.4)\n", - "Requirement already satisfied: numpy>=1.21.0 in /usr/local/lib/python3.10/dist-packages (from sunpy) (1.25.2)\n", - "Requirement already satisfied: packaging>=19.0 in /usr/local/lib/python3.10/dist-packages (from sunpy) (24.0)\n", - "Requirement already satisfied: parfive[ftp]>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from sunpy) (2.0.2)\n", - "Requirement already satisfied: pyerfa>=2.0 in /usr/local/lib/python3.10/dist-packages (from astropy!=5.1.0,>=5.0.6->sunpy) (2.0.1.1)\n", - "Requirement already satisfied: PyYAML>=3.13 in /usr/local/lib/python3.10/dist-packages (from astropy!=5.1.0,>=5.0.6->sunpy) (6.0.1)\n", - "Requirement already satisfied: tqdm>=4.27.0 in /usr/local/lib/python3.10/dist-packages (from parfive[ftp]>=2.0.0->sunpy) (4.66.2)\n", - "Requirement already satisfied: aiohttp in /usr/local/lib/python3.10/dist-packages (from parfive[ftp]>=2.0.0->sunpy) (3.9.3)\n", - "Requirement already satisfied: aioftp>=0.17.1 in /usr/local/lib/python3.10/dist-packages (from parfive[ftp]>=2.0.0->sunpy) (0.22.3)\n", - "Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (1.3.1)\n", - "Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (23.2.0)\n", - "Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (1.4.1)\n", - "Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (6.0.5)\n", - "Requirement already satisfied: yarl<2.0,>=1.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (1.9.4)\n", - "Requirement already satisfied: async-timeout<5.0,>=4.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->parfive[ftp]>=2.0.0->sunpy) (4.0.3)\n", - "Requirement already satisfied: idna>=2.0 in /usr/local/lib/python3.10/dist-packages (from yarl<2.0,>=1.0->aiohttp->parfive[ftp]>=2.0.0->sunpy) (3.6)\n" - ] - } - ], - "source": [ - "pip install sunpy" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "id": "sqDeNTSrJ7YA" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/imogennagle/opt/miniconda3/envs/sunpy/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], - "source": [ - "#import required libraries\n", - "import astropy\n", - "from astropy import wcs\n", - "from astropy.io import fits\n", - "from astropy.modeling.models import Gaussian2D\n", - "import astropy.units as u\n", - "from astropy.utils.data import download_file\n", - "from astropy.visualization import astropy_mpl_style\n", - "import cv2\n", - "import glob\n", - "import mahotas\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import scipy\n", - "import scipy.interpolate\n", - "import sunpy\n", - "import sunpy.map\n", - "import sys\n", - "from scipy.interpolate import interp2d, RectBivariateSpline\n", - "\n", - "plt.style.use(astropy_mpl_style)" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": { - "id": "npCkEG_xLQEz" - }, - "outputs": [], - "source": [ - "#load in required fits file. Make sure to use full disk images\n", - "im171 = glob.glob('171.fts')\n", - "im193 = glob.glob('193.fts')\n", - "im211 = glob.glob('211.fts')\n", - "imhmi = glob.glob('hmi.fts')" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": { - "id": "6EwDwosRLS00" - }, - "outputs": [], - "source": [ - "#make sure all required files exist\n", - "if im171 == [] or im193 == [] or im211 == [] or imhmi == []:\n", - "\tprint(\"Not all required files present\")\n", - "\tsys.exit()" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": { - "id": "mp__D-fxLmTO" - }, - "outputs": [], - "source": [ - "#reads in fits files and scales images to a size of 4096. Ensures correct image resolution before processing.\n", - "\n", - "'''Changes: switched interp2d to RectBivariateSpline, changed syntax for np.arrange\n", - "to be more compatabile with current python version, changed variable name x to \"scaled_array\",\n", - "added an \"if statement\" at the end to ensure that image resolutions are correct before processing'''\n", - "\n", - "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", - "\n", - "scaled_array=np.arange(start = 0, stop = 4096, step = 4)\n", - "\n", - "hdu_number=0\n", - "heda=fits.getheader(im171[0],hdu_number)\n", - "data= fits.getdata(im171[0], ext=0)/(heda[\"EXPTIME\"])\n", - "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,data)\n", - "data=dn(np.arange(0,4096),np.arange(0,4096))\n", - "\n", - "if len(data) != 4096:\n", - " print(\"Incorrect image resolution\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": { - "id": "woeQladjLn_q" - }, - "outputs": [], - "source": [ - "#reads in fits files and scales images to a size of 4096. Ensures correct image resolution before processing.\n", - "\n", - "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", - "\n", - "hedb=fits.getheader(im193[0],hdu_number)\n", - "datb= fits.getdata(im193[0], ext=0)/(hedb[\"EXPTIME\"])\n", - "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datb)\n", - "datb=dn(np.arange(0,4096),np.arange(0,4096))\n", - "\n", - "if len(datb) != 4096:\n", - " print(\"Incorrect image resolution\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "EAqYkrcXLp9I", - "outputId": "e7813ba4-5457-4527-e5b1-8ad6ed3634f1" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[ 3.42794344e-19 -9.93202024e-18 -1.28940303e-17 ... 1.50616176e-18\n", - " 1.50616176e-18 1.50616176e-18]\n", - " [ 6.10773115e-18 -9.98323479e-02 -1.14094112e-01 ... -1.70009813e-02\n", - " -1.70009813e-02 -1.70009813e-02]\n", - " [ 8.81061434e-18 -1.50645453e-01 -1.72166232e-01 ... -1.94296929e-02\n", - " -1.94296929e-02 -1.94296929e-02]\n", - " ...\n", - " [ 3.53679056e-18 -4.80522035e-02 -5.76100609e-02 ... 2.80098706e-01\n", - " 2.80098706e-01 2.80098706e-01]\n", - " [ 3.53679056e-18 -4.80522035e-02 -5.76100609e-02 ... 2.80098706e-01\n", - " 2.80098706e-01 2.80098706e-01]\n", - " [ 3.53679056e-18 -4.80522035e-02 -5.76100609e-02 ... 2.80098706e-01\n", - " 2.80098706e-01 2.80098706e-01]]\n" - ] - } - ], - "source": [ - "'''TO FIX: Invalid 'Blank' keyword in header warning'''\n", - "\n", - "hedc=fits.getheader(im211[0],hdu_number)\n", - "datc= fits.getdata(im211[0], ext=0)/(hedc[\"EXPTIME\"])\n", - "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datc)\n", - "datc=dn(np.arange(0,4096),np.arange(0,4096))\n", - "\n", - "if len(datc) != 4096:\n", - " print(\"Incorrect image resolution\")\n", - "\n", - "print(datc)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "vifW2C7HLr2u", - "outputId": "4b998d95-4a45-441d-e9af-c754235949d3" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: VerifyWarning: Invalid 'BLANK' keyword in header. The 'BLANK' keyword is only applicable to integer data, and will be ignored in this HDU. [astropy.io.fits.hdu.image]\n", - "WARNING:astropy:VerifyWarning: Invalid 'BLANK' keyword in header. The 'BLANK' keyword is only applicable to integer data, and will be ignored in this HDU.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", - " -2.14748368e+08 -2.14748368e+08]\n", - " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", - " -2.14748368e+08 -2.14748368e+08]\n", - " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", - " -2.14748368e+08 -2.14748368e+08]\n", - " ...\n", - " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", - " -2.14748368e+08 -2.14748368e+08]\n", - " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", - " -2.14748368e+08 -2.14748368e+08]\n", - " [-2.14748368e+08 -2.14748368e+08 -2.14748368e+08 ... -2.14748368e+08\n", - " -2.14748368e+08 -2.14748368e+08]]\n" - ] - } - ], - "source": [ - "'''Changes: to get rid of indexing error, copied scaling from data, datb, datc so that cell 189 runs correctly (before the data was out of the range of the image resolution which was 1024 instead of 4096)\n", - "Exposure time for hmi is zero, didn't scale by exposure time'''\n", - "\n", - "hedm=fits.getheader(imhmi[0],hdu_number)\n", - "datm= fits.getdata(imhmi[0], ext=0)\n", - "dn=scipy.interpolate.RectBivariateSpline(scaled_array,scaled_array,datm)\n", - "datm=dn(np.arange(0,4096),np.arange(0,4096))\n", - "\n", - "if len(datm) != 4096:\n", - " print(\"Incorrect image resolution\")\n", - "\n", - "print(datm)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "metadata": { - "id": "z4JBQBiULtrG" - }, - "outputs": [], - "source": [ - "#rotates array if 'crota1' is greawter than 90\n", - "if hedm['crota1'] > 90:\n", - "\tdatm=np.rot90(np.rot90(datm))" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": { - "id": "0ZPZxHgrLwfY" - }, - "outputs": [], - "source": [ - "#defines the shape (length) of the array as \"s\" and the solar radius as \"rs\"\n", - "s=np.shape(data)\n", - "rs=heda['rsun']" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": { - "id": "qS50C1BZLyMI" - }, - "outputs": [], - "source": [ - "#ensures \"cype1\" and \"ctype2\" are correctly defined as \"solar_x\" and \"solar_y\" respectively\n", - "if hedb[\"ctype1\"] != 'solar_x ':\n", - "\thedb[\"ctype1\"]='solar_x '\n", - "\thedb[\"ctype2\"]='solar_y '" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": { - "id": "Kz4S85e4L1_S" - }, - "outputs": [], - "source": [ - "#rescales \"cdelt1\", \"cdelt2\", \"cpix1\", and \"cpix2\" if \"cdelt1\" > 1\n", - "if heda['cdelt1'] > 1:\n", - "\theda['cdelt1'],heda['cdelt2'],heda['crpix1'],heda['crpix2']=heda['cdelt1']/4.,heda['cdelt2']/4.,heda['crpix1']*4.0,heda['crpix2']*4.0\n", - "\thedb['cdelt1'],hedb['cdelt2'],hedb['crpix1'],hedb['crpix2']=hedb['cdelt1']/4.,hedb['cdelt2']/4.,hedb['crpix1']*4.0,hedb['crpix2']*4.0\n", - "\thedc['cdelt1'],hedc['cdelt2'],hedc['crpix1'],hedc['crpix2']=hedc['cdelt1']/4.,hedc['cdelt2']/4.,hedc['crpix1']*4.0,hedc['crpix2']*4.0" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": { - "id": "acXRp68LL33M" - }, - "outputs": [], - "source": [ - "#converts pixel values to arcseconds\n", - "dattoarc=heda['cdelt1']\n", - "conver=(s[0]/2)*dattoarc/hedm['cdelt1']-(s[1]/2)\n", - "convermul=dattoarc/hedm['cdelt1']" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "K1sknmYxL6hf", - "outputId": "8fdcf6a0-99cf-4bcc-a332-6b457fe964d9" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: VerifyWarning: Verification reported errors: [astropy.io.fits.verify]\n", - "WARNING:astropy:VerifyWarning: Verification reported errors:\n", - "WARNING: VerifyWarning: Card 35: [astropy.io.fits.verify]\n", - "WARNING:astropy:VerifyWarning: Card 35:\n", - "WARNING: VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O' [astropy.io.fits.verify]\n", - "WARNING:astropy:VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O'\n", - "WARNING: VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", - " [astropy.io.fits.verify]\n", - "WARNING:astropy:VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", - "\n", - "WARNING: VerifyWarning: Verification reported errors: [astropy.io.fits.verify]\n", - "WARNING:astropy:VerifyWarning: Verification reported errors:\n", - "WARNING: VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O' [astropy.io.fits.verify]\n", - "WARNING:astropy:VerifyWarning: Unfixable error: Illegal keyword name 'DATE_D$O'\n", - "WARNING: VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", - " [astropy.io.fits.verify]\n", - "WARNING:astropy:VerifyWarning: Note: astropy.io.fits uses zero-based indexing.\n", - "\n", - "WARNING: FITSFixedWarning: CROTACN1= 0.00000000000 / Rotation x center \n", - "keyword looks very much like CROTAn but isn't. [astropy.wcs.wcs]\n", - "WARNING:astropy:FITSFixedWarning: CROTACN1= 0.00000000000 / Rotation x center \n", - "keyword looks very much like CROTAn but isn't.\n", - "WARNING: FITSFixedWarning: CROTACN2= 0.00000000000 / Rotation y center \n", - "keyword looks very much like CROTAn but isn't. [astropy.wcs.wcs]\n", - "WARNING:astropy:FITSFixedWarning: CROTACN2= 0.00000000000 / Rotation y center \n", - "keyword looks very much like CROTAn but isn't.\n", - "WARNING: FITSFixedWarning: 'datfix' made the change 'Invalid DATE-OBS format '31-Jan-2024 00:24:40.843''. [astropy.wcs.wcs]\n", - "WARNING:astropy:FITSFixedWarning: 'datfix' made the change 'Invalid DATE-OBS format '31-Jan-2024 00:24:40.843''.\n" - ] - } - ], - "source": [ - "#Changes to Heliographic Stonyhurst coordinate system\n", - "\n", - "'''TO FIX: Warnings for illegal keyword names'''\n", - "\n", - "aia=sunpy.map.Map(im171)\n", - "adj=4096./aia.dimensions[0].value\n", - "x, y = (np.meshgrid(*[np.arange(adj*v.value) for v in aia.dimensions]) * u.pixel)/adj\n", - "hpc = aia.pixel_to_world(x, y)\n", - "hg=hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst)\n", - "\n", - "csys=wcs.WCS(hedb)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": { - "id": "QUE4rIA7L-UZ" - }, - "outputs": [], - "source": [ - "#setting up arrays to be used in later processing\n", - "ident=1\n", - "iarr=np.zeros((s[0],s[1]),dtype=np.byte)\n", - "offarr,slate=np.array(iarr),np.array(iarr)\n", - "bmcool=np.zeros((s[0],s[1]),dtype=np.float32)\n", - "cand,bmmix,bmhot=np.array(bmcool),np.array(bmcool),np.array(bmcool)\n", - "circ=np.zeros((s[0],s[1]),dtype=int)" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": { - "id": "elqZFWcnMBrZ" - }, - "outputs": [], - "source": [ - "#creation of a 2d gaussian for magnetic cut offs\n", - "\n", - "r = (s[1]/2.0)-450\n", - "xgrid,ygrid=np.meshgrid(np.arange(s[0]),np.arange(s[1]))\n", - "center=[int(s[1]/2.0),int(s[1]/2.0)]\n", - "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 > r**2)\n", - "y,x=np.mgrid[0:4096,0:4096]\n", - "garr=Gaussian2D(1,s[0]/2,s[1]/2,2000/2.3548,2000/2.3548)(x,y)\n", - "garr[w]=1.0" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": { - "id": "06jPD4pRMGdw" - }, - "outputs": [], - "source": [ - "#creates sub-arrays of props to isolate column of index 0 and column of index 1\n", - "props=np.zeros((26,30),dtype='','','','BMAX','BMIN','TOT_B+','TOT_B-','','',''\n", - "props[:,1]='num','\"','\"','H°','\"','\"','\"','\"','\"','\"','\"','\"','H°','°','Mm^2','%','G','G','G','G','G','G','G','Mx','Mx','Mx'" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": { - "id": "XpbOolCwMVMF" - }, - "outputs": [], - "source": [ - "#removes negative data values\n", - "data[np.where(data <= 0)]=0\n", - "datb[np.where(datb <= 0)]=0\n", - "datc[np.where(datc <= 0)]=0" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "metadata": { - "id": "L3c366LOMQcW" - }, - "outputs": [], - "source": [ - "#ignores division errors in the following logarithms and sets conditions for t0, t1, and t2\n", - "with np.errstate(divide = 'ignore'):\n", - "\tt0=np.log10(datc)\n", - "\tt1=np.log10(datb)\n", - "\tt2=np.log10(data)\n", - "t0[np.where(t0 < 0.8)] = 0.8\n", - "t0[np.where(t0 > 2.7)] = 2.7\n", - "t1[np.where(t1 < 1.4)] = 1.4\n", - "t1[np.where(t1 > 3.0)] = 3.0\n", - "t2[np.where(t2 < 1.2)] = 1.2\n", - "t2[np.where(t2 > 3.9)] = 3.9" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": { - "id": "3oO_rrbgMZ4x" - }, - "outputs": [], - "source": [ - "#makes a multi-wavelength image for contours\n", - "t0=np.array(((t0-0.8)/(2.7-0.8))*255,dtype=np.float32)\n", - "t1=np.array(((t1-1.4)/(3.0-1.4))*255,dtype=np.float32)\n", - "t2=np.array(((t2-1.2)/(3.9-1.2))*255,dtype=np.float32)" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": { - "id": "Q9jr_gA-Mc6J" - }, - "outputs": [], - "source": [ - "#ignores division and invalid erros in the following conditions to create 3 segmented bitmasks\n", - "with np.errstate(divide = 'ignore',invalid='ignore'):\n", - "\tbmmix[np.where(t2/t0 >= ((np.mean(data)*0.6357)/(np.mean(datc))))]=1\n", - "\tbmhot[np.where(t0+t1 < (0.7*(np.mean(datb)+np.mean(datc))))]=1\n", - "\tbmcool[np.where(t2/t1 >= ((np.mean(data)*1.5102)/(np.mean(datb))))]=1" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": { - "id": "U_avvniSMe7a" - }, - "outputs": [], - "source": [ - "#conjunction of 3 segmentations\n", - "cand=bmcool*bmmix*bmhot" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "id": "-cady694uJY9", - "outputId": "bbd1fe49-75f1-4794-d970-415127af8f2b" - }, - "outputs": [ - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "string" - }, - "text/plain": [ - "'TO FIX: there is no code written for this section'" - ] - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#plot tricolour image with lon/lat contours\n", - "'''TO FIX: there is no code written for this section'''\n" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "metadata": { - "id": "rpZ36REKMggU" - }, - "outputs": [], - "source": [ - "#removes off detector mis-identifications\n", - "r=(s[1]/2.0)-100\n", - "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2)\n", - "circ[w]=1.0\n", - "cand=cand*circ" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "metadata": { - "id": "q8AzT5xXMgh-" - }, - "outputs": [], - "source": [ - "#seperates on-disk and off-limb coronal holes\n", - "circ[:]=0\n", - "r=(rs/dattoarc)-10\n", - "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2)\n", - "circ[w]=1.0\n", - "r=(rs/dattoarc)+40\n", - "w=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 >= r**2)\n", - "circ[w]=1.0\n", - "cand=cand*circ" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "id": "iVgAh6Y-ut80", - "outputId": "df72cbe1-0887-443c-f826-e7cd914436e0" - }, - "outputs": [ - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "string" - }, - "text/plain": [ - "'TO FIX: No code for this section'" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#open file for property storage\n", - "'''TO FIX: No code for this section'''" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "metadata": { - "id": "G911hr4d01R0" - }, - "outputs": [], - "source": [ - "#contours the identified datapoints\n", - "cand=np.array(cand,dtype=np.uint8)\n", - "cont,heir=cv2.findContours(cand,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)" - ] - }, - { - "cell_type": "code", - "execution_count": 58, - "metadata": { - "id": "lQI5QT8mMnQ9" - }, - "outputs": [], - "source": [ - "#sorts contours by size\n", - "sizes=[]\n", - "for i in range(len(cont)):\n", - "\tsizes=np.append(sizes,len(cont[i]))\n", - "reord=sizes.ravel().argsort()[::-1]\n", - "tmp=list(cont)\n", - "for i in range(len(cont)):\n", - "\ttmp[i]=cont[reord[i]]\n", - "cont=list(tmp)" - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 1000 - }, - "id": "OGhJ_fJT6j3t", - "outputId": "c00d4c6e-bdbb-4ca3-e426-ad0c36a70112" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(array([ 582, 582, 582, ..., 3207, 3208, 3208]), array([1674, 1675, 1676, ..., 3069, 3067, 3068]))\n", - "(array([2606, 2606, 2606, ..., 2687, 2688, 2688]), array([2408, 2409, 2410, ..., 2347, 2345, 2346]))\n", - "(array([1611, 1611, 1611, ..., 1826, 1826, 1826]), array([2278, 2279, 2280, ..., 2332, 2333, 2334]))\n", - "(array([1268, 1268, 1268, ..., 1551, 1551, 1551]), array([2994, 2995, 2996, ..., 3285, 3286, 3287]))\n", - "(array([ 621, 621, 621, ..., 1790, 1791, 1791]), array([1463, 1464, 1465, ..., 1153, 1151, 1152]))\n", - "(array([582, 582, 582, ..., 934, 935, 935]), array([1674, 1675, 1676, ..., 1729, 1727, 1728]))\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - ":180: RuntimeWarning: divide by zero encountered in log10\n", - " data_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9))\n", - ":181: RuntimeWarning: divide by zero encountered in log10\n", - " data_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0))\n", - ":182: RuntimeWarning: divide by zero encountered in log10\n", - " data_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7))\n", - ":210: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored\n", - " plt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2)\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAMWCAYAAABsvhCnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACvpUlEQVR4nOzdZ2CT5cLG8etJ0klL2RvZQ1DAY9kgIiqKgiIOhr5OXIB7Cx4VFHEdFXAheHAPEMU92HvJlL1lr1JaupO8H3rySJmduTP+vy+2aZpcraXN9dzL8nq9XgEAAABAEThMBwAAAAAQ/CgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIqMYgEAAACgyCgWAAAAAIrMZToAAKDodu7cKbfbLUnavXu3nnrqKWVlZeW5z/79+7Vt2zZ5vd5CPYfD4VD9+vWVkJCQ5/b4+Hi99tprKlWqlCQpOjpalSpVKtRzAACCl+Ut7F8YAECx83g82rp1q5KSkvTYY4/pyJEjWr16tdxut7Kzs+XxeExHLBKn0ymXy6WIiAidc845Kl++vF599VXFxsbqrLPOMh0PAFAEFAsA8BOv16tNmzZp9OjR+u2337R582ZlZ2fbIw1FYVmWXK5/BqETEhJUrVq1E+536623qm3btoV6jokTJ+rXX3894fZt27YpLS3Nfj8nJ6fQoyLHcjqdioyMVOPGjXXttdeqb9++ql27dpEfFwBQMigWAFCMUlJSNGvWLP373//W6tWrlZ6eXuAX2Q6HQy6XS/Hx8apZs6ZatGihgQMHyuFwqHnz5nI4gnN5XGZmplatWiVJGjp0qLZt26bNmzcrIyND2dnZhfo+lSpVSi1atNCIESP0r3/9S1FRUSURHQCQDxQLACiErVu36oknntCMGTO0f//+fI86OBwORUVFqW7durr66qt1ww03qEmTJnI6nSWcOLhkZGRow4YNGjlypGbMmKFt27YpKysr3+XD6XSqRo0a6tatmwYPHnzS0RsAQPGiWADAaRw5ckQff/yxhg0blu8C4XK5VKlSJXXu3FnPPvus6tev74ek4cXj8Wjt2rV6/PHHNX/+fB0+fFg5OTln/Dyn06maNWvqlVde0ZVXXqno6Gg/pAWA8ECxAID/OXLkiPr376/ffvtNycnJp706blmWYmNj1axZMw0bNkwXXHBBnjUOMCc9PV3ff/+9hg8frvXr1+dZ/3EylmWpfPnyuvbaa/XGG28wnQoAColiASAs5eTk6KmnntKYMWPOWCIiIiJUt25djR49WhdccIEiIiL8mBTFJS0tTd99950ef/xx7d69+7QjHJZlqUKFCnrqqad0//33y7IsPyYFgOBEsQAQFnbs2KErr7xSq1atOu10pujoaHXo0EHvv/++6tSp48eEMGXRokUaMGCAli9ffsLZH8eKiIhQp06d9NVXX6ls2bJ+TAgAwYFiASAkzZ07V71799aOHTtOORrhcrmUmJiozz//nG1MkceiRYt02223ac2aNacsog6HQw0bNtTkyZPVoEEDPycEgMATnHsWAsBx5s6dq7POOksOh0OWZal9+/b6+++/85SKhIQEvfPOO/buQtnZ2Zo3bx6lAido2bKlVq5caZ/JkZ6eriFDhigmJsa+j28BecOGDWVZlhwOh84++2xt2LDBYHIAMIcRCwBBadu2bbr66qu1YsWKU55GXa1aNX399ddq166dn9MhHHz22We67777dPDgwZN+3Ol0qnPnzvriiy9Uvnx5P6cDAP9jxAJA0HjmmWcUHR0ty7JUu3ZtLVu2LE+pqFatmmbNmiWv1yuv16udO3dSKlBi+vbtqwMHDsjr9crj8eizzz5TuXLl7I+73W798ccfqlChgizLUnx8vD744AODiQGgZDFiASBgbd26VZdddpnWr19/0nUSpUuX1ssvv6z+/fsH7WnUCE1ut1vPPPOM3njjjZNud2tZllq1aqUff/yR0QwAIYNiASCgfP3117rrrruUlJR0wsccDocuueQSTZ48WZGRkQbSAYWTlJSkrl27avHixSctyZUrV9Z3332n1q1bG0gHAMWDS3wAjBsyZIgiIiJkWZauv/76PKUiISFB06ZNk9frldvt1i+//EKpQNApW7asFi5cKI/HI4/Ho3feeUexsbH2x/fu3as2bdrIsizFxMQwZQpAUGLEAoARQ4YM0fDhw0+6lWeTJk00depUVa5c2UAywL9Wrlypbt26aceOHSd8LDIyUqNGjVL//v0NJAOAgmHEAoDfDB48WC6XS5ZladiwYXapsCxLffr0kdvtltfr1V9//UWpQNg499xz7a2Rs7OzdcEFF9gfy8rK0p133inLshQVFaX333/fYFIAOD1GLACUqNdff12PPfbYCSMTvjLxySefyLIsQ+mAwOV2u3XRRRdp5syZJ3wsMjJSn332mXr16mUgGQCcHMUCQLGbPn26unXrpvT09Dy3UyaAwnG73ercubNmzZp1wscSEhK0cuVK1axZ00AyAPgHxQJAsTh69KiaNGmi7du3n/Cxnj17auLEiZQJoBi43W61atVKf/755wkfa9GihRYvXiyn02kgGYBwxxoLAEXSp08fORwOxcXF5SkVjRo1UlZWlrxer7755htKBVBMnE6nlixZIq/Xq+TkZFWpUsX+2LJly+RyueR0OjV48GCDKQGEI0YsABTYrFmz1LVr1xOmOsXFxWnx4sVq1KiRoWRA+Pruu+90ww03KDMzM8/tCQkJWrt2bZ4CAgAlgRELAPni8XjUqVMnWZalCy64wC4VDodDw4cPl9frVUpKCqUCMOSqq65SRkaGvF6vbrvtNvv25ORkVa1aVZZl5bkdAIobIxYATmv9+vU677zzlJaWluf2Ro0aaeXKlYqIiDCUDMCZJCUlqUmTJtqzZ0+e2ytWrKj169erTJkyZoIBCEmMWAA4qT59+siyLDVq1MguFU6nU2PHjpXX69XatWspFUCAK1u2rHbv3i2v16shQ4bYa53279+vsmXLyuFw6OmnnzacEkCoYMQCgC0tLU2NGjU64QTgatWqacuWLYqMjDSUDEBx2bFjh5o2baojR47kub158+ZasmQJO0oBKDRGLABo7ty5ioiIUKlSpexSYVmWXn75ZXm9Xu3cuZNSAYSIGjVqKDk5WV6vV7fccot9+/Lly+VyuRQbG6sDBw6YCwggaFEsgDD28ssvy7IstW/fXjk5OZKk2NhYLV++XB6PR48++qjhhABK0ocffiiv16tJkybZUxvT09NVsWJFORwOff/994YTAggmTIUCwlD37t31ww8/5LmtadOmWrVqlaFEAAKB2+1WtWrVtG/fvjy3P/roo3r55ZcNpQIQLCgWQJjweDxq2LChNm3alOf2G264QV988YWhVAACVdu2bTV//vw8t3Xs2FEzZ840lAhAoGMqFBDiUlNTVbp0aTmdTrtUOBwOvffee/J6vZQKACc1b948eb1eDRgwwL5t1qxZsixL1atXl9vtNpgOQCBixAIIUampqapatapSU1Pt21wul5YsWaJmzZoZTAYgGE2aNEm9evXSsS8bqlatqu3bt8vlchlMBiBQUCyAEJOSkqJq1arlKRSlS5fW3r17FR0dbTAZgFCwdetWNWrUSFlZWfZt1apV07Zt2ygYQJijWAAhgkIBwJ+2bNmis88+W5mZmfZtFAwgvFEsgCCXnJysGjVqUCgAGMEIBgAfigUQpE5WKBISErRnzx4KBQC/27Ztmxo2bJinYLAGAwgvFAsgyBw9elRVqlShUAAISFu3blXjxo1PmCK1fft2OZ1Og8kAlDS2mwWChNfrVb169RQXF2eXioSEBB09elSHDx+mVAAICLVr11ZGRoa2bt2qyMhISdKuXbvkcrl04YUXmg0HoERRLIAg0KVLFzkcDm3evFmSFBMTYxeK2NhYw+kA4ES1atVSZmamtm7dao9UzJgxQ5ZlaeDAgYbTASgJFAsggN1///2yLEtTp06VlHsOxV9//aW0tDQKBYCgUKtWLeXk5Oibb76RZVmSpNGjR8uyLI0ZM8ZwOgDFiTUWQAD69ttv1bNnzzy3vf/+++rfv7+hRABQPB588EG98cYb9vsOh0Nr165V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U+3bt1e9evX0+OOPq3///pKkc889V6tWrZLEDAL4H5PgUWgrVqywS8VFF11EqQAAhJU///xTGzdu1Lp16+T1evXmm28W+TE7dOigm266SQ6HQ4mJiae8X1RUlGrUqKHNmzfrzjvv1FdffSWPx6OVK1eqSpUqkqTGjRtzOjf8ihELFMqxB+BVqlRJe/fuNZwIAAD/evjhh+0D7qKjozV79ux8f+7atWt11113qVq1avr888/t25OTk7Vv3748i7ZPZ9++ferWrZv9vm/tRVxcnLKysuRwOOR2u/OdCygKRixQKL4TRp1OJ6UCABCWXnvtNXm9Xg0ZMqTAB9TdeOONSk9P14YNG5SYmKhWrVrpzTffVJcuXdSnTx/16dOnQI933nnnacGCBZKkmJgYezqUx+NRfHx8gR4LKCyKBQqsXLly9mIx35kVAACEq48//liWZRX489xutzIyMuytZT/++GPVqlVLzz//vDZs2KCjR4+e8TEqVaqkxYsXa8yYMXI6nZo0aZLcbrcaNmyoIUOGSJJSU1MLdL4GUFgUCxRI27Zt7ZM+d+zYwVkVAICwNmDAAG3dulXVqlXL9+csWbLEfjsqKkqDBg1SRkaGvF6vtm7daheCa665psB5atSoocWLFysxMVFDhw7VzTffLCl3XeS9995b4McDCoJXhci3oUOH2mdVjB07VtWrVzecCAAAs8aMGSMpd6vX/Nq/f78kadu2bae8z5dffqmDBw/af3cL6t1331Xz5s01fvx4u6C88847+u233wr1eEB+sHgb+bJ8+XK1aNFCktS9e3dNnjzZbCAAAALAJ598optuukm33XbbKUcE9uzZY+/U5NOqVSt5vV7l5OSccvT/rLPO0t9//605c+YoKiqqUPk6d+6slJQU1atXT5s2bZIkpaSkKC4urlCPB5wOxQJndOwOUDVq1NDff/9tOBEAAIHBt7ZixowZKlWqVJ6P9e/fX0uXLpUk1a1bV1999ZUkqV27dqpataq2bdumK664Qj/88MMpH9/hcMjr9Wr06NFq3bp1gfNNmDBBL730khwOhyIjI5WRkXHCwXpAcaFY4IxcLpfcbrciIiJYrA0AwDFq1aql7du3n/Qgu9atW8uyLFmWpezs7JN+/siRIzVw4MDTPofL5VKpUqU0derUfOfKzs5Wnz59tHXrVvu2mJgYpaenS5LKlCljr5kEigtrLHBatWvXtve/LuhWegAAhDrfi3NfcdiwYYM95eiuu+5STk6OlixZou7du+vHH3/U4sWLlZGRoVmzZikjI+OMpULKLRY5OTl5bhswYIDGjx9/ys/p0qWLtm7dqvr169u3paen61//+pck6fDhw+rRo0fBvljgDCgWOKVBgwbZC8tWrVrFDlAAABzn8OHDcjgcateundasWaM+ffrohhtukCQdPHhQknTuuedq8uTJ6tatm84//3xFRUWpQ4cO+V434fF4lJaWpvvvv1+S9Pzzz2vBggUaOXKkVqxYcdLPadeunSSdsFvVn3/+qeuvv16S9P333+vHH38s+BcNnAJToXBSS5YsUWJioqTcgvHWW28ZTgQAQGDyeDxyOp2S/hld6NmzpyZNmqQ6depo8+bNRX4Op9Op0qVL648//tCyZct0xx132B+75557dPvtt5/wOf369dO6devkcrk0b948bd++Xffcc4/27dunxMREe/oWi7lRXCgWOMGxi7Xr1aunjRs3Gk4EAEBg27Jli9asWaMrrrjCvq24SoXb7ZbL5dJ//vMfdezYUZK0a9cu9ejRQ7GxsUpLS1Pv3r31yCOP5OvxfDtSxcbG6ujRoyzmRrFhbgtOEBMTI0mKiIigVAAAkA916tRR6dKlJUmRkZHyer3FUip8jy3JLhVS7hSn1q1bKy0tTZL0xRdfaO3atad8jJtvvlmtWrWSJFWpUkVer9e+iOj1elW2bNliyYrwRrFAHi1atGCxNgAAhdChQwd5vV5lZmYW6+OeanLJ6NGjNWjQIPv9J554wn77iy++UGJiohITE7Vq1SqtWbNGHo9HKSkpGjBggGJiYpSUlGSXlcOHD+uuu+4q1twIPxQL2IYNG6bly5dLkn744QcWawMAEAC++eabPP/12bBhg0aPHm2/f9VVV9lvv/nmm/bbt9xyiz3VqXv37nr66aeVnp6uzp07a9asWerXr58k6f3339f06dNL6stAGOCVIyRJR48e1ZAhQyTl/mI6do4oAAAwp2/fvpJ0wkhInz59FBkZqYsvvliSNG7cOPtjQ4cOtXeduvTSS7Vv3z5dd911ec6jqlmzphwOhz7//HPVq1dPUu5J3UBhsXgbkv452TMuLk4pKSmm4wAAAOWe2L1lyxZ16dJFI0aM0O+//67p06fr119/lSRNnDhR11xzjb78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lK/D75555hlJ0hNPPFGimYBQ4dskITExUVu3bi3x5xs8eLA8Ho/atWtX4s8FwCzf2TTDhw83nCT0sMYiHziZESeze/duVatWTZK0YMEC+6T2U0lMTFS3bt30448/+iMeENQOHjyoZs2aac+ePbIsSwsWLCix5+rRo4d27dqlK664Qj/++KMcDscZD8EEEJyys7MVGRkpidd1JYERiwJg1ALHqlq1qj1tok2bNrrjjjtOe/9zzjlHP/30Ey9YgHwoX768du7cqe3bt8vtduuqq64qsefatWuXJNmlv0yZMiX2XADMGjt2rCSd8WIgCodikQ++xbfXXXed4SQINC6XSy1btlSZMmW0bNmy0973ww8/lGVZ9pUSAGdWvXp19ejRQzt37lSrVq30zjvvFPtzvPXWWxo5cqR9GN/BgweL/TkABAbflORzzjnHcJLQRLHIB98P4dy5cw0nQSBauHChoqOjJeWOXJyKZVmaO3euPB6POnTo4K94QND77rvvFBkZKY/HY19tLE7t2rXThg0bdOjQIQ0aNKjYHx9A4EhOTpYk/fDDD4aThCbWWORDVlaWvZc63y6czK233qrx48fL6/Wqd+/eeuSRR05532uvvVbbtm1jhyiggG6//XaNGzdOCQkJmjJlSpEfb8+ePerZs6c9pTEiIkJpaWn52pABQHBi3WzJYsQiH46dupKfrUURfj788EN5PB5FRkbq+++/P+19L774Ynm9XnvXGwD5M3bsWJUrV65Yzpl46qmndOWVV8rtdmv48OHyeDzKysqiVAAhbObMmZLEgccliGKRT75ycc899xhOgkDmdrvP+Avr7rvvltPp1NNPP+2nVEDouO++++T1ejVy5MhCP8aoUaP022+/aeDAgXK73XriiSd4oQGEgT59+kiSatasaThJ6KJY5NPVV18tSfr000/NBkFAq1q1qlJTU7Vz587T3q969eo6cOCAlixZ4qdkQGj497//re7du2v8+PGFLheffPKJ4uLiilROAAQf3w5wn3/+ueEkoYs1FvnEOgvkl8PhUNmyZfXbb7+d9n4tW7ZUXFxcsUzrAMJNjx499P333+v5559Xt27dCvS5rVq1UuXKle0XGQDCA+srSh4jFvl07DqLjIwMg0kQ6KpXr65Dhw6d8X4NGjRQSkqKHxIBoWfy5MmqWbOmnnnmGe3YsaNAn+vxeBQR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p1Nutzvs1sXcfvvtWr58OVd8AKCYVKtWTbt37y7Q35PExETFxcXpgw8+4IVyPnB2RWCiBpeQ+Ph4zrQwaMSIEfZVnmnTpuUpFT7PPvusduzYoZdeekkffvihfWjRzTff7Nespo0dO1Z33nmnvF4vxQIATqFFixbq0KGDqlSpYp+P5PPJJ5/YZyRdccUV2r17t66//vp8P/bcuXMl5U7TpVTkD2dXBCaKRQm66667JElvvPGG2SBh5sCBA3ryySflcrm0ePFiRUZG2h/bt2+f2rVrJ0nq2LGjqlevrscff1yWZal9+/a69dZb9ddff5mKboyvhDmdTv3000+G0wBA4Fm+fLnmzJmjvXv36vfff1fFihUVExMjy7J000032Wcm/fTTT2rWrJkee+yxfD+272yKTz/9tESyh6J169ZJkn777TfDSXAspkKVMN+oxd69e1WpUiXDacKDb7vf2bNn51mM7fV61bZtW+Xk5Khv3772L/CuXbvm+cVUunRpTZ061e+5A8Ell1yipKQkde7cOWy/BwBwMsfuyuTbRvZYcXFxmj59eqEe++DBg+rataveeOMN3X///UWJGRbGjRun22+/XRLToAINIxYlLCEhQZLsw1tQsvbu3ausrCzdf//9dql49NFHlZiYqJYtWyonJ0evvvpqnqtCM2bMsK/YJyYmhvUL6t9//13du3fXtGnTVLp0aWVnZ5uOBAABYeTIkfbbl1xyiV599VX7/cWLFxeqVIwfP16dOnVS165dFRUVRanIpzvvvFOSdMEFFxhOguMxYlHC/v77b5111lmSaNX+MHLkSN13332KjIxUTk6OvWbguuuuU69evU46d3Xq1Knq0qWLZs6cqdjYWH9HDkjffPONXnzxRUVHRys9Pd10HAAwzuPxKC4uTunp6Zo/f76cTqfuvvtuDR48WDVr1izw4/3444/697//LYfDoY8++kj9+vUrgdSh59hF2x6Ph/M9AgwjFiWsZs2a9g993759DacJfYMGDdK5554rKXdXjiFDhsjr9eqrr7465YK4iy66SFLu/Fnkuuaaa/TAAw8oIyPDdBQA8JvExERZlqWLL744z+07duyQ0+lUenq6xowZI5fLJcuy9N577xWqVEiyt0J3u92UigKoU6eOJNnrWxBYGLHwgxdeeEGDBw+WxKhFIMrJyVFERITGjx+vpk2bmo4TMLxer1q2bKmoqCgtXLjQ3oEDAEJVzZo17RLhcrn0448/qkuXLnI6nfJ6vZo7d669SLsoli1bpgEDBigzM1MbNmwo8CF64cxXJvi+BSZGLPzg6aeftt/etWuXwSQ4ma+++kqSKBXHsSxLL730krKzs9W8eXN17tzZdCQAKFF///23pNxRhMzMTI0aNUrr1q2Tx+MptlIhSRMnTlRmZqYkqUGDBsXymOFgxIgR9tuUisBEsfAT31BpvXr1DCfB8XxTpI4tgMh18cUXa+HChSpbtqxmzJhhOg4AlKi33npLUu5UWim3YLRp00aSiq1USNLQoUPtx33kkUeK7XFD3ZNPPilJ6t27t+EkOBWKhZ9s3bpVkpSRkaGcnByzYWB77rnnFBUVpTJlyujXX381HSdguVwueb1etW/f3nQUACgx9913nyTpiiuukCR9//33Onz4sIYMGVLszzVq1ChJ0ubNm4v9sUPRihUr7Onkn3/+ueE0OBWKhZ84HA77oLbzzjvPcBpIuSecPvvss3K73Tp8+DCnd55G6dKlJeWeDrtw4ULDaQCg+EyfPl1OpzPPi9VbbrlFixcv1vz58zVr1ixdddVVJfb8FIv8adu2rSSpRo0ahpPgdCgWfjRp0iRJ0qpVqwwnCW933323LMtSpUqVZFmWunfvrq5du2rWrFmmowWsL7/8Uj/88IMkqXXr1qpbty5nXAAICdu3b5fH41Hfvn3thcG+C4Eul6tELzo1aNBAy5Yt4/fpGXi9XqWlpUn658RtBCaKhR9169bNfvvBBx80mCS8ffDBB4qOjlabNm3sfcRfeOEF07ECXpUqVewrelu2bFFkZKQsy9KHH35oOBkAFN6GDRvst6tUqaJZs2b5bRtT3wFvTJE+Pd828i6Xi/OmAhzFws98i7TeeOMNs0HCVNmyZeV2u9WxY0eNGjVKlSpVMh0pqDRo0EBTp061TyqXpJ07dxpMBACF07lzZ3322WcaPny4IiMjdd555+k///mP36bF3n777Ro7dqzKly/PVNwz+OuvvyRJs2fPNpwEZ8I5Fgb4roR8/fXXuvbaaw2nCS/x8fFKTU213x8xYoS6dOliMFFweu6557R8+XJt375dK1assK8mAUAwiImJUUZGhlwul3JyclShQgX98ssvfs2QmJioChUqaP/+/X593mDTp08fffHFF5I4CywYMGJhQKtWrSTl/mOBf6WkpMjr9crr9apGjRp6/PHH7e0FfX755RetWLHCUMLg8O9//1vvvPOOJKlZs2ZyOBzyeDyGUwHA6X300UeyLEsZGRmS/pmCdODAAb+8aJ0zZ45atmypxMRESdKNN95Y4s8Z7Hyl4tgzLBC4GLEwwOPxyOl0Sso9MK9q1aqGE4Wv9u3ba+7cuVq8eLH+85//6NNPP7U/dsUVV+i5554zmC6wpaamqnPnzipXrpwOHjwoSXI6nTpy5AhzYAEEjP379+vaa6/VnDlz5Ha7FRERoezsbFWoUEEffPCBrr76aklSbGysZs6cWWI5nn/+eU2ePFmxsbHavHmzKleuXGLPFSref/993XXXXZIYrQgWFAtDKleurH379ik+Pl5HjhwxHSdspaamKj4+3n6/UaNGWrt2rSIiImRZlubNm2cwXfDIzs5Wt27dlJSUJCl3e+XmzZurf//+uvPOO+V0OrVr1y7169dP06ZNM5wWQDipWLGiDhw4IMuyNGvWLEVHR+f5eMuWLeX1ejVo0CDdfPPNxf78s2fP1gMPPCBJuummm/TRRx8V+3OEKqfTKY/Ho+7du2vy5Mmm4yAfKBaGHDlyRAkJCZJyX5S5XC7DicLXK6+8ounTp+v999/X3LlzNXz4cC1dulRvvvkmB8IV0KeffqopU6Zo5cqV9tWlhIQEHT58WLVq1dL27dt1/fXX68svv/R7tjvvvFNLly5VvXr17KF1AKHP4XCobNmy+u2330768W3btmnr1q3q1KlTsT/3119/rREjRiguLk579+5lNLcAVqxYoebNm0titCKYUCwMio6OVmZmpipVqqS9e/eajhP2mjVrppUrV0rKHVH68ccfDScKbm63W998841GjBihyMhIZWVlybIseb1ebdy4UW3atFFGRoYSEhLsfeRLomBnZ2dr//79ql69un3b9u3bVbNmzWJ/LgCBx7IsDR482J7y5C/t27dXZmamXC6X0tLSFBER4dfnD3YRERHKyclRvXr1tHHjRtNxkE8s3jZo27ZtkqR9+/YZTgJJatOmjf12dna2MjIylJKSoldeeYXDiwrB6XTquuuu0x133KGsrCzVqVPHvupUv359HTx4UDk5Odq5c6ecTqciIiIUFRWlunXrqmHDhurVq5fmzJljjy4MGjRIvXr10nXXXadDhw5p2LBhGj58uIYP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LpDH49FVV10lSbrxxhsNpwJCn+/K+JEjR/Lc7vF47Dn+heHxeIqUK1ytXr3aPoB07969htOguFAsAlRiYqK9r71v/iFCV5kyZSRJ119/vdkgKDETJ07MsyvUe++9p1KlSun1119XcnLySbepBVB83n33XUlSnz59lJiYqFtuuUWS1Lp1a3Xp0kU9e/bUihUrTvsYXq9XaWlpeu6553TllVcqMTFR06dPV9euXUs6fshp3ry5JKlu3bqnPGAWwYf5FgFsx44dqlixovbt26ft27fbw7gILR999JFSUlJ07733MgUqRD3++OOaMmWKIiMjlZmZqYoVK2rs2LFauHChqlatKkl65plnDKcEQlurVq0UHR0tp9Op1q1ba+rUqZo0aZK8Xq+aNGmiNWvW6LbbbtPAgQPt0nG8rl276tChQ3kek10cC27MmDH29rwbN240nAbFicXbAa5ixYo6cOCA/YIEoeX888/Xn3/+KUlavHix4TQoCZs3b9b111+vqKgo/fXXX6pXr54kqV27dpo3b559P34VA/7l24nv2L+vjRs31rp162RZlipXrqwDBw6oadOmuuOOO3TffffJ6/WqatWqmjhxotq2bWsyflDzfe8vuugiTZkyxXAaFCemQgU43wmUWVlZxbqLBczyeDxq166d/vzzT91+++2UihDWp08fOZ1OZWRk2KVCkubOncv6KcAg33lBx24Tu3btWk2ePFlly5bV3r17FRUVpeXLl2vQoEHyer2KiorSli1bKBVFMHjwYPttSkXoYd5FgHO5XGrRooWWLVumzp07s0gsRFSoUEFJSUmSpCVLlhhOg5Iyfvx4ud1uzZw586QfdzgcjFQAhpzqgk737t118OBBP6cJHy+88IIk6b777jOcBCWBEYsgsHTpUkm5UyWefPJJw2lQHLp27SqHwyGXy6Vly5Zp4MCBpiOhmLndbo0cOVL169dXx44dTccBAOPat29vv/3mm28aTIKSQrEIEnfccYck6aWXXjKcBEXl8Xi0du1azZ07V+vWrZMkLVu2zGwoFLsuXbpIyt0yGgDCXWZmpubOnStJmjRpkuE0KCks3g4ivsVObdu2tf9xIvhccMEFmjVrlv1+bGyspk+fbh+KiOC0adMm1alTRw6HQ8nJyerSpYsWLVqUZ4tZAAhXZcuW1eHDh9mMJsRRLILI5MmT7QO1cnJy5HQ6DSdCYSQkJCg1NVWvv/66vvzyS40cOdJ0JBTRnXfeae/u5eNwOFicDQDKXRR/9tlnS5IOHz6shIQEw4lQUigWQSY6OlqZmZkqU6aMvfgXwcU38uQzduxY+6AgBI9XX31V06ZNU1JSkrKysiRJF154oR555BE9+OCD+u233zj0DgAkOZ1OeTwe1a5dW1u2bDEdByWIuRdBZufOnZJyG/+mTZsMp0Fh/P3337r55ptVqlQpSVL//v0NJ0JBDRw4UF988YUOHDhglwopd1rbFVdcofXr11MqAEDSyJEj7R0tN2/ebDgNShrFIsiUL19ederUkSQ1bNjQcBoURo0aNfTf//5Xqamp2rNnjzwej66++mp16NBBl19+uXr16qXs7GzTMXEMj8ejJ598Ujt27FDr1q01f/589evXTzk5OXm2C/b92wQA5PJtK9urV68TRuwRepgKFaR8/zjvvfdejR492nAaFMXtt9+ucePGybKsPPPy77nnHt1+++2G04Wf++67T4sXL9aFF16oyy+/XO3bt1e7du2Uk5MjKXdIf86cOWrdurX9OU2bNlX9+vX13XffmYoNAAGnadOmWr16tSzL4hyuMEGxCFKPPfaYXnnlFUm5++Wzo1Do2LZtm1q2bKn9+/erbdu2mjdvnv2xr7/+mqviJejyyy/X/v37T/qx6OhoXXLJJZo8ebKfUwFA8Dl8+LDKli0rSZo3b57atGljOBH8gWIRxHyLoSpXrqw9e/aYjoNidskll+iPP/5QlSpVtGLFCtWsWVOZmZlasGABO4IVwdGjR7V27Vqdf/759m379+/X5ZdffsJ9t27dqtatW+upp57ilFgAKICIiAjl5OSobNmyOnTokOk48BOKRRDbu3evqlSpIklat24day5CnNvtlsvl0r/+9S+9//77puMEpWeeeUY//fSTJGnx4sWSck+0b9Wq1Qnbw27ZsoUF2ABQCCNHjrQvxng8HtZWhBHmzwSxypUr29NifPtDI3RlZGRIks466yzDSYLLCy+8oMTERF100UX66aef7JGKm266SV6vV23btpXX67VLhWVZcrvdlAoAKCQWbIcvikWQ823d5vF41Lt3b8NpUJJ8e39///33hpMEl0mTJikmJkYZGRlq27atPVKRnp6u9u3b24uyfeuUHn74YdYsAUAh1a1bV1LuRZoJEyYYTgN/YypUCBg1apQGDRokiYXcoczj8ahOnTravn27/eIYp/fCCy9o0qRJOv7XnK9oSLmFwuPx2P/l3xAAFM7mzZtVr149SdLKlSt1zjnnGE4Ef+OvZwgYOHCgoqKiJEnx8fGG06CkOJ1Obd++XX369DEdJWjMmDHjpMPw6enp8nq98nq99sd9WyEeu40sACD/6tevL0mqWbMmpSJMUSxCRGpqqiQpLS1N7733nuE0KAkPPfSQpNxdjZA/1157rbxerypWrHjK++Tk5Mjr9SotLU3r16/XwoUL/ZgQAEJDz5497dHh7du3G04DUygWIcLlcunqq6+WJN19991mw6BENGrUSJLsaW+S9NRTTykxMVGJiYm66667TEULWHfccYf69OmjAwcOaPfu3ae833vvvaekpCQ1aNCAhYYAUEAZGRn69ttvJUlvvfWW2TAwijUWIcbhcMjr9apOnTr2wm6EhmrVqmn37t1yOp1yu9365JNPdOONN6pcuXL2HuGsvTi5xMRERUREKCMj44T1E1FRUcrKyjphu1kAQP5ER0crMzNTsbGxjKqHOUYsQsyff/4pKXcHoalTpxpOg+L0559/qkGDBvbi4htvvFGSVKpUKfs+vtPYkdcdd9yh7Oxs7dmzR127dpVlWbIsSxEREXapiIyMNB0TAILOk08+qczMTEnSwYMHDaeBaRSLENOiRQudd955kqQuXboYToPiVKVKFa1fv14ZGRnasmWLSpcurZkzZ2rLli12yfj5558NpwxMH3zwgaTc815+++03nXPOOerUqZO8Xq8qVKggj8ej/v37G04JAMElKytLL730kiTp/vvvV3R0tOFEMI2pUCHKNyWqVq1a2rp1q+k4KGHx8fFKTU3VhAkTTjjYzbeVarhauXKlbr31Vvv9u+++W3fccYf9vsfjUatWrZSSkqK4uDgTEQEgKPmmQEVFRdlbeCO8he+rjRC3bNkySdK2bds0ZcoUs2FQ4mJiYiRJtWrVynP7I488olatWql9+/bq2rWrunXrpkmTJpmIaIxvNy2Hw6ERI0bkKRWS1LdvX0miVABAATz++OP2FKjDhw+bDYOAwYhFCPvXv/6lpUuXStIJB4QhtHg8HjmdTsXGxqpnz56aOHGiqlWrps2bN6t58+Zavny5LMuyz21YtGjRSR8nJydHDocjZEY4LrjgAqWlpUmSXnrpJV188cV5Pt66dWu53W61aNHC/rcCADi97Oxse13awIEDNXLkSMOJECgoFiHONyWqdu3a2rJli+k4KEFbt25VnTp1JOXudJSZmXnCTkelS5dWamqq5s+fL6fTmefzk5KSdMkll0iSZs6cqdjYWP+FLyE9evTQrl27VLt2bU2YMEFSbslu2bKlXbTS09OZFwwABcAUKJxKaFyWxCktX75cUu6LTqZEhbbatWvbp0lnZGTI6/WesH3qmDFj5PV67dOlMzIy1KpVKyUmJtqlQpJWrFjh1+wl5bvvvtNdd92lzz//3L7Ndy3F6/XqnHPOoVQAQAE8+uijTIHCKTFiEQbOO+88e80F/7shSZZlqVWrVnK73VqyZInmzp2rPn36aNu2berRo4eeeeYZ0xFLjG/EYs+ePapcubLpOAAQNI6dAjVgwACNGjXKcCIEGopFmPBNiapSpcppTyBGeBg2bJiGDBliv+9bo1GpUiX9+OOPBpOVvP79+2vp0qWqWrWqdu3aZToOAASNyMhIZWdnMwUKp8RUqDCxceNGSdKePXvsPf0RvgYPHqzrr79eTZo0kfRP8Xz33XcNJys59957rxITE7V06VJZlqULL7zQdCQACBrXXnutsrOzJTEFCqfGiEUY6dmzp7799ltJubv/HL94F+HHt+Db4XDo6quv1lNPPWU6UolJTExU69attWDBAh09ejQkFqcDgD9s2rRJ9evXlySNHDlSAwcONJwIgYpiEWaioqKUlZWl6Ohopaenm44Dw3xToAYNGqSbb77ZdJwSlZiYKEmqU6eONm/ebDgNAAQP36h2tWrVtHPnTtNxEMCYChVmjh49Kil3N6DevXsbTgPTfLtGLViwwHCSkjdmzBhJ0sMPP2w4CQAEj8aNG9sbv1AqcCYUizDjcrk0YsQISdKXX36pNWvWGE4Ek7p16yZJevvttw0nKXn33nuvnE6nBgwYYDoKAASFUaNGad26dZKkefPmGU6DYECxCEOPPfaYatSoIUn24l2Ep6ysLEnShRdeqKSkJMNpSlZ2drbKly9vOgYABIWcnBwNGjRIknTFFVeoTZs2hhMhGLDGIoxZliVJbEEbxtxutxo1aqTNmzfL6/UqNjZW06ZNC8mF/YmJiTr33HND5vA/AChJvq1lIyIi7ItQwJkwYhHGtm7dKil3C9rhw4ebDQMjnE6nNm7cKI/HoylTpigtLU2tW7dWz549TUcrNg899JDatWsnSfrXv/5lOA0ABL5u3brZW8umpqYaToNgwohFmOvdu7e+/PJLSVJycrJKly5tOBFM2rhxozp16qRdu3apdevWGj16tOlIRdayZUt5vV7VrVtXmzZtMh0HAALa9OnT1blzZ0m5ayxYl4aCoFhA8fHxSk1NlcPhsHcJQnjzbUvs88gjjwTtLmKtWrVSdHS0vSMaAODkvF6vHI7cySxnn322Vq9ebTgRgg1ToaDk5GRJuWca1K5d22wYBITdu3erQYMGeu655+RyufTmm2/q0KFD8ng8pqMV2J133qm0tDTTMQAg4JUqVUpS7rkVlAoUBiMWkCStWrVK5557riRp2LBhevrppw0nQqAoXbq0UlJSJOX+sVm4cKHhRAXTpUsXJScni191AHBql19+uX755RdJuTsGRkREGE6EYMSIBSRJ55xzjj3VZfDgwVq/fr3hRAgUR44ckdfrldPpDLoRiz59+tgjcgCAk/vkk0/sUjFq1ChKBQqNEQvkUblyZe3bt0+SuMKLPHzbE0dGRmr27Nn2PNxA9eSTT+r333/XU089pV69erEjFACchNvtlsvlkiRdcMEFmjFjhuFECGaB/coAfrd37177bQ4Tw7E2bdqk0aNHKysrS6+//ro9PSpQ+dYLvfDCC5QKADiF6OhoSVJERASlAkXGiAVOsHPnTvtk7ksuuUS//fab4UQIJJUqVdL+/fslSYsXLzac5tRWrFih2267jZE3ADiFWrVqafv27ZJYV4HiwYgFTlC9enWNGTNGkvT777/rv//9r9lACCj79u1Tp06dJEnp6emG05xa9erVTUcAgIB166232qViyZIllAoUC4oFTuqOO+7QhRdeKCn3lw9nAOBY06dPlyRt3rzZbJDTWLZsmekIABCQZs6caV80HDJkCNNFUWyYCoXTiouL09GjR2VZVtDtCISSZVmWHA6HvvzyS9WpU8d0nBOMHTtW77zzDlOhAOAYHo9HTqdTktSoUSOtXbvWcCKEEkYscFpHjhyRlLtDFIu5caw///xTHo9H1113nTIyMkzHOQGHOwHAiSIjIyXlLtamVKC4USxwWg6HQzt27JAkHTp0SO3atTOcCIGgQ4cOeYbOX3zxRYNpTm7WrFkqV66c6RgAEDCqVq0qt9stSUxxRomgWOCMqlevrrFjx0qS5s2bp2effdZsIBg3d+5cxcfHa/Hixfr111/1/PPPm450goiICKWmppqOAQABoXv37tqzZ4+k3DVoLNZGSaBYIF9uu+02XXnllZKk5557joWxYSw1NVVer1dnn322pMA976RUqVKsCwIA5a45++GHHyRJzz//vJo3b244EUIVi7dRIDVq1NDOnTslsed1uLruuus0YcIETZ8+XXFxcabjnNJDDz2kmTNnsngbQFg79myqTp062bv6ASWBEQsUyI4dO+RyuST9c1onwktCQoIk2YfkBapzzz3XdAQAMMrj8dilokKFCpQKlDhGLFBgx25VV7p0aSUnJxtOBH+yLCvP+7GxscrOzlZ2drbuvvtu3XHHHYaS/WPHjh26+uqr2UoRQFhzuVxyu91yOBz2om2gJFEsUCi7d+9WtWrVJEmJiYlatGiR4UTwl7S0NE2ePFmzZ8/W7t279eOPP8rhcKhhw4Zavny5nE6nZs+ebXSaXLt27ZSVlcU0KABhq1KlSvbIMlOX4S9MhUKhVK1aVR9++KEkafHixXrkkUcMJ4K/xMbGqnfv3ho1apQmTpyojIwMpaWladmyZVq3bp3cbre6du1qLN9HH32krKwsLVy40FgGADCpdevWdqlYvnw5pQJ+Q7FAod1yyy265557JEmvvfaaXnrpJcOJYFrDhg119tln2wcr+ttnn32mt956S9WqVVPLli2NZAAAk3r27GlfWJk4caKaNWtmOBHCCcUCRfL222+re/fukqQnn3xSf/75p+FEMG3fvn1Gnvftt9/W66+/rooVK9o7lwFAOHn//ff17bffSpKGDh2qa665xmwghB3WWKBYVKtWTbt375Ykpaens2NUGNu2bZtq164tKXcEoWHDhn553tatWys6OppD8QCEpXnz5qldu3aSpDZt2mjevHmGEyEcUSxQbEqVKqW0tDRJLBQLd9u3b1etWrUUExOjWbNm+eU5fX9QMzMz/fJ8ABAofL9zJalBgwZav3694UQIV0yFQrFJTU21t6GNiooynAYmtWjRQpL0wAMP+O05o6KilJWV5bfnA4BAkJqaapeK0qVLUypgFMUCxcayLGVnZ8uyLHm93oA+lRnFq3LlyrIsS9HR0brhhhuUlJSk8uXLq1evXn7L4Cu1HAAFIFx4PB6VLl1aUu6htZwrBdMoFihWlmVp165dkqSjR48qPj7ecCKUNI/Ho3379ql+/frKycnRV199JUn64IMP/JZh165dOnz4sCRpzZo1fnteADDF4/EoIiJCXq9XlmUZ240POBZrLFAi9uzZo6pVq0qS6tatq02bNhlOhOKSkpKi5ORkvfDCC3r33XflcDjk8Xi0aNGiE07l9peLL75Yhw8f5kA8AGEjNjZW6enpsixLmZmZrGtEQGDEAiWiSpUq9j7amzdvVmJiouFEKIqYmBhFR0crLi5OpUuXVs2aNbVs2TJJktfr1XPPPWesVEjS4cOHVaFCBT377LNFehyPx2OPfABAoKpYsaLS09MlSWlpaZQKBAxGLFCiPvroI918882SpPPPP1+LFy82nAgFtXLlSjVr1kwRERHyeDyyLEs5OTmSJIfDERAnXC9atMg+rLFRo0Zau3btSe+3ZcsWXXLJJYqJidGqVatUp04dTZs2TZK0detWXXjhhZKknJwce80GAASSihUr6sCBA5KkVatWqWnTpoYTAf+gWKDEjR8/XrfccoskqX379po9e7bZQCiQunXrasuWLXYpzMjI0KBBg5SWlqZq1arplVdeMZzwHz169NCuXbtOmBKVnZ2tJ598UkuWLLEXd8fHxyslJSXP/eLi4pSamspZLAACUtWqVbVnzx5J0l9//aUmTZoYTgTkRbGAX3z88cf6v//7P0nS4MGDNXToUMOJkF//+c9/9NBDD9lrKXwSEhI0ZcoUv2bJyclRt27dNGjQIPvE92MlJiae9GAoh8Nhl42zzjpL33zzzSmfwzdtr1+/ftqwYYM9ItO5c2dNnTq1uL4UACiQyy+/XL/88oskSgUCF8UCftOzZ099++23kigXweall17SK6+8ovLly6t79+5auXKlfv/9d7300ku6+OKLS/S5X375ZXXq1Enjxo3TkiVL8nysatWqevLJJ+3D8S655BIlJSXlGbEYNmyYhgwZooULF8rhOPWysoyMDEVHR2vlypW6//77T7rDyrp16/x2kjgA+BxbKoYPH64nnnjCcCLg5CgW8KtrrrlGkyZNkkS5CHaWZal06dIlchX/8OHDJy0sF198sVq3bq3hw4erfPny2r9/vyRp8eLF+uqrr/Tyyy+ratWq9pbHvpwREREnjGIca8qUKXr88cftqVCSThih8alXr542btxY1C8RAPLl2FLx4osv6sknnzScCDg1igX8jpGL0GBZluLi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KHAA+mlVLi4u8PHxwZgxY/D222/D39+/nKut2M6ePYuYmBisW7cOaWlpKCgoeOlokoG9vT28vb0xfPhwfP7559KO7kREVD4YLIiIzESj0WDbtm1YtWoVzpw5g7y8vFJ3nDKED6VSibZt26JXr14IDAxEYGBgOVVtHfv27cMff/yBLVu24Pr168jKyoJKpSpxaDCQyWRwd3dHp06d8NlnnyEkJIQBgojIShgsiIgsQKfTIT09HdOnT0dsbCwePnyIp0+fmtTq1tAGFdBP76lWrZr0taCgIHTu3LnIOYMHD4aXl1eZ7pecnIyYmJgix2NiYpCcnCy9zszMlEZxRFE0+Xt0cXFB3bp18frrr2PRokVwcnKCTMZu6UREtobBgojIRuh0OqSlpWHWrFlIS0vD8ePHUVhYiLy8PACoNPttGAKRUqmEnZ0devbsierVq2PhwoUMDUREFRiDBRFRBfTsj26VSoXp06dDpVIBAO7du4cTJ04UCSIqlUp6T1k5OzsbbTwIAHK5HD169JBGTLy8vDBr1iyjgMAF0kRElR+DBRERERERmYzjzUREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhkDBZERERERGQyBgsiIiIiIjIZgwUREREREZmMwYKIiIiIiEzGYEFERERERCZjsCAiIiIiIpMxWBARERERkckYLIiIiIiIyGQMFkREREREZDIGCyIiIiIiMhmDBRERERERmYzBgoiIiIiITMZgQUREREREJmOwICIiIiIikzFYEBERERGRyRgsiIiIiIjIZAwWRERERERkMgYLIiIiIiIyGYMFERERERGZjMGCiIiIiIhMxmBBREREREQmY7AgIiIiIiKTMVgQEREREZHJGCyIiIiIiMhk/w9GtSHWpgpaQwAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#=====cycles through contours=========\n", - "\n", - "for i in range(len(cont)):\n", - "\n", - "\tx=np.append(x,len(cont[i]))\n", - "\n", - "#=====only takes values of minimum surface length and calculates area======\n", - "\n", - "\tif len(cont[i]) <= 100:\n", - "\t\tcontinue\n", - "\tarea=0.5*np.abs(np.dot(cont[i][:,0,0],np.roll(cont[i][:,0,1],1))-np.dot(cont[i][:,0,1],np.roll(cont[i][:,0,0],1)))\n", - "\tarcar=(area*(dattoarc**2))\n", - "\tif arcar > 1000:\n", - "\n", - "#=====finds centroid=======\n", - "\n", - "\t\tchpts=len(cont[i])\n", - "\t\tcent=[np.mean(cont[i][:,0,0]),np.mean(cont[i][:,0,1])]\n", - "\n", - "#===remove quiet sun regions encompassed by coronal holes======\n", - "\n", - "\t\tif (cand[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0) and (iarr[np.max(cont[i][:,0,0])+1,cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1]] > 0):\n", - "\t\t\tmahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate)\n", - "\t\t\tiarr[np.where(slate == 1)]=0\n", - "\t\t\tslate[:]=0\n", - "\n", - "\t\telse:\n", - "\n", - "#====create a simple centre point======\n", - "\n", - "\t\t\tarccent=csys.all_pix2world(cent[0],cent[1],0)\n", - "\n", - "#====classifies off limb CH regions========\n", - "\n", - "\t\t\tif (((arccent[0]**2)+(arccent[1]**2)) > (rs**2)) or (np.sum(np.array(csys.all_pix2world(cont[i][0,0,0],cont[i][0,0,1],0))**2) > (rs**2)):\n", - "\t\t\t\tmahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),offarr)\n", - "\t\t\telse:\n", - "\n", - "#=====classifies on disk coronal holes=======\n", - "\n", - "\t\t\t\tmahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:,0,1],cont[i][:,0,0]))),slate)\n", - "\t\t\t\tposlin=np.where(slate == 1)\n", - "\t\t\t\tslate[:]=0\n", - "\t\t\t\tprint(poslin)\n", - "\n", - "#====create an array for magnetic polarity========\n", - "\n", - "\t\t\t\tpos=np.zeros((len(poslin[0]),2),dtype=np.uint)\n", - "\t\t\t\tpos[:,0]=np.array((poslin[0]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint)\n", - "\t\t\t\tpos[:,1]=np.array((poslin[1]-(s[0]/2))*convermul+(s[1]/2),dtype=np.uint)\n", - "\t\t\t\tnpix=list(np.histogram(datm[pos[:,0],pos[:,1]],bins=np.arange(np.round(np.min(datm[pos[:,0],pos[:,1]]))-0.5,np.round(np.max(datm[pos[:,0],pos[:,1]]))+0.6,1)))\n", - "\t\t\t\tnpix[0][np.where(npix[0]==0)]=1\n", - "\t\t\t\tnpix[1]=npix[1][:-1]+0.5\n", - "\n", - "\t\t\t\twh1=np.where(npix[1] > 0)\n", - "\t\t\t\twh2=np.where(npix[1] < 0)\n", - "\n", - "#=====magnetic cut offs dependant on area=========\n", - "\n", - "\t\t\t\tif np.absolute((np.sum(npix[0][wh1])-np.sum(npix[0][wh2]))/np.sqrt(np.sum(npix[0]))) <= 10 and arcar < 9000:\n", - "\t\t\t\t\tcontinue\n", - "\t\t\t\tif np.absolute(np.mean(datm[pos[:,0],pos[:,1]])) < garr[int(cent[0]),int(cent[1])] and arcar < 40000:\n", - "\t\t\t\t\tcontinue\n", - "\t\t\t\tiarr[poslin]=ident\n", - "\n", - "#====create an accurate center point=======\n", - "\n", - "\t\t\t\typos=np.sum((poslin[0])*np.absolute(hg.lat[poslin]))/np.sum(np.absolute(hg.lat[poslin]))\n", - "\t\t\t\txpos=np.sum((poslin[1])*np.absolute(hg.lon[poslin]))/np.sum(np.absolute(hg.lon[poslin]))\n", - "\n", - "\t\t\t\tarccent=csys.all_pix2world(xpos,ypos,0)\n", - "\n", - "#======calculate average angle coronal hole is subjected to======\n", - "\n", - "\t\t\t\tdist=np.sqrt((arccent[0]**2)+(arccent[1]**2))\n", - "\t\t\t\tang=np.arcsin(dist/rs)\n", - "\n", - "#=====calculate area of CH with minimal projection effects======\n", - "\n", - "\t\t\t\ttrupixar=abs(area/np.cos(ang))\n", - "\t\t\t\ttruarcar=trupixar*(dattoarc**2)\n", - "\t\t\t\ttrummar=truarcar*((6.96e+08/rs)**2)\n", - "\n", - "\n", - "#====find CH extent in lattitude and longitude========\n", - "\n", - "\t\t\t\tmaxxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])]\n", - "\t\t\t\tmaxxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0])]\n", - "\t\t\t\tmaxylat=hg.lat[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]]\n", - "\t\t\t\tmaxylon=hg.lon[np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0]]\n", - "\t\t\t\tminxlat=hg.lat[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])]\n", - "\t\t\t\tminxlon=hg.lon[cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0])]\n", - "\t\t\t\tminylat=hg.lat[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]]\n", - "\t\t\t\tminylon=hg.lon[np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0]]\n", - "\n", - "#=====CH centroid in lat/lon=======\n", - "\n", - "\t\t\t\tcentlat=hg.lat[int(ypos),int(xpos)]\n", - "\t\t\t\tcentlon=hg.lon[int(ypos),int(xpos)]\n", - "\n", - "#====caluclate the mean magnetic field=====\n", - "\n", - "\t\t\t\tmB=np.mean(datm[pos[:,0],pos[:,1]])\n", - "\t\t\t\tmBpos=np.sum(npix[0][wh1]*npix[1][wh1])/np.sum(npix[0][wh1])\n", - "\t\t\t\tmBneg=np.sum(npix[0][wh2]*npix[1][wh2])/np.sum(npix[0][wh2])\n", - "\n", - "#=====finds coordinates of CH boundaries=======\n", - "\n", - "\t\t\t\tYwb,Xwb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.max(cont[i][:,0,0]))[0][0],0,1],np.max(cont[i][:,0,0]),0)\n", - "\t\t\t\tYeb,Xeb=csys.all_pix2world(cont[i][np.where(cont[i][:,0,0] == np.min(cont[i][:,0,0]))[0][0],0,1],np.min(cont[i][:,0,0]),0)\n", - "\t\t\t\tYnb,Xnb=csys.all_pix2world(np.max(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.max(cont[i][:,0,1]))[0][0],0,0],0)\n", - "\t\t\t\tYsb,Xsb=csys.all_pix2world(np.min(cont[i][:,0,1]),cont[i][np.where(cont[i][:,0,1] == np.min(cont[i][:,0,1]))[0][0],0,0],0)\n", - "\n", - "\t\t\t\twidth=round(maxxlon.value)-round(minxlon.value)\n", - "\n", - "\t\t\t\tif minxlon.value >= 0.0 : eastl='W'+str(int(np.round(minxlon.value)))\n", - "\t\t\t\telse : eastl='E'+str(np.absolute(int(np.round(minxlon.value))))\n", - "\t\t\t\tif maxxlon.value >= 0.0 : westl='W'+str(int(np.round(maxxlon.value)))\n", - "\t\t\t\telse : westl='E'+str(np.absolute(int(np.round(maxxlon.value))))\n", - "\n", - "\t\t\t\tif centlat >= 0.0 : centlat='N'+str(int(np.round(centlat.value)))\n", - "\t\t\t\telse : centlat='S'+str(np.absolute(int(np.round(centlat.value))))\n", - "\t\t\t\tif centlon >= 0.0 : centlon='W'+str(int(np.round(centlon.value)))\n", - "\t\t\t\telse : centlon='E'+str(np.absolute(int(np.round(centlon.value))))\n", - "\n", - "#====insertions of CH properties into property array=====\n", - "\n", - "\t\t\t\tprops[0,ident+1]=str(ident)\n", - "\t\t\t\tprops[1,ident+1]=str(np.round(arccent[0]))\n", - "\t\t\t\tprops[2,ident+1]=str(np.round(arccent[1]))\n", - "\t\t\t\tprops[3,ident+1]=str(centlon+centlat)\n", - "\t\t\t\tprops[4,ident+1]=str(np.round(Xeb))\n", - "\t\t\t\tprops[5,ident+1]=str(np.round(Yeb))\n", - "\t\t\t\tprops[6,ident+1]=str(np.round(Xwb))\n", - "\t\t\t\tprops[7,ident+1]=str(np.round(Ywb))\n", - "\t\t\t\tprops[8,ident+1]=str(np.round(Xnb))\n", - "\t\t\t\tprops[9,ident+1]=str(np.round(Ynb))\n", - "\t\t\t\tprops[10,ident+1]=str(np.round(Xsb))\n", - "\t\t\t\tprops[11,ident+1]=str(np.round(Ysb))\n", - "\t\t\t\tprops[12,ident+1]=str(eastl+'-'+westl)\n", - "\t\t\t\tprops[13,ident+1]=str(width)\n", - "\t\t\t\tprops[14,ident+1]='{:.1e}'.format(trummar/1e+12)\n", - "\t\t\t\tprops[15,ident+1]=str(np.round((arcar*100/(np.pi*(rs**2))),1))\n", - "\t\t\t\tprops[16,ident+1]=str(np.round(mB,1))\n", - "\t\t\t\tprops[17,ident+1]=str(np.round(mBpos,1))\n", - "\t\t\t\tprops[18,ident+1]=str(np.round(mBneg,1))\n", - "\t\t\t\tprops[19,ident+1]=str(np.round(np.max(npix[1]),1))\n", - "\t\t\t\tprops[20,ident+1]=str(np.round(np.min(npix[1]),1))\n", - "\t\t\t\ttbpos= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] > 0)])\n", - "\t\t\t\tprops[21,ident+1]='{:.1e}'.format(tbpos)\n", - "\t\t\t\ttbneg= np.sum(datm[pos[:,0],pos[:,1]][np.where(datm[pos[:,0],pos[:,1]] < 0)])\n", - "\t\t\t\tprops[22,ident+1]='{:.1e}'.format(tbneg)\n", - "\t\t\t\tprops[23,ident+1]='{:.1e}'.format(mB*trummar*1e+16)\n", - "\t\t\t\tprops[24,ident+1]='{:.1e}'.format(mBpos*trummar*1e+16)\n", - "\t\t\t\tprops[25,ident+1]='{:.1e}'.format(mBneg*trummar*1e+16)\n", - "\n", - "#=====sets up code for next possible coronal hole=====\n", - "\n", - "\t\t\t\tident=ident+1\n", - "\n", - "#=====sets ident back to max value of iarr======\n", - "\n", - "ident=ident-1\n", - "np.savetxt('ch_summary.txt', props, fmt = '%s')\n", - "\n", - "#====create image in output folder=======\n", - "#from scipy.misc import bytescale\n", - "\n", - "from skimage.util import img_as_ubyte\n", - "\n", - "def rescale01(arr, cmin=None, cmax=None, a=0, b=1):\n", - " if cmin or cmax:\n", - " arr = np.clip(arr, cmin, cmax)\n", - " return (b-a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a\n", - "\n", - "\n", - "def plot_tricolor():\n", - "\ttricolorarray = np.zeros((4096, 4096, 3))\n", - "\n", - "\tdata_a = img_as_ubyte(rescale01(np.log10(data), cmin = 1.2, cmax = 3.9))\n", - "\tdata_b = img_as_ubyte(rescale01(np.log10(datb), cmin = 1.4, cmax = 3.0))\n", - "\tdata_c = img_as_ubyte(rescale01(np.log10(datc), cmin = 0.8, cmax = 2.7))\n", - "\n", - "\ttricolorarray[..., 0] = data_c/np.max(data_c)\n", - "\ttricolorarray[..., 1] = data_b/np.max(data_b)\n", - "\ttricolorarray[..., 2] = data_a/np.max(data_a)\n", - "\n", - "\n", - "\tfig, ax = plt.subplots(figsize = (10, 10))\n", - "\n", - "\tplt.imshow(tricolorarray, origin = 'lower')#, extent = )\n", - "\tcs=plt.contour(xgrid,ygrid,slate,colors='white',linewidths=0.5)\n", - "\tplt.savefig('tricolor.png')\n", - "\tplt.close()\n", - "\n", - "def plot_mask(slate=slate):\n", - "\tchs=np.where(iarr > 0)\n", - "\tslate[chs]=1\n", - "\tslate=np.array(slate,dtype=np.uint8)\n", - "\tcont,heir=cv2.findContours(slate,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)\n", - "\n", - "\tcirc[:]=0\n", - "\tr=(rs/dattoarc)\n", - "\tw=np.where((xgrid-center[0])**2+(ygrid-center[1])**2 <= r**2)\n", - "\tcirc[w]=1.0\n", - "\n", - "\tplt.figure(figsize=(10,10))\n", - "\tplt.xlim(143,4014)\n", - "\tplt.ylim(143,4014)\n", - "\tplt.scatter(chs[1],chs[0],marker='s',s=0.0205,c='black',cmap='viridis',edgecolor='none',alpha=0.2)\n", - "\tplt.gca().set_aspect('equal', adjustable='box')\n", - "\tplt.axis('off')\n", - "\tcs=plt.contour(xgrid,ygrid,slate,colors='black',linewidths=0.5)\n", - "\tcs=plt.contour(xgrid,ygrid,circ,colors='black',linewidths=1.0)\n", - "\n", - "\tplt.savefig('CH_mask_'+hedb[\"DATE\"]+'.png',transparent=True)\n", - "\t#plt.close()\n", - "#====stores all CH properties in a text file=====\n", - "\n", - "plot_tricolor()\n", - "plot_mask()\n", - "\n", - "#====EOF====" - ] - } - ], - "metadata": { - "colab": { - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "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.12.1" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/chimera.py b/chimera.py deleted file mode 100644 index 646a6e9..0000000 --- a/chimera.py +++ /dev/null @@ -1,994 +0,0 @@ -"""Package for Coronal Hole Identification Algorithm""" -import glob -import sys - -import astropy.units as u -import cv2 -import mahotas -import matplotlib.pyplot as plt -import numpy as np -import scipy -import scipy.interpolate -import sunpy -import sunpy.map -from astropy import wcs -from astropy.io import fits -from astropy.modeling.models import Gaussian2D -from astropy.visualization import astropy_mpl_style -from skimage.util import img_as_ubyte - -plt.style.use(astropy_mpl_style) - -"""loading in the images as fits files""" - -file_path = "./" - -im171 = glob.glob(file_path + "*171*.fts") -im193 = glob.glob(file_path + "*193*.fts") -im211 = glob.glob(file_path + "*211*.fts") -imhmi = glob.glob(file_path + "*hmi*.fts") - -"""ensure that all images are present""" - -if im171 == [] or im193 == [] or im211 == [] or imhmi == []: - print("Not all required files present") - sys.exit() - - -def rescale_aia(image: np.array, orig_res: int, desired_res: int): - """ - Rescale the input aia image dimensions. - - Parameters - ---------- - image: 'np.array' - orig_res: 'int' - desired_res: 'int - - Returns - ------- - 'np.array' - """ - - if desired_res > orig_res: - scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) - dn = scipy.interpolate.RectBivariateSpline( - scaled_array, scaled_array, fits.getdata(image[0], 0) / (fits.getheader(image[0], 0)["EXPTIME"]) - ) - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - elif desired_res < orig_res: - scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], 0)) - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - - -def rescale_hmi(image: np.array, orig_res: int, desired_res: int): - """ - Rescale the input hmi image dimensions. - - Parameters - ---------- - image: 'np.array' - orig_res: 'int' - desired_res: 'int - - Returns - ------- - 'np.array' - """ - if desired_res > orig_res: - scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], ext=0)) - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - elif desired_res < orig_res: - scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], ext=0)) - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - - -"""defining data arrays which are used in later steps""" - -data = rescale_aia(im171, 1024, 4096) -datb = rescale_aia(im193, 1024, 4096) -datc = rescale_aia(im211, 1024, 4096) -datm = rescale_hmi(imhmi, 1024, 4096) - - -def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): - """ - Defines headers and filters aia arrays to meet header requirements - - Parameters - ---------- - aiaa: 'np.array' - aiab: 'np.array' - aiac: 'np.array' - aiam: 'np.array' - - Returns - ------- - 'np.array' - - """ - global heda, hedb, hedc, hedm, datm - heda = fits.getheader(aiaa[0], 0) - hedb = fits.getheader(aiab[0], 0) - hedc = fits.getheader(aiac[0], 0) - hedm = fits.getheader(aiam[0], 0) - if hedb["ctype1"] != "solar_x ": - hedb["ctype1"] = "solar_x " - hedb["ctype2"] = "solar_y " - if heda["cdelt1"] > 1: - heda["cdelt1"], heda["cdelt2"], heda["crpix1"], heda["crpix2"] = ( - heda["cdelt1"] / 4.0, - heda["cdelt2"] / 4.0, - heda["crpix1"] * 4.0, - heda["crpix2"] * 4.0, - ) - hedb["cdelt1"], hedb["cdelt2"], hedb["crpix1"], hedb["crpix2"] = ( - hedb["cdelt1"] / 4.0, - hedb["cdelt2"] / 4.0, - hedb["crpix1"] * 4.0, - hedb["crpix2"] * 4.0, - ) - hedc["cdelt1"], hedc["cdelt2"], hedc["crpix1"], hedc["crpix2"] = ( - hedc["cdelt1"] / 4.0, - hedc["cdelt2"] / 4.0, - hedc["crpix1"] * 4.0, - hedc["crpix2"] * 4.0, - ) - if hedm["crota1"] > 90: - datm = np.rot90(np.rot90(datm)) - - -filter(im171, im193, im211, imhmi) - - -def remove_neg(aiaa: np.array, aiab: np.array, aiac: np.array): - """ - Removes negative values from arrays - - Parameters - ---------- - aiaa: 'np.array' - aiab: 'np.array' - aiac: 'np.array' - - Returns - ------- - 'np.array' - - """ - global data, datb, datc - data[np.where(data <= 0)] = 0 - datb[np.where(datb <= 0)] = 0 - datc[np.where(datc <= 0)] = 0 - - -remove_neg(im171, im193, im211) - -"""defines the shape of the arrays as "s" and "rs" as the solar radius""" -s = np.shape(data) -rs = heda["rsun"] - - -def pix_arc(aia: np.array): - global dattoarc - dattoarc = heda["cdelt1"] - global conver - conver = ((s[0]) / 2) * dattoarc / hedm["cdelt1"] - (s[1] / 2) - global convermul - convermul = dattoarc / hedm["cdelt1"] - - -pix_arc(im171) - - -def to_helio(image: np.array): - """ - Converts arrays to the Heliographic Stonyhurst coordinate system - - Parameters - ---------- - image: 'np.array' - - Returns - ------- - 'np.array' - - """ - aia = sunpy.map.Map(image) - adj = 4096 / aia.dimensions[0].value - x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj - global hpc - hpc = aia.pixel_to_world(x, y) - global hg - hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) - global csys - csys = wcs.WCS(hedb) - print(csys) - - -to_helio(im171) - -"""Setting up arrays to be used in later processing""" -ident = 1 -iarr = np.zeros((s[0], s[1]), dtype=np.byte) -bmcool = np.zeros((s[0], s[1]), dtype=np.float32) -offarr, slate = np.array(iarr), np.array(iarr) -cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) -circ = np.zeros((s[0], s[1]), dtype=int) - -"""creation of a 2d gaussian for magnetic cut offs""" -r = (s[1] / 2.0) - 450 -xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) -center = [int(s[1] / 2.0), int(s[1] / 2.0)] -w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) -y, x = np.mgrid[0:4096, 0:4096] -garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) -garr[w] = 1.0 - -"""creates sub-arrays of props to isolate column of index 0 and column of index 1""" -props = np.zeros((26, 30), dtype="", - "", - "", - "BMAX", - "BMIN", - "TOT_B+", - "TOT_B-", - "", - "", - "", -) -props[:, 1] = ( - "num", - '"', - '"', - "H°", - '"', - '"', - '"', - '"', - '"', - '"', - '"', - '"', - "H°", - "°", - "Mm^2", - "%", - "G", - "G", - "G", - "G", - "G", - "G", - "G", - "Mx", - "Mx", - "Mx", -) - -"""define threshold values in log space""" - -with np.errstate(divide="ignore"): - t0 = np.log10(datc) - t1 = np.log10(datb) - t2 = np.log10(data) - - -class Bounds: - """Mixin to change and define array boundaries and slopes""" - - def __init__(self, upper, lower, slope): - self.upper = upper - self.lower = lower - self.slope = slope - - def new_u(self, new_upper): - self.upper = new_upper - - def new_l(self, new_lower): - self.lower = new_lower - - def new_s(self, new_slope): - self.slope = new_slope - - -t0b = Bounds(0.8, 2.7, 255) -t1b = Bounds(1.4, 3.0, 255) -t2b = Bounds(1.2, 3.9, 255) - - -def threshold(tval: np.array): - """ - Threshold arrays based on desired boundaries - - Parameters - ---------- - tval: 'np.array' - - Returns - ------- - 'np.array' - - """ - global t0, t1, t2 - if tval.all() == t0.all(): - t0[np.where(t0 < t0b.upper)] = t0b.upper - t0[np.where(t0 > t0b.lower)] = t0b.lower - if tval.all() == t1.all(): - t1[np.where(t1 < t1b.upper)] = t1b.upper - t1[np.where(t1 > t1b.lower)] = t2b.lower - if tval.all() == t2.all(): - t2[np.where(t2 < t2b.upper)] = t2b.upper - t2[np.where(t2 > t2b.lower)] = t2b.lower - else: - print("Must input valid logarithmic arrays") - - -threshold(t0) -threshold(t1) -threshold(t2) - - -def set_contour(tval: np.array): - """Sets contour values for bounded arrays - - Parameters - ---------- - tval: 'np.array' - - Returns - ------- - 'np.array' - - """ - global t0, t1, t2 - if tval.all() == t0.all(): - t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) - elif tval.all() == t1.all(): - t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) - elif tval.all() == t2.all(): - t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) - - -set_contour(t0) -set_contour(t1) -set_contour(t2) - - -def create_mask(): - """ - Creates 3 segmented bitmasks - - Returns - ------- - 'np.array' - - """ - - global t0, t1, t2, bmmix, bmhot, bmcool - with np.errstate(divide="ignore", invalid="ignore"): - bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 - bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 - bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 - - -create_mask() - - -def conjunction(): - """ - Creates a conjunction of 3 segmentations - - Returns - ------- - 'np.array' - - """ - global bmhot, bmcool, bmmix, cand - cand = bmcool * bmmix * bmhot - - -conjunction() - - -def misid(): - """ - Removes off-detector mis-identification - - Returns - ------- - 'np.array' - - """ - global s, r, w, circ, cand - r = (s[1] / 2.0) - 100 - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[w] = 1.0 - cand = cand * circ - - -misid() - - -def on_off(): - """ - Seperates on-disk and off-limb coronal holes - - Returns - ------- - 'np.array' - - """ - global circ, cand - circ[:] = 0 - r = (rs / dattoarc) - 10 - inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[inside] = 1.0 - r = (rs / dattoarc) + 40 - outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) - circ[outside] = 1.0 - cand = cand * circ - - -on_off() - - -def contours(): - """ - Contours the identified datapoints - - Returns - ------- - cand: 'np.array' - cont: 'tuple' - heir: 'np.array' - - """ - global cand, cont, heir - cand = np.array(cand, dtype=np.uint8) - cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - -contours() - - -def sort(): - """ - Sorts the contours by size - - Returns - ------- - reord: 'list' - tmp: 'list' - cont: 'list' - - """ - global sizes, reord, tmp, cont - sizes = [] - for i in range(len(cont)): - sizes = np.append(sizes, len(cont[i])) - reord = sizes.ravel().argsort()[::-1] - tmp = list(cont) - for i in range(len(cont)): - tmp[i] = cont[reord[i]] - cont = list(tmp) - - -sort() - - -# =====cycles through contours========= - - -def extent(i, ypos, xpos, hg, cont): - """ - Finds coronal hole extent in latitude and longitude - - Parameters - ---------- - i: 'int' - ypos: 'astropy.units.quantity.Quantity' - xpos: 'astropy.units.quantity.Quantity' - hg: 'astropy.coordinates.sky_coordinate.SkyCoord' - cont: 'list' - - Returns - ------- - maxxlon: 'astropy.coordinates.angles.core.Longitude' - minxlon: 'astropy.coordinates.angles.core.Longitude' - centlat: 'astropy.coordinates.angles.core.Latitude' - centlon: 'astropy.coordinates.angles.core.Longitude' - - """ - global maxxlat, maxxlon, maxylat, maxylon, minxlon, minylat, minylon, minxlat - maxxlat = hg.lat[ - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - ] - maxxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - ] - maxylat = hg.lat[ - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - maxylon = hg.lon[ - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - minxlat = hg.lat[ - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - ] - minxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - ] - minylat = hg.lat[ - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - minylon = hg.lon[ - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - - # =====CH centroid in lat/lon======= - - centlat = hg.lat[int(ypos), int(xpos)] - centlon = hg.lon[int(ypos), int(xpos)] - return maxxlon, minxlon, centlat, centlon - - -def coords(i, csys, cont): - """ - Finds coordinates of CH boundaries - - Parameters - ---------- - i: 'int' - csys: 'astropy.wcs.wcs.WCS' - cont: 'list' - - Returns - ------- - Ywb: 'np.array' - Xwb: 'np.array' - Yeb: 'np.array' - Xeb: 'np.array' - Ynb: 'np.array' - Xnb: 'np.array' - Ysb: 'np.array' - Xsb: 'np.array' - """ - global Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb - Ywb, Xwb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - 0, - ) - Yeb, Xeb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - 0, - ) - Ynb, Xnb = csys.all_pix2world( - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - Ysb, Xsb = csys.all_pix2world( - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - - return Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb - - -def ins_prop( - datm, - rs, - ident, - props, - arcar, - arccent, - pos, - npix, - trummar, - centlat, - centlon, - mB, - mBpos, - mBneg, - Ywb, - Xwb, - Yeb, - Xeb, - Ynb, - Xnb, - Ysb, - Xsb, - width, - eastl, - westl, -): - """ - Insertion of CH properties into property array - - Parameters - ---------- - datm: 'np.array' - rs: 'float' - ident: 'int' - props: 'np.array' - arcar: 'np.float64' - arccent: 'list' - pos: 'np.array' - npix: 'list' - trummar: 'np.float64' - centlat: 'str' - centlon: 'str' - mB: 'np.float64' - mBpos: 'np.float64' - mBneg: 'np.float64' - - Returns - ------- - props[0, ident + 1]: 'str' - props[1, ident + 1]: 'str' - props[2, ident + 1]: 'str' - props[3, ident + 1]: 'str' - props[4, ident + 1]: 'str' - props[5, ident + 1]: 'str' - props[6, ident + 1]: 'str' - props[7, ident + 1]: 'str' - props[8, ident + 1]: 'str' - props[9, ident + 1]: 'str' - props[10, ident + 1]: 'str' - props[11, ident + 1]: 'str' - props[12, ident + 1]: 'str' - props[13, ident + 1]: 'str' - props[14, ident + 1]: 'str' - props[15, ident + 1]: 'str' - props[16, ident + 1]: 'str' - props[17, ident + 1]: 'str' - props[18, ident + 1]: 'str' - props[19, ident + 1]: 'str' - props[20, ident + 1]: 'str' - tbpos: 'np.float64' - props[21, ident + 1]: 'str' - tbneg: 'np.float64' - props[22, ident + 1]: 'str' - props[23, ident + 1]: 'str' - props[24, ident + 1]: 'str' - props[25, ident + 1]: 'str' - - """ - props[0, ident + 1] = str(ident) - props[1, ident + 1] = str(np.round(arccent[0])) - props[2, ident + 1] = str(np.round(arccent[1])) - props[3, ident + 1] = str(centlon + centlat) - props[4, ident + 1] = str(np.round(Xeb)) - props[5, ident + 1] = str(np.round(Yeb)) - props[6, ident + 1] = str(np.round(Xwb)) - props[7, ident + 1] = str(np.round(Ywb)) - props[8, ident + 1] = str(np.round(Xnb)) - props[9, ident + 1] = str(np.round(Ynb)) - props[10, ident + 1] = str(np.round(Xsb)) - props[11, ident + 1] = str(np.round(Ysb)) - props[12, ident + 1] = str(eastl + "-" + westl) - props[13, ident + 1] = str(width) - props[14, ident + 1] = f"{trummar/1e+12:.1e}" - props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) - props[16, ident + 1] = str(np.round(mB, 1)) - props[17, ident + 1] = str(np.round(mBpos, 1)) - props[18, ident + 1] = str(np.round(mBneg, 1)) - props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) - props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) - tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) - props[21, ident + 1] = f"{tbpos:.1e}" - tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) - props[22, ident + 1] = f"{tbneg:.1e}" - props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" - props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" - props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" - - -"""Cycles through contours""" - -for i in range(len(cont)): - x = np.append(x, len(cont[i])) - - """only takes values of minimum surface length and calculates area""" - - if len(cont[i]) <= 100: - continue - area = 0.5 * np.abs( - np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) - - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) - ) - arcar = area * (dattoarc**2) - if arcar > 1000: - """finds centroid""" - - chpts = len(cont[i]) - cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] - - """remove quiet sun regions encompassed by coronal holes""" - if ( - cand[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ) and ( - iarr[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - print(slate) - iarr[np.where(slate == 1)] = 0 - slate[:] = 0 - - else: - """Create a simple centre point if coronal hole regions is not quiet""" - - arccent = csys.all_pix2world(cent[0], cent[1], 0) - - """classifies off limb CH regions""" - - if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( - np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) - else: - """classifies on disk coronal holes""" - - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - poslin = np.where(slate == 1) - slate[:] = 0 - print(poslin) - - """create an array for magnetic polarity""" - - pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) - pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - npix = list( - np.histogram( - datm[pos[:, 0], pos[:, 1]], - bins=np.arange( - np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, - np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, - 1, - ), - ) - ) - npix[0][np.where(npix[0] == 0)] = 1 - npix[1] = npix[1][:-1] + 0.5 - - wh1 = np.where(npix[1] > 0) - wh2 = np.where(npix[1] < 0) - - """Filters magnetic cutoff values by area""" - - if ( - np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) - <= 10 - and arcar < 9000 - ): - continue - if ( - np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] - and arcar < 40000 - ): - continue - iarr[poslin] = ident - - """create an accurate center point""" - - ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) - xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) - - arccent = csys.all_pix2world(xpos, ypos, 0) - - """calculate average angle coronal hole is subjected to""" - - dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) - ang = np.arcsin(dist / rs) - - """calculate area of CH with minimal projection effects""" - - trupixar = abs(area / np.cos(ang)) - truarcar = trupixar * (dattoarc**2) - trummar = truarcar * ((6.96e08 / rs) ** 2) - - """find CH extent in lattitude and longitude""" - - maxxlon, minxlon, centlat, centlon = extent(i, ypos, xpos, hg, cont) - - """caluclate the mean magnetic field""" - - mB = np.mean(datm[pos[:, 0], pos[:, 1]]) - mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) - mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) - - """finds coordinates of CH boundaries""" - - Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb = coords(i, csys, cont) - - width = round(maxxlon.value) - round(minxlon.value) - - if minxlon.value >= 0.0: - eastl = "W" + str(int(np.round(minxlon.value))) - else: - eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) - if maxxlon.value >= 0.0: - westl = "W" + str(int(np.round(maxxlon.value))) - else: - westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) - - if centlat >= 0.0: - centlat = "N" + str(int(np.round(centlat.value))) - else: - centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) - if centlon >= 0.0: - centlon = "W" + str(int(np.round(centlon.value))) - else: - centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) - - """insertions of CH properties into property array""" - - ins_prop( - datm, - rs, - ident, - props, - arcar, - arccent, - pos, - npix, - trummar, - centlat, - centlon, - mB, - mBpos, - mBneg, - Ywb, - Xwb, - Yeb, - Xeb, - Ynb, - Xnb, - Ysb, - Xsb, - width, - eastl, - westl, - ) - """sets up code for next possible coronal hole""" - - ident = ident + 1 - -"""sets ident back to max value of iarr""" - -ident = ident - 1 - -"""stores all CH properties in a text file""" -np.savetxt("ch_summary.txt", props, fmt="%s") - - -def rescale01(arr, cmin=None, cmax=None, a=0, b=1): - """ - Rescales array - - Parameters - ---------- - arr: 'np.arr' - cmin: 'np.float' - cmax: 'np.float' - a: 'int' - b: 'int' - - Returns - ------- - np.array - - """ - if cmin or cmax: - arr = np.clip(arr, cmin, cmax) - return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a - - -def plot_tricolor(): - """ - Plots a tricolor mask of image data - - Returns - ------- - plot: 'matplotlib.image.AxesImage' - - """ - - tricolorarray = np.zeros((4096, 4096, 3)) - - data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) - data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) - data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) - - tricolorarray[..., 0] = data_c / np.max(data_c) - tricolorarray[..., 1] = data_b / np.max(data_b) - tricolorarray[..., 2] = data_a / np.max(data_a) - - fig, ax = plt.subplots(figsize=(10, 10)) - - plt.imshow(tricolorarray, origin="lower") - plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) - plt.savefig("tricolor.png") - plt.close() - - -def plot_mask(slate=slate): - """ - Plots the contour mask - - Parameters - ---------- - slate: 'np.array' - - Returns - ------- - plot: 'matplotlib.image.AxesImage' - - """ - - chs = np.where(iarr > 0) - slate[chs] = 1 - slate = np.array(slate, dtype=np.uint8) - cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - circ[:] = 0 - r = rs / dattoarc - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[w] = 1.0 - - plt.figure(figsize=(10, 10)) - plt.xlim(143, 4014) - plt.ylim(143, 4014) - plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) - plt.gca().set_aspect("equal", adjustable="box") - plt.axis("off") - plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) - plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) - - plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) - - -plot_tricolor() -plot_mask() diff --git a/chimerapy/chimera.py b/chimerapy/chimera.py index 4462553..646a6e9 100644 --- a/chimerapy/chimera.py +++ b/chimerapy/chimera.py @@ -1,8 +1,4 @@ -""" - -""" - - +"""Package for Coronal Hole Identification Algorithm""" import glob import sys @@ -11,63 +7,114 @@ import mahotas import matplotlib.pyplot as plt import numpy as np +import scipy import scipy.interpolate +import sunpy import sunpy.map from astropy import wcs from astropy.io import fits from astropy.modeling.models import Gaussian2D +from astropy.visualization import astropy_mpl_style from skimage.util import img_as_ubyte +plt.style.use(astropy_mpl_style) -def chimera_legacy(): - file_path = "./" +"""loading in the images as fits files""" - im171 = glob.glob(file_path + "*171*.fts.gz") - im193 = glob.glob(file_path + "*193*.fts.gz") - im211 = glob.glob(file_path + "*211*.fts.gz") - imhmi = glob.glob(file_path + "*hmi*.fts.gz") +file_path = "./" - circ, data, datb, datc, dattoarc, hedb, iarr, props, rs, slate, center, xgrid, ygrid = chimera( - im171, im193, im211, imhmi - ) +im171 = glob.glob(file_path + "*171*.fts") +im193 = glob.glob(file_path + "*193*.fts") +im211 = glob.glob(file_path + "*211*.fts") +imhmi = glob.glob(file_path + "*hmi*.fts") - # =====sets ident back to max value of iarr====== - - # ident = ident - 1 - np.savetxt("ch_summary.txt", props, fmt="%s") - - plot_tricolor(data, datb, datc, xgrid, ygrid, slate) - plot_mask(slate, iarr, circ, rs, dattoarc, center, xgrid, ygrid, hedb) - - -def chimera(im171, im193, im211, imhmi): - if im171 == [] or im193 == [] or im211 == [] or imhmi == []: - print("Not all required files present") - sys.exit() - # =====Reads in data and resizes images===== - x = np.arange(0, 1024) * 4 - hdu_number = 0 - heda = fits.getheader(im171[0], hdu_number) - data = fits.getdata(im171[0], ext=0) / (heda["EXPTIME"]) - dn = scipy.interpolate.interp2d(x, x, data) - data = dn(np.arange(0, 4096), np.arange(0, 4096)) - hedb = fits.getheader(im193[0], hdu_number) - datb = fits.getdata(im193[0], ext=0) / (hedb["EXPTIME"]) - dn = scipy.interpolate.interp2d(x, x, datb) - datb = dn(np.arange(0, 4096), np.arange(0, 4096)) - hedc = fits.getheader(im211[0], hdu_number) - datc = fits.getdata(im211[0], ext=0) / (hedc["EXPTIME"]) - dn = scipy.interpolate.interp2d(x, x, datc) - datc = dn(np.arange(0, 4096), np.arange(0, 4096)) - hedm = fits.getheader(imhmi[0], hdu_number) - datm = fits.getdata(imhmi[0], ext=0) - # dn = scipy.interpolate.interp2d(np.arange(4096), np.arange(4096), datm) - # datm = dn(np.arange(0, 1024)*4, np.arange(0, 1024)*4) - if hedm["crota1"] > 90: - datm = np.rot90(np.rot90(datm)) - # =====Specifies solar radius and calculates conversion value of pixel to arcsec===== - s = np.shape(data) - rs = heda["rsun"] +"""ensure that all images are present""" + +if im171 == [] or im193 == [] or im211 == [] or imhmi == []: + print("Not all required files present") + sys.exit() + + +def rescale_aia(image: np.array, orig_res: int, desired_res: int): + """ + Rescale the input aia image dimensions. + + Parameters + ---------- + image: 'np.array' + orig_res: 'int' + desired_res: 'int + + Returns + ------- + 'np.array' + """ + + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline( + scaled_array, scaled_array, fits.getdata(image[0], 0) / (fits.getheader(image[0], 0)["EXPTIME"]) + ) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], 0)) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + + +def rescale_hmi(image: np.array, orig_res: int, desired_res: int): + """ + Rescale the input hmi image dimensions. + + Parameters + ---------- + image: 'np.array' + orig_res: 'int' + desired_res: 'int + + Returns + ------- + 'np.array' + """ + if desired_res > orig_res: + scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], ext=0)) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + elif desired_res < orig_res: + scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) + dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, fits.getdata(image[0], ext=0)) + return dn(np.arange(0, desired_res), np.arange(0, desired_res)) + + +"""defining data arrays which are used in later steps""" + +data = rescale_aia(im171, 1024, 4096) +datb = rescale_aia(im193, 1024, 4096) +datc = rescale_aia(im211, 1024, 4096) +datm = rescale_hmi(imhmi, 1024, 4096) + + +def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): + """ + Defines headers and filters aia arrays to meet header requirements + + Parameters + ---------- + aiaa: 'np.array' + aiab: 'np.array' + aiac: 'np.array' + aiam: 'np.array' + + Returns + ------- + 'np.array' + + """ + global heda, hedb, hedc, hedm, datm + heda = fits.getheader(aiaa[0], 0) + hedb = fits.getheader(aiab[0], 0) + hedc = fits.getheader(aiac[0], 0) + hedm = fits.getheader(aiam[0], 0) if hedb["ctype1"] != "solar_x ": hedb["ctype1"] = "solar_x " hedb["ctype2"] = "solar_y " @@ -90,136 +137,354 @@ def chimera(im171, im193, im211, imhmi): hedc["crpix1"] * 4.0, hedc["crpix2"] * 4.0, ) + if hedm["crota1"] > 90: + datm = np.rot90(np.rot90(datm)) + + +filter(im171, im193, im211, imhmi) + + +def remove_neg(aiaa: np.array, aiab: np.array, aiac: np.array): + """ + Removes negative values from arrays + + Parameters + ---------- + aiaa: 'np.array' + aiab: 'np.array' + aiac: 'np.array' + + Returns + ------- + 'np.array' + + """ + global data, datb, datc + data[np.where(data <= 0)] = 0 + datb[np.where(datb <= 0)] = 0 + datc[np.where(datc <= 0)] = 0 + + +remove_neg(im171, im193, im211) + +"""defines the shape of the arrays as "s" and "rs" as the solar radius""" +s = np.shape(data) +rs = heda["rsun"] + + +def pix_arc(aia: np.array): + global dattoarc dattoarc = heda["cdelt1"] + global conver + conver = ((s[0]) / 2) * dattoarc / hedm["cdelt1"] - (s[1] / 2) + global convermul convermul = dattoarc / hedm["cdelt1"] - # =====Alternative coordinate systems===== - hdul = fits.open(im171[0]) - hdul[0].header["CUNIT1"] = "arcsec" - hdul[0].header["CUNIT2"] = "arcsec" - aia = sunpy.map.Map(hdul[0].data, hdul[0].header) - adj = 4096.0 / aia.dimensions[0].value + + +pix_arc(im171) + + +def to_helio(image: np.array): + """ + Converts arrays to the Heliographic Stonyhurst coordinate system + + Parameters + ---------- + image: 'np.array' + + Returns + ------- + 'np.array' + + """ + aia = sunpy.map.Map(image) + adj = 4096 / aia.dimensions[0].value x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj + global hpc hpc = aia.pixel_to_world(x, y) + global hg hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + global csys csys = wcs.WCS(hedb) - # =======setting up arrays to be used============ - ident = 1 - iarr = np.zeros((s[0], s[1]), dtype=np.byte) - offarr, slate = np.array(iarr), np.array(iarr) - bmcool = np.zeros((s[0], s[1]), dtype=np.float32) - cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) - circ = np.zeros((s[0], s[1]), dtype=int) - # =======creation of a 2d gaussian for magnetic cut offs=========== - r = (s[1] / 2.0) - 450 - xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) - center = [int(s[1] / 2.0), int(s[1] / 2.0)] - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) - y, x = np.mgrid[0:4096, 0:4096] - garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) - garr[w] = 1.0 - # ======creation of array for CH properties========== - props = np.zeros((26, 30), dtype="", - "", - "", - "BMAX", - "BMIN", - "TOT_B+", - "TOT_B-", - "", - "", - "", - ) - props[:, 1] = ( - "num", - '"', - '"', - "H deg", - '"', - '"', - '"', - '"', - '"', - '"', - '"', - '"', - "H deg", - "deg", - "Mm^2", - "%", - "G", - "G", - "G", - "G", - "G", - "G", - "G", - "Mx", - "Mx", - "Mx", - ) - # =====removes negative data values===== - data[np.where(data <= 0)] = 0 - datb[np.where(datb <= 0)] = 0 - datc[np.where(datc <= 0)] = 0 - # ============make a multi-wavelength image for contours================== - with np.errstate(divide="ignore"): - t0 = np.log10(datc) - t1 = np.log10(datb) - t2 = np.log10(data) - t0[np.where(t0 < 0.8)] = 0.8 - t0[np.where(t0 > 2.7)] = 2.7 - t1[np.where(t1 < 1.4)] = 1.4 - t1[np.where(t1 > 3.0)] = 3.0 - t2[np.where(t2 < 1.2)] = 1.2 - t2[np.where(t2 > 3.9)] = 3.9 - t0 = np.array(((t0 - 0.8) / (2.7 - 0.8)) * 255, dtype=np.float32) - t1 = np.array(((t1 - 1.4) / (3.0 - 1.4)) * 255, dtype=np.float32) - t2 = np.array(((t2 - 1.2) / (3.9 - 1.2)) * 255, dtype=np.float32) - # ====create 3 segmented bitmasks===== + print(csys) + + +to_helio(im171) + +"""Setting up arrays to be used in later processing""" +ident = 1 +iarr = np.zeros((s[0], s[1]), dtype=np.byte) +bmcool = np.zeros((s[0], s[1]), dtype=np.float32) +offarr, slate = np.array(iarr), np.array(iarr) +cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) +circ = np.zeros((s[0], s[1]), dtype=int) + +"""creation of a 2d gaussian for magnetic cut offs""" +r = (s[1] / 2.0) - 450 +xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) +center = [int(s[1] / 2.0), int(s[1] / 2.0)] +w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) +y, x = np.mgrid[0:4096, 0:4096] +garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) +garr[w] = 1.0 + +"""creates sub-arrays of props to isolate column of index 0 and column of index 1""" +props = np.zeros((26, 30), dtype="", + "", + "", + "BMAX", + "BMIN", + "TOT_B+", + "TOT_B-", + "", + "", + "", +) +props[:, 1] = ( + "num", + '"', + '"', + "H°", + '"', + '"', + '"', + '"', + '"', + '"', + '"', + '"', + "H°", + "°", + "Mm^2", + "%", + "G", + "G", + "G", + "G", + "G", + "G", + "G", + "Mx", + "Mx", + "Mx", +) + +"""define threshold values in log space""" + +with np.errstate(divide="ignore"): + t0 = np.log10(datc) + t1 = np.log10(datb) + t2 = np.log10(data) + + +class Bounds: + """Mixin to change and define array boundaries and slopes""" + + def __init__(self, upper, lower, slope): + self.upper = upper + self.lower = lower + self.slope = slope + + def new_u(self, new_upper): + self.upper = new_upper + + def new_l(self, new_lower): + self.lower = new_lower + + def new_s(self, new_slope): + self.slope = new_slope + + +t0b = Bounds(0.8, 2.7, 255) +t1b = Bounds(1.4, 3.0, 255) +t2b = Bounds(1.2, 3.9, 255) + + +def threshold(tval: np.array): + """ + Threshold arrays based on desired boundaries + + Parameters + ---------- + tval: 'np.array' + + Returns + ------- + 'np.array' + + """ + global t0, t1, t2 + if tval.all() == t0.all(): + t0[np.where(t0 < t0b.upper)] = t0b.upper + t0[np.where(t0 > t0b.lower)] = t0b.lower + if tval.all() == t1.all(): + t1[np.where(t1 < t1b.upper)] = t1b.upper + t1[np.where(t1 > t1b.lower)] = t2b.lower + if tval.all() == t2.all(): + t2[np.where(t2 < t2b.upper)] = t2b.upper + t2[np.where(t2 > t2b.lower)] = t2b.lower + else: + print("Must input valid logarithmic arrays") + + +threshold(t0) +threshold(t1) +threshold(t2) + + +def set_contour(tval: np.array): + """Sets contour values for bounded arrays + + Parameters + ---------- + tval: 'np.array' + + Returns + ------- + 'np.array' + + """ + global t0, t1, t2 + if tval.all() == t0.all(): + t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) + elif tval.all() == t1.all(): + t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) + elif tval.all() == t2.all(): + t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) + + +set_contour(t0) +set_contour(t1) +set_contour(t2) + + +def create_mask(): + """ + Creates 3 segmented bitmasks + + Returns + ------- + 'np.array' + + """ + + global t0, t1, t2, bmmix, bmhot, bmcool with np.errstate(divide="ignore", invalid="ignore"): bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 - # ====logical conjunction of 3 segmentations======= + + +create_mask() + + +def conjunction(): + """ + Creates a conjunction of 3 segmentations + + Returns + ------- + 'np.array' + + """ + global bmhot, bmcool, bmmix, cand cand = bmcool * bmmix * bmhot - # ====plot tricolour image with lon/lat conotours======= - # ======removes off detector mis-identifications========== + + +conjunction() + + +def misid(): + """ + Removes off-detector mis-identification + + Returns + ------- + 'np.array' + + """ + global s, r, w, circ, cand r = (s[1] / 2.0) - 100 w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) circ[w] = 1.0 cand = cand * circ - # =======Separates on-disk and off-limb CHs=============== + + +misid() + + +def on_off(): + """ + Seperates on-disk and off-limb coronal holes + + Returns + ------- + 'np.array' + + """ + global circ, cand circ[:] = 0 r = (rs / dattoarc) - 10 - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[w] = 1.0 + inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[inside] = 1.0 r = (rs / dattoarc) + 40 - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) - circ[w] = 1.0 + outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) + circ[outside] = 1.0 cand = cand * circ - # ====open file for property storage===== - # =====contours the identified datapoints======= + + +on_off() + + +def contours(): + """ + Contours the identified datapoints + + Returns + ------- + cand: 'np.array' + cont: 'tuple' + heir: 'np.array' + + """ + global cand, cont, heir cand = np.array(cand, dtype=np.uint8) cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - # ======sorts contours by size============ + + +contours() + + +def sort(): + """ + Sorts the contours by size + + Returns + ------- + reord: 'list' + tmp: 'list' + cont: 'list' + + """ + global sizes, reord, tmp, cont sizes = [] for i in range(len(cont)): sizes = np.append(sizes, len(cont[i])) @@ -228,257 +493,449 @@ def chimera(im171, im193, im211, imhmi): for i in range(len(cont)): tmp[i] = cont[reord[i]] cont = list(tmp) - # =====cycles through contours========= - for i in range(len(cont)): - x = np.append(x, len(cont[i])) - # =====only takes values of minimum surface length and calculates area====== - if len(cont[i]) <= 100: - continue - area = 0.5 * np.abs( - np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) - - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) - ) - arcar = area * (dattoarc**2) - if arcar > 1000: - # =====finds centroid======= - - # chpts = len(cont[i]) - cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] - - # ===remove quiet sun regions encompassed by coronal holes====== - - if ( - cand[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ) and ( - iarr[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 +sort() + + +# =====cycles through contours========= + + +def extent(i, ypos, xpos, hg, cont): + """ + Finds coronal hole extent in latitude and longitude + + Parameters + ---------- + i: 'int' + ypos: 'astropy.units.quantity.Quantity' + xpos: 'astropy.units.quantity.Quantity' + hg: 'astropy.coordinates.sky_coordinate.SkyCoord' + cont: 'list' + + Returns + ------- + maxxlon: 'astropy.coordinates.angles.core.Longitude' + minxlon: 'astropy.coordinates.angles.core.Longitude' + centlat: 'astropy.coordinates.angles.core.Latitude' + centlon: 'astropy.coordinates.angles.core.Longitude' + + """ + global maxxlat, maxxlon, maxylat, maxylon, minxlon, minylat, minylon, minxlat + maxxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + maxylat = hg.lat[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + maxylon = hg.lon[ + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minxlat = hg.lat[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + minylat = hg.lat[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + minylon = hg.lon[ + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + ] + + # =====CH centroid in lat/lon======= + + centlat = hg.lat[int(ypos), int(xpos)] + centlon = hg.lon[int(ypos), int(xpos)] + return maxxlon, minxlon, centlat, centlon + + +def coords(i, csys, cont): + """ + Finds coordinates of CH boundaries + + Parameters + ---------- + i: 'int' + csys: 'astropy.wcs.wcs.WCS' + cont: 'list' + + Returns + ------- + Ywb: 'np.array' + Xwb: 'np.array' + Yeb: 'np.array' + Xeb: 'np.array' + Ynb: 'np.array' + Xnb: 'np.array' + Ysb: 'np.array' + Xsb: 'np.array' + """ + global Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb + Ywb, Xwb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + 0, + ) + Yeb, Xeb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + 0, + ) + Ynb, Xnb = csys.all_pix2world( + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + Ysb, Xsb = csys.all_pix2world( + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + + return Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb + + +def ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, +): + """ + Insertion of CH properties into property array + + Parameters + ---------- + datm: 'np.array' + rs: 'float' + ident: 'int' + props: 'np.array' + arcar: 'np.float64' + arccent: 'list' + pos: 'np.array' + npix: 'list' + trummar: 'np.float64' + centlat: 'str' + centlon: 'str' + mB: 'np.float64' + mBpos: 'np.float64' + mBneg: 'np.float64' + + Returns + ------- + props[0, ident + 1]: 'str' + props[1, ident + 1]: 'str' + props[2, ident + 1]: 'str' + props[3, ident + 1]: 'str' + props[4, ident + 1]: 'str' + props[5, ident + 1]: 'str' + props[6, ident + 1]: 'str' + props[7, ident + 1]: 'str' + props[8, ident + 1]: 'str' + props[9, ident + 1]: 'str' + props[10, ident + 1]: 'str' + props[11, ident + 1]: 'str' + props[12, ident + 1]: 'str' + props[13, ident + 1]: 'str' + props[14, ident + 1]: 'str' + props[15, ident + 1]: 'str' + props[16, ident + 1]: 'str' + props[17, ident + 1]: 'str' + props[18, ident + 1]: 'str' + props[19, ident + 1]: 'str' + props[20, ident + 1]: 'str' + tbpos: 'np.float64' + props[21, ident + 1]: 'str' + tbneg: 'np.float64' + props[22, ident + 1]: 'str' + props[23, ident + 1]: 'str' + props[24, ident + 1]: 'str' + props[25, ident + 1]: 'str' + + """ + props[0, ident + 1] = str(ident) + props[1, ident + 1] = str(np.round(arccent[0])) + props[2, ident + 1] = str(np.round(arccent[1])) + props[3, ident + 1] = str(centlon + centlat) + props[4, ident + 1] = str(np.round(Xeb)) + props[5, ident + 1] = str(np.round(Yeb)) + props[6, ident + 1] = str(np.round(Xwb)) + props[7, ident + 1] = str(np.round(Ywb)) + props[8, ident + 1] = str(np.round(Xnb)) + props[9, ident + 1] = str(np.round(Ynb)) + props[10, ident + 1] = str(np.round(Xsb)) + props[11, ident + 1] = str(np.round(Ysb)) + props[12, ident + 1] = str(eastl + "-" + westl) + props[13, ident + 1] = str(width) + props[14, ident + 1] = f"{trummar/1e+12:.1e}" + props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) + props[16, ident + 1] = str(np.round(mB, 1)) + props[17, ident + 1] = str(np.round(mBpos, 1)) + props[18, ident + 1] = str(np.round(mBneg, 1)) + props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) + props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) + tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) + props[21, ident + 1] = f"{tbpos:.1e}" + tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) + props[22, ident + 1] = f"{tbneg:.1e}" + props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" + props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" + props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" + + +"""Cycles through contours""" + +for i in range(len(cont)): + x = np.append(x, len(cont[i])) + + """only takes values of minimum surface length and calculates area""" + + if len(cont[i]) <= 100: + continue + area = 0.5 * np.abs( + np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) + - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) + ) + arcar = area * (dattoarc**2) + if arcar > 1000: + """finds centroid""" + + chpts = len(cont[i]) + cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] + + """remove quiet sun regions encompassed by coronal holes""" + if ( + cand[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + print(slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 + + else: + """Create a simple centre point if coronal hole regions is not quiet""" + + arccent = csys.all_pix2world(cent[0], cent[1], 0) + + """classifies off limb CH regions""" + + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + else: + """classifies on disk coronal holes""" + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - iarr[np.where(slate == 1)] = 0 + poslin = np.where(slate == 1) slate[:] = 0 + print(poslin) + + """create an array for magnetic polarity""" + + pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) + pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + npix = list( + np.histogram( + datm[pos[:, 0], pos[:, 1]], + bins=np.arange( + np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, + np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, + 1, + ), + ) + ) + npix[0][np.where(npix[0] == 0)] = 1 + npix[1] = npix[1][:-1] + 0.5 - else: - # ====create a simple centre point====== - - arccent = csys.all_pix2world(cent[0], cent[1], 0) + wh1 = np.where(npix[1] > 0) + wh2 = np.where(npix[1] < 0) - # ====classifies off limb CH regions======== + """Filters magnetic cutoff values by area""" - if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( - np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + if ( + np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) + <= 10 + and arcar < 9000 ): - mahotas.polygon.fill_polygon( - np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr - ) - else: - # =====classifies on disk coronal holes======= + continue + if ( + np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] + and arcar < 40000 + ): + continue + iarr[poslin] = ident - mahotas.polygon.fill_polygon( - np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate - ) - poslin = np.where(slate == 1) - slate[:] = 0 - - # ====create an array for magnetic polarity======== - - pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) - pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - npix = list( - np.histogram( - datm[pos[:, 0], pos[:, 1]], - bins=np.arange( - np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, - np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, - 1, - ), - ) - ) - npix[0][np.where(npix[0] == 0)] = 1 - npix[1] = npix[1][:-1] + 0.5 - - wh1 = np.where(npix[1] > 0) - wh2 = np.where(npix[1] < 0) - - # =====magnetic cut offs dependent on area========= - - if ( - np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) - <= 10 - and arcar < 9000 - ): - continue - if ( - np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] - and arcar < 40000 - ): - continue - iarr[poslin] = ident - - # ====create an accurate center point======= - - ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum( - np.absolute(hg.lat[poslin]) - ) - xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum( - np.absolute(hg.lon[poslin]) - ) + """create an accurate center point""" - arccent = csys.all_pix2world(xpos, ypos, 0) - - # ======calculate average angle coronal hole is subjected to====== - - dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) - ang = np.arcsin(dist / rs) - - # =====calculate area of CH with minimal projection effects====== - - trupixar = abs(area / np.cos(ang)) - truarcar = trupixar * (dattoarc**2) - trummar = truarcar * ((6.96e08 / rs) ** 2) - - # ====find CH extent in latitude and longitude======== - - # maxxlat = hg.lat[ - # cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - # np.max(cont[i][:, 0, 0]), - # ] - maxxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - ] - # maxylat = hg.lat[ - # np.max(cont[i][:, 0, 1]), - # cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - # ] - # maxylon = hg.lon[ - # np.max(cont[i][:, 0, 1]), - # cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - # ] - # minxlat = hg.lat[ - # cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - # np.min(cont[i][:, 0, 0]), - # ] - minxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - ] - # minylat = hg.lat[ - # np.min(cont[i][:, 0, 1]), - # cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - # ] - # minylon = hg.lon[ - # np.min(cont[i][:, 0, 1]), - # cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - # ] - - # =====CH centroid in lat/lon======= - - centlat = hg.lat[int(ypos), int(xpos)] - centlon = hg.lon[int(ypos), int(xpos)] - - # ====calculate the mean magnetic field===== - - mB = np.mean(datm[pos[:, 0], pos[:, 1]]) - mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) - mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) - - # =====finds coordinates of CH boundaries======= - - Ywb, Xwb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - 0, - ) - Yeb, Xeb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - 0, - ) - Ynb, Xnb = csys.all_pix2world( - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - Ysb, Xsb = csys.all_pix2world( - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) + ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) + xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) + + arccent = csys.all_pix2world(xpos, ypos, 0) + + """calculate average angle coronal hole is subjected to""" + + dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) + ang = np.arcsin(dist / rs) - width = round(maxxlon.value) - round(minxlon.value) - - if minxlon.value >= 0.0: - eastl = "W" + str(int(np.round(minxlon.value))) - else: - eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) - if maxxlon.value >= 0.0: - westl = "W" + str(int(np.round(maxxlon.value))) - else: - westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) - - if centlat >= 0.0: - centlat = "N" + str(int(np.round(centlat.value))) - else: - centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) - if centlon >= 0.0: - centlon = "W" + str(int(np.round(centlon.value))) - else: - centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) - - # ====insertions of CH properties into property array===== - - props[0, ident + 1] = str(ident) - props[1, ident + 1] = str(np.round(arccent[0])) - props[2, ident + 1] = str(np.round(arccent[1])) - props[3, ident + 1] = str(centlon + centlat) - props[4, ident + 1] = str(np.round(Xeb)) - props[5, ident + 1] = str(np.round(Yeb)) - props[6, ident + 1] = str(np.round(Xwb)) - props[7, ident + 1] = str(np.round(Ywb)) - props[8, ident + 1] = str(np.round(Xnb)) - props[9, ident + 1] = str(np.round(Ynb)) - props[10, ident + 1] = str(np.round(Xsb)) - props[11, ident + 1] = str(np.round(Ysb)) - props[12, ident + 1] = str(eastl + "-" + westl) - props[13, ident + 1] = str(width) - props[14, ident + 1] = f"{trummar / 1e+12:.1e}" - props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) - props[16, ident + 1] = str(np.round(mB, 1)) - props[17, ident + 1] = str(np.round(mBpos, 1)) - props[18, ident + 1] = str(np.round(mBneg, 1)) - props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) - props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) - tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) - props[21, ident + 1] = f"{tbpos:.1e}" - tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) - props[22, ident + 1] = f"{tbneg:.1e}" - props[23, ident + 1] = f"{mB * trummar * 1e+16:.1e}" - props[24, ident + 1] = f"{mBpos * trummar * 1e+16:.1e}" - props[25, ident + 1] = f"{mBneg * trummar * 1e+16:.1e}" - - # =====sets up code for next possible coronal hole===== - - ident = ident + 1 - return circ, data, datb, datc, dattoarc, hedb, iarr, props, rs, slate, center, xgrid, ygrid + """calculate area of CH with minimal projection effects""" + + trupixar = abs(area / np.cos(ang)) + truarcar = trupixar * (dattoarc**2) + trummar = truarcar * ((6.96e08 / rs) ** 2) + + """find CH extent in lattitude and longitude""" + + maxxlon, minxlon, centlat, centlon = extent(i, ypos, xpos, hg, cont) + + """caluclate the mean magnetic field""" + + mB = np.mean(datm[pos[:, 0], pos[:, 1]]) + mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) + mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) + + """finds coordinates of CH boundaries""" + + Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb = coords(i, csys, cont) + + width = round(maxxlon.value) - round(minxlon.value) + + if minxlon.value >= 0.0: + eastl = "W" + str(int(np.round(minxlon.value))) + else: + eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0: + westl = "W" + str(int(np.round(maxxlon.value))) + else: + westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0: + centlat = "N" + str(int(np.round(centlat.value))) + else: + centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0: + centlon = "W" + str(int(np.round(centlon.value))) + else: + centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) + + """insertions of CH properties into property array""" + + ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, + ) + """sets up code for next possible coronal hole""" + + ident = ident + 1 + +"""sets ident back to max value of iarr""" + +ident = ident - 1 + +"""stores all CH properties in a text file""" +np.savetxt("ch_summary.txt", props, fmt="%s") def rescale01(arr, cmin=None, cmax=None, a=0, b=1): + """ + Rescales array + + Parameters + ---------- + arr: 'np.arr' + cmin: 'np.float' + cmax: 'np.float' + a: 'int' + b: 'int' + + Returns + ------- + np.array + + """ if cmin or cmax: arr = np.clip(arr, cmin, cmax) return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a -def plot_tricolor(data, datb, datc, xgrid, ygrid, slate): +def plot_tricolor(): + """ + Plots a tricolor mask of image data + + Returns + ------- + plot: 'matplotlib.image.AxesImage' + + """ + tricolorarray = np.zeros((4096, 4096, 3)) data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) @@ -491,17 +948,30 @@ def plot_tricolor(data, datb, datc, xgrid, ygrid, slate): fig, ax = plt.subplots(figsize=(10, 10)) - plt.imshow(tricolorarray, origin="lower") # , extent = ) + plt.imshow(tricolorarray, origin="lower") plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) plt.savefig("tricolor.png") plt.close() -def plot_mask(slate, iarr, circ, rs, dattoarc, center, xgrid, ygrid, hedb): +def plot_mask(slate=slate): + """ + Plots the contour mask + + Parameters + ---------- + slate: 'np.array' + + Returns + ------- + plot: 'matplotlib.image.AxesImage' + + """ + chs = np.where(iarr > 0) slate[chs] = 1 slate = np.array(slate, dtype=np.uint8) - # cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) circ[:] = 0 r = rs / dattoarc @@ -518,4 +988,7 @@ def plot_mask(slate, iarr, circ, rs, dattoarc, center, xgrid, ygrid, hedb): plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) - plt.close() + + +plot_tricolor() +plot_mask() diff --git a/chimerapy/chimera_legacy.py b/chimerapy/chimera_legacy.py new file mode 100644 index 0000000..4462553 --- /dev/null +++ b/chimerapy/chimera_legacy.py @@ -0,0 +1,521 @@ +""" + +""" + + +import glob +import sys + +import astropy.units as u +import cv2 +import mahotas +import matplotlib.pyplot as plt +import numpy as np +import scipy.interpolate +import sunpy.map +from astropy import wcs +from astropy.io import fits +from astropy.modeling.models import Gaussian2D +from skimage.util import img_as_ubyte + + +def chimera_legacy(): + file_path = "./" + + im171 = glob.glob(file_path + "*171*.fts.gz") + im193 = glob.glob(file_path + "*193*.fts.gz") + im211 = glob.glob(file_path + "*211*.fts.gz") + imhmi = glob.glob(file_path + "*hmi*.fts.gz") + + circ, data, datb, datc, dattoarc, hedb, iarr, props, rs, slate, center, xgrid, ygrid = chimera( + im171, im193, im211, imhmi + ) + + # =====sets ident back to max value of iarr====== + + # ident = ident - 1 + np.savetxt("ch_summary.txt", props, fmt="%s") + + plot_tricolor(data, datb, datc, xgrid, ygrid, slate) + plot_mask(slate, iarr, circ, rs, dattoarc, center, xgrid, ygrid, hedb) + + +def chimera(im171, im193, im211, imhmi): + if im171 == [] or im193 == [] or im211 == [] or imhmi == []: + print("Not all required files present") + sys.exit() + # =====Reads in data and resizes images===== + x = np.arange(0, 1024) * 4 + hdu_number = 0 + heda = fits.getheader(im171[0], hdu_number) + data = fits.getdata(im171[0], ext=0) / (heda["EXPTIME"]) + dn = scipy.interpolate.interp2d(x, x, data) + data = dn(np.arange(0, 4096), np.arange(0, 4096)) + hedb = fits.getheader(im193[0], hdu_number) + datb = fits.getdata(im193[0], ext=0) / (hedb["EXPTIME"]) + dn = scipy.interpolate.interp2d(x, x, datb) + datb = dn(np.arange(0, 4096), np.arange(0, 4096)) + hedc = fits.getheader(im211[0], hdu_number) + datc = fits.getdata(im211[0], ext=0) / (hedc["EXPTIME"]) + dn = scipy.interpolate.interp2d(x, x, datc) + datc = dn(np.arange(0, 4096), np.arange(0, 4096)) + hedm = fits.getheader(imhmi[0], hdu_number) + datm = fits.getdata(imhmi[0], ext=0) + # dn = scipy.interpolate.interp2d(np.arange(4096), np.arange(4096), datm) + # datm = dn(np.arange(0, 1024)*4, np.arange(0, 1024)*4) + if hedm["crota1"] > 90: + datm = np.rot90(np.rot90(datm)) + # =====Specifies solar radius and calculates conversion value of pixel to arcsec===== + s = np.shape(data) + rs = heda["rsun"] + if hedb["ctype1"] != "solar_x ": + hedb["ctype1"] = "solar_x " + hedb["ctype2"] = "solar_y " + if heda["cdelt1"] > 1: + heda["cdelt1"], heda["cdelt2"], heda["crpix1"], heda["crpix2"] = ( + heda["cdelt1"] / 4.0, + heda["cdelt2"] / 4.0, + heda["crpix1"] * 4.0, + heda["crpix2"] * 4.0, + ) + hedb["cdelt1"], hedb["cdelt2"], hedb["crpix1"], hedb["crpix2"] = ( + hedb["cdelt1"] / 4.0, + hedb["cdelt2"] / 4.0, + hedb["crpix1"] * 4.0, + hedb["crpix2"] * 4.0, + ) + hedc["cdelt1"], hedc["cdelt2"], hedc["crpix1"], hedc["crpix2"] = ( + hedc["cdelt1"] / 4.0, + hedc["cdelt2"] / 4.0, + hedc["crpix1"] * 4.0, + hedc["crpix2"] * 4.0, + ) + dattoarc = heda["cdelt1"] + convermul = dattoarc / hedm["cdelt1"] + # =====Alternative coordinate systems===== + hdul = fits.open(im171[0]) + hdul[0].header["CUNIT1"] = "arcsec" + hdul[0].header["CUNIT2"] = "arcsec" + aia = sunpy.map.Map(hdul[0].data, hdul[0].header) + adj = 4096.0 / aia.dimensions[0].value + x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj + hpc = aia.pixel_to_world(x, y) + hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + csys = wcs.WCS(hedb) + # =======setting up arrays to be used============ + ident = 1 + iarr = np.zeros((s[0], s[1]), dtype=np.byte) + offarr, slate = np.array(iarr), np.array(iarr) + bmcool = np.zeros((s[0], s[1]), dtype=np.float32) + cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) + circ = np.zeros((s[0], s[1]), dtype=int) + # =======creation of a 2d gaussian for magnetic cut offs=========== + r = (s[1] / 2.0) - 450 + xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) + center = [int(s[1] / 2.0), int(s[1] / 2.0)] + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) + y, x = np.mgrid[0:4096, 0:4096] + garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) + garr[w] = 1.0 + # ======creation of array for CH properties========== + props = np.zeros((26, 30), dtype="", + "", + "", + "BMAX", + "BMIN", + "TOT_B+", + "TOT_B-", + "", + "", + "", + ) + props[:, 1] = ( + "num", + '"', + '"', + "H deg", + '"', + '"', + '"', + '"', + '"', + '"', + '"', + '"', + "H deg", + "deg", + "Mm^2", + "%", + "G", + "G", + "G", + "G", + "G", + "G", + "G", + "Mx", + "Mx", + "Mx", + ) + # =====removes negative data values===== + data[np.where(data <= 0)] = 0 + datb[np.where(datb <= 0)] = 0 + datc[np.where(datc <= 0)] = 0 + # ============make a multi-wavelength image for contours================== + with np.errstate(divide="ignore"): + t0 = np.log10(datc) + t1 = np.log10(datb) + t2 = np.log10(data) + t0[np.where(t0 < 0.8)] = 0.8 + t0[np.where(t0 > 2.7)] = 2.7 + t1[np.where(t1 < 1.4)] = 1.4 + t1[np.where(t1 > 3.0)] = 3.0 + t2[np.where(t2 < 1.2)] = 1.2 + t2[np.where(t2 > 3.9)] = 3.9 + t0 = np.array(((t0 - 0.8) / (2.7 - 0.8)) * 255, dtype=np.float32) + t1 = np.array(((t1 - 1.4) / (3.0 - 1.4)) * 255, dtype=np.float32) + t2 = np.array(((t2 - 1.2) / (3.9 - 1.2)) * 255, dtype=np.float32) + # ====create 3 segmented bitmasks===== + with np.errstate(divide="ignore", invalid="ignore"): + bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 + bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 + bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 + # ====logical conjunction of 3 segmentations======= + cand = bmcool * bmmix * bmhot + # ====plot tricolour image with lon/lat conotours======= + # ======removes off detector mis-identifications========== + r = (s[1] / 2.0) - 100 + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + cand = cand * circ + # =======Separates on-disk and off-limb CHs=============== + circ[:] = 0 + r = (rs / dattoarc) - 10 + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + r = (rs / dattoarc) + 40 + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) + circ[w] = 1.0 + cand = cand * circ + # ====open file for property storage===== + # =====contours the identified datapoints======= + cand = np.array(cand, dtype=np.uint8) + cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + # ======sorts contours by size============ + sizes = [] + for i in range(len(cont)): + sizes = np.append(sizes, len(cont[i])) + reord = sizes.ravel().argsort()[::-1] + tmp = list(cont) + for i in range(len(cont)): + tmp[i] = cont[reord[i]] + cont = list(tmp) + # =====cycles through contours========= + for i in range(len(cont)): + x = np.append(x, len(cont[i])) + + # =====only takes values of minimum surface length and calculates area====== + + if len(cont[i]) <= 100: + continue + area = 0.5 * np.abs( + np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) + - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) + ) + arcar = area * (dattoarc**2) + if arcar > 1000: + # =====finds centroid======= + + # chpts = len(cont[i]) + cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] + + # ===remove quiet sun regions encompassed by coronal holes====== + + if ( + cand[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 + + else: + # ====create a simple centre point====== + + arccent = csys.all_pix2world(cent[0], cent[1], 0) + + # ====classifies off limb CH regions======== + + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + ): + mahotas.polygon.fill_polygon( + np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr + ) + else: + # =====classifies on disk coronal holes======= + + mahotas.polygon.fill_polygon( + np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate + ) + poslin = np.where(slate == 1) + slate[:] = 0 + + # ====create an array for magnetic polarity======== + + pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) + pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + npix = list( + np.histogram( + datm[pos[:, 0], pos[:, 1]], + bins=np.arange( + np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, + np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, + 1, + ), + ) + ) + npix[0][np.where(npix[0] == 0)] = 1 + npix[1] = npix[1][:-1] + 0.5 + + wh1 = np.where(npix[1] > 0) + wh2 = np.where(npix[1] < 0) + + # =====magnetic cut offs dependent on area========= + + if ( + np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) + <= 10 + and arcar < 9000 + ): + continue + if ( + np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] + and arcar < 40000 + ): + continue + iarr[poslin] = ident + + # ====create an accurate center point======= + + ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum( + np.absolute(hg.lat[poslin]) + ) + xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum( + np.absolute(hg.lon[poslin]) + ) + + arccent = csys.all_pix2world(xpos, ypos, 0) + + # ======calculate average angle coronal hole is subjected to====== + + dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) + ang = np.arcsin(dist / rs) + + # =====calculate area of CH with minimal projection effects====== + + trupixar = abs(area / np.cos(ang)) + truarcar = trupixar * (dattoarc**2) + trummar = truarcar * ((6.96e08 / rs) ** 2) + + # ====find CH extent in latitude and longitude======== + + # maxxlat = hg.lat[ + # cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + # np.max(cont[i][:, 0, 0]), + # ] + maxxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + ] + # maxylat = hg.lat[ + # np.max(cont[i][:, 0, 1]), + # cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + # ] + # maxylon = hg.lon[ + # np.max(cont[i][:, 0, 1]), + # cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + # ] + # minxlat = hg.lat[ + # cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + # np.min(cont[i][:, 0, 0]), + # ] + minxlon = hg.lon[ + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + ] + # minylat = hg.lat[ + # np.min(cont[i][:, 0, 1]), + # cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + # ] + # minylon = hg.lon[ + # np.min(cont[i][:, 0, 1]), + # cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + # ] + + # =====CH centroid in lat/lon======= + + centlat = hg.lat[int(ypos), int(xpos)] + centlon = hg.lon[int(ypos), int(xpos)] + + # ====calculate the mean magnetic field===== + + mB = np.mean(datm[pos[:, 0], pos[:, 1]]) + mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) + mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) + + # =====finds coordinates of CH boundaries======= + + Ywb, Xwb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + 0, + ) + Yeb, Xeb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + 0, + ) + Ynb, Xnb = csys.all_pix2world( + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + Ysb, Xsb = csys.all_pix2world( + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + + width = round(maxxlon.value) - round(minxlon.value) + + if minxlon.value >= 0.0: + eastl = "W" + str(int(np.round(minxlon.value))) + else: + eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0: + westl = "W" + str(int(np.round(maxxlon.value))) + else: + westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0: + centlat = "N" + str(int(np.round(centlat.value))) + else: + centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0: + centlon = "W" + str(int(np.round(centlon.value))) + else: + centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) + + # ====insertions of CH properties into property array===== + + props[0, ident + 1] = str(ident) + props[1, ident + 1] = str(np.round(arccent[0])) + props[2, ident + 1] = str(np.round(arccent[1])) + props[3, ident + 1] = str(centlon + centlat) + props[4, ident + 1] = str(np.round(Xeb)) + props[5, ident + 1] = str(np.round(Yeb)) + props[6, ident + 1] = str(np.round(Xwb)) + props[7, ident + 1] = str(np.round(Ywb)) + props[8, ident + 1] = str(np.round(Xnb)) + props[9, ident + 1] = str(np.round(Ynb)) + props[10, ident + 1] = str(np.round(Xsb)) + props[11, ident + 1] = str(np.round(Ysb)) + props[12, ident + 1] = str(eastl + "-" + westl) + props[13, ident + 1] = str(width) + props[14, ident + 1] = f"{trummar / 1e+12:.1e}" + props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) + props[16, ident + 1] = str(np.round(mB, 1)) + props[17, ident + 1] = str(np.round(mBpos, 1)) + props[18, ident + 1] = str(np.round(mBneg, 1)) + props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) + props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) + tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) + props[21, ident + 1] = f"{tbpos:.1e}" + tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) + props[22, ident + 1] = f"{tbneg:.1e}" + props[23, ident + 1] = f"{mB * trummar * 1e+16:.1e}" + props[24, ident + 1] = f"{mBpos * trummar * 1e+16:.1e}" + props[25, ident + 1] = f"{mBneg * trummar * 1e+16:.1e}" + + # =====sets up code for next possible coronal hole===== + + ident = ident + 1 + return circ, data, datb, datc, dattoarc, hedb, iarr, props, rs, slate, center, xgrid, ygrid + + +def rescale01(arr, cmin=None, cmax=None, a=0, b=1): + if cmin or cmax: + arr = np.clip(arr, cmin, cmax) + return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + + +def plot_tricolor(data, datb, datc, xgrid, ygrid, slate): + tricolorarray = np.zeros((4096, 4096, 3)) + + data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) + data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) + data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) + + tricolorarray[..., 0] = data_c / np.max(data_c) + tricolorarray[..., 1] = data_b / np.max(data_b) + tricolorarray[..., 2] = data_a / np.max(data_a) + + fig, ax = plt.subplots(figsize=(10, 10)) + + plt.imshow(tricolorarray, origin="lower") # , extent = ) + plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) + plt.savefig("tricolor.png") + plt.close() + + +def plot_mask(slate, iarr, circ, rs, dattoarc, center, xgrid, ygrid, hedb): + chs = np.where(iarr > 0) + slate[chs] = 1 + slate = np.array(slate, dtype=np.uint8) + # cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + circ[:] = 0 + r = rs / dattoarc + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + + plt.figure(figsize=(10, 10)) + plt.xlim(143, 4014) + plt.ylim(143, 4014) + plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) + plt.gca().set_aspect("equal", adjustable="box") + plt.axis("off") + plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) + plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) + + plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) + plt.close() diff --git a/chimerapy/tests/test_chimera.py b/chimerapy/tests/test_chimera.py index e08da98..518ab50 100644 --- a/chimerapy/tests/test_chimera.py +++ b/chimerapy/tests/test_chimera.py @@ -1,29 +1,424 @@ +import glob import os -from pathlib import Path +import warnings -from parfive import Downloader +import mahotas +import numpy as np +from astropy.io import fits +from astropy.modeling.models import Gaussian2D -from chimerapy.chimera import chimera_legacy +from chimerapy.chimera import (Bounds, Xeb, Xnb, Xsb, Xwb, Yeb, Ynb, Ysb, Ywb, + ang, arcar, arccent, area, cent, centlat, + centlon, chpts, cont, coords, csys, data, datb, + datc, datm, dist, eastl, extent, filter, hg, + ins_prop, mB, mBneg, mBpos, npix, pos, + remove_neg, rescale_aia, rescale_hmi, + set_contour, sort, threshold, truarcar, trummar, + trupixar, westl, width, xpos, ypos) -INPUT_FILES = { - "aia171": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00171_fd_20160922_103010.fts.gz", - "aia193": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00193_fd_20160922_103041.fts.gz", - "aia211": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00211_fd_20160922_103046.fts.gz", - "hmi_mag": "https://solarmonitor.org/data/2016/09/22/fits/shmi/shmi_maglc_fd_20160922_094640.fts.gz", -} +file_path = "./" +im171 = glob.glob(file_path + "*171*.fts") +im193 = glob.glob(file_path + "*193*.fts") +im211 = glob.glob(file_path + "*211*.fts") +imhmi = glob.glob(file_path + "*hmi*.fts") -def test_chimera(tmp_path): - Downloader.simple_download(INPUT_FILES.values(), path=tmp_path) - os.chdir(tmp_path) - chimera_legacy() - test_summary_file = Path(__file__).parent / "test_ch_summary.txt" - with test_summary_file.open("r") as f: - test_summary_text = f.read() +def img_present(): + assert im171 != [] or im193 != [] or im211 != [] or imhmi != [], "Not all required files present" - ch_summary_file = tmp_path / "ch_summary.txt" - with ch_summary_file.open("r") as f: - ch_summary_text = f.read() - assert ch_summary_text == test_summary_text +def rest_rescale(): + global data, datb, datc, datm + data = rescale_aia(im171, 1024, 4096) + datb = rescale_aia(im193, 1024, 4096) + datc = rescale_aia(im211, 1024, 4096) + datm = rescale_hmi(imhmi, 1024, 4096) + assert ( + len(data) == len(datb) == len(datc) == len(datm) == 4096 + ), "Array size does not match desired array size" + + +heda = fits.getheader(im171[0], 0) +hedb = fits.getheader(im193[0], 0) +hedc = fits.getheader(im211[0], 0) +hedm = fits.getheader(imhmi[0], 0) + + +def test_filter(): + initial_values = { + 'hedb["ctyple1"]': hedb["ctype1"], + 'hedb["ctype2"]': hedb["ctype2"], + 'heda["cdelt1"]': heda["cdelt1"], + 'heda["cdelt2"]': heda["cdelt2"], + 'heda["crpix1"]': heda["crpix1"], + 'heda["crpix2"]': heda["crpix2"], + 'hedb["cdelt1"]': hedb["cdelt1"], + 'hedb["cdelt2"]': hedb["cdelt2"], + 'hedb["crpix1"]': hedb["crpix1"], + 'hedb["crpix2"]': hedb["crpix2"], + 'hedc["cdelt1"]': hedc["cdelt1"], + 'hedc["cdelt2"]': hedc["cdelt2"], + 'hedc["crpix1"]': hedc["crpix1"], + 'hedc["crpix2"]': hedc["crpix2"], + "datm": datm, + } + filter(im171, im193, im211, imhmi) + final_values = { + 'hedb["ctyple1"]': hedb["ctype1"], + 'hedb["ctype2"]': hedb["ctype2"], + 'heda["cdelt1"]': heda["cdelt1"], + 'heda["cdelt2"]': heda["cdelt2"], + 'heda["crpix1"]': heda["crpix1"], + 'heda["crpix2"]': heda["crpix2"], + 'hedb["cdelt1"]': hedb["cdelt1"], + 'hedb["cdelt2"]': hedb["cdelt2"], + 'hedb["crpix1"]': hedb["crpix1"], + 'hedb["crpix2"]': hedb["crpix2"], + 'hedc["cdelt1"]': hedc["cdelt1"], + 'hedc["cdelt2"]': hedc["cdelt2"], + 'hedc["crpix1"]': hedc["crpix1"], + 'hedc["crpix2"]': hedc["crpix2"], + "datm": datm, + } + if initial_values == final_values: + raise Warning("No filtering occured - ensure filtering conditions were not met") + + +def test_neg(): + remove_neg(im171, im193, im211) + for num in im171: + assert num >= 0, "Array still contains negative number" + for num in im193: + assert num >= 0, "Array still contains negative number" + for num in im211: + assert num >= 0, "Array still contains negative number" + + +def tremove_neg(): + assert len(data[data < 0]) == 0, "Data contains negative values" + assert len(datb[datb < 0]) == 0, "Data contains negative values" + assert len(datc[datc < 0]) == 0, "Data contains negative values" + + +def s_rs(): + global s, rs + s = np.shape(data) + rs = heda["rsun"] + assert s == (4096, 4096), "Incorrect data shape" + if rs < 970 or rs > 975: + warnings.warn("Solar radius may be inconsistant with accepted value (~973)") + + +s_rs() + +ident = 1 +iarr = np.zeros((s[0], s[1]), dtype=np.byte) +bmcool = np.zeros((s[0], s[1]), dtype=np.float32) +offarr, slate = np.array(iarr), np.array(iarr) +cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) +circ = np.zeros((s[0], s[1]), dtype=int) + +r = (s[1] / 2.0) - 450 +xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) +center = [int(s[1] / 2.0), int(s[1] / 2.0)] +w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) +y, x = np.mgrid[0:4096, 0:4096] +garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) +garr[w] = 1.0 + +props = np.zeros((26, 30), dtype="", + "", + "", + "BMAX", + "BMIN", + "TOT_B+", + "TOT_B-", + "", + "", + "", +) +props[:, 1] = ( + "num", + '"', + '"', + "H°", + '"', + '"', + '"', + '"', + '"', + '"', + '"', + '"', + "H°", + "°", + "Mm^2", + "%", + "G", + "G", + "G", + "G", + "G", + "G", + "G", + "Mx", + "Mx", + "Mx", +) + +assert len(props) == 26, "Incorrect property array size" + + +def test_bounds(): + t0b = Bounds(0.8, 2.7, 255) + t1b = Bounds(1.4, 3.0, 255) + t2b = Bounds(1.2, 3.9, 255) + assert t0b.upper > t0b.lower, "Upper bound must be greater than lower bound" + assert t1b.upper > t0b.lower, "Upper bound must be greater than lower bound" + assert t2b.upper > t0b.lower, "Upper bound must be greater than lower bound" + + +with np.errstate(divide="ignore"): + t0 = np.log10(datc) + t1 = np.log10(datb) + t2 = np.log10(data) + + +def test_threshold(): + init = {"t0": t0, "t1": t1, "t2": t2} + assert init["t0"] != threshold(t0), "Data was not bounded by threshold values" + assert init["t1"] != threshold(t0), "Data was not bounded by threshold values" + assert init["t2"] != threshold(t0), "Data was not bounded by threshold values" + + +t0b = Bounds(0.8, 2.7, 255) +t1b = Bounds(1.4, 3.0, 255) +t2b = Bounds(1.2, 3.9, 255) + + +def test_contour(): + init = {"t0": t0, "t1": t1, "t2": t2} + assert init["t0"] != set_contour(t0), "Data was not bounded by threshold values" + assert init["t1"] != set_contour(t0), "Data was not bounded by threshold values" + assert init["t2"] != set_contour(t0), "Data was not bounded by threshold values" + + +def has_dupl(arr): + seen = set() + for num in arr: + if num in seen: + return True + seen.add(num) + return False + + +def test_dupl(): + global sizes, reord, tmp, cont + sort() + assert not has_dupl(reord), "Sorted list should contain no duplicates" + assert not has_dupl(tmp), "Sorted list should contain no duplicates" + assert not has_dupl(cont), "Sorted list should contain no duplicates" + + +for i in range(len(cont)): + x = np.append(x, len(cont[i])) + + def test_extent(): + i_maxxlat = None + i_maxxlon = None + i_maxylat = None + i_maxylon = None + i_minxlon = None + i_minylat = None + i_minylon = None + i_minxlat = None + extent(i, ypos, xpos, hg, cont) + global maxxlat, maxxlon, maxylat, maxylon, minxlon, minylat, minylon, minxlat + assert i_maxxlat != maxxlat, "maxxlat not created successfully" + assert i_maxxlon != maxxlon, "maxxlon not created successfully" + assert i_maxylat != maxylat, "maxylat not created successfully" + assert i_maxylon != maxylon, "maxylon not created successfully" + assert i_minxlon != minxlon, "minxlon not created successfully" + assert i_minylat != minylat, "minylat not created successfully" + assert i_minylon != minylon, "minylon not created successfully" + assert i_minxlat != minxlat, "minxlat not created successfully" + + def test_coords(): + i_Ywb = None + i_Xwb = None + i_Yeb = None + i_Xeb = None + i_Ynb = None + i_Xnb = None + i_Ysb = None + i_Xsb = None + coords(i, csys, cont) + assert i_Ywb != Ywb, "Ywb not created successfully" + assert i_Xwb != Xwb, "Xwb not created successfully" + assert i_Yeb != Yeb, "Yeb not created successfully" + assert i_Xeb != Xeb, "Xeb not created successfully" + assert i_Ynb != Ynb, "Ynb not created successfully" + assert i_Xnb != Xnb, "Xnb not created successfully" + assert i_Ysb != Ysb, "Ysb not created successfully" + assert i_Xsb != Xsb, "Xsb not created successfully" + + def test_props(): + ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, + ) + assert np.any(props is None) is not False, "Property array should not contain empty entries" + assert np.all(props is None) is not False, "Property array should not be empty" + + def test_loop_variables(): + for i in range(len(cont)): + assert len(cont[i]) > 100, "Contour length should be greater than 100" + + def test_fill_polygon(): + # Example data similar to what might be used in your application + cand = np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) + + iarr = np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) + + # Example contour data + cont = np.array([[[[1, 2], [1, 3], [2, 3], [2, 2]]]]) # Example contour with vertices + + # Example slate array + slate = np.array(iarr) + + # Extracting the polygon vertices from cont[i] + polygon_vertices = np.array(list(zip(cont[0][:, 0, 1], cont[0][:, 0, 0]))) + + # Simulate conditions to enter the if statement + if ( + cand[ + np.max(cont[0][:, 0, 0]) + 1, + cont[0][np.where(cont[0][:, 0, 0] == np.max(cont[0][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[0][:, 0, 0]) + 1, + cont[0][np.where(cont[0][:, 0, 0] == np.max(cont[0][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ): + # Call fill_polygon with the polygon vertices and slate array + mahotas.polygon.fill_polygon(polygon_vertices, slate) + + # Perform actions after filling the polygon + print("After filling polygon:") + print(slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 # Reset slate array + + # Assertions to verify correctness + assert np.array_equal( + slate, + np.array([[0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]), + ), "Test failed: slate array does not match expected result" + + assert np.array_equal( + iarr, + np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]), + ), "Test failed: iarr array does not match expected result" + + def test_limb(): + arccent = csys.all_pix2world(cent[0], cent[1], 0) + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + assert np.sum(offarr) > 0, "Offarr was not modified successfully" + else: + # =====classifies on disk coronal holes======= + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + slate[:] = 0 + assert np.sum(slate) > 0, "Slate was not modified successfully" + + def test_magpol(): + assert npix[0][np.where(npix[0] == 0)] != 0, "Npix[0] should not be equal to zero at its zeros" + assert npix[1] != 0, "Npix[1] should not be equal to 0" + npixtest = [ + np.array([2, -1, 0, 3, -2, 1]), # Example npix[0] (bin counts) + np.array([1, -1, 0, 2, -2, 1]), # Example npix[1] (bin edges) + ] + wh1_expected = np.where(npixtest[1] > 0) + wh1_actual = np.where(npixtest[1] > 0) + + wh2_expected = np.where(npixtest[1] < 0) + wh2_actual = np.where(npixtest[1] < 0) + + assert np.array_equal( + wh1_actual, wh1_expected + ), f"Test failed for wh1: Expected {wh1_expected}, got {wh1_actual}" + assert np.array_equal( + wh2_actual, wh2_expected + ), f"Test failed for wh1: Expected {wh2_expected}, got {wh2_actual}" + + assert area is not None, "Area variable not created successfully" + assert arcar is not None, "Arcar variable not created successfully" + assert chpts is not None, "Chpts variable not created successfully" + assert cent is not None, "Cent variable not created successfully" + assert arccent is not None, "Arccent variable not created successfully" + assert ypos is not None, "Ypos variable not created successfully" + assert xpos is not None, "Xpos variable not created suffessfully" + assert dist is not None, "Dist variable not created successfully" + assert ang is not None, "Ang variable not created successfully" + assert trupixar is not None, "Trupixar variable not created successfully" + assert truarcar is not None, "Truarcar variable not created successfully" + assert trummar is not None, "Trummar variable not created successfully" + assert mB is not None, "mB variable not created successfully" + assert mBpos is not None, "mBpos variable not created successfully" + assert mBneg is not None, "mBneg variable not created successfully" + assert width is not None, "width variable not created successfully" + assert eastl is not None, "eastl variable not created successfully" + assert westl is not None, "westl variable not created successfully" + assert centlat is not None, "centlat variable not created successfully" + assert centlon is not None, "centlon variable not created successfully" + +assert os.path.exists("ch_summary.txt"), "Summary file not saved correctly" +assert os.path.exists("tricolor.png"), "Tricolor image not saved correctly" +assert os.path.exists("CH_mask_" + hedb["DATE"] + ".png") diff --git a/chimerapy/tests/test_chimera_legacy.py b/chimerapy/tests/test_chimera_legacy.py new file mode 100644 index 0000000..41e30a3 --- /dev/null +++ b/chimerapy/tests/test_chimera_legacy.py @@ -0,0 +1,29 @@ +import os +from pathlib import Path + +from parfive import Downloader + +from chimerapy.chimera_legacy import chimera_legacy + +INPUT_FILES = { + "aia171": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00171_fd_20160922_103010.fts.gz", + "aia193": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00193_fd_20160922_103041.fts.gz", + "aia211": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00211_fd_20160922_103046.fts.gz", + "hmi_mag": "https://solarmonitor.org/data/2016/09/22/fits/shmi/shmi_maglc_fd_20160922_094640.fts.gz", +} + + +def test_chimera(tmp_path): + Downloader.simple_download(INPUT_FILES.values(), path=tmp_path) + os.chdir(tmp_path) + chimera_legacy() + + test_summary_file = Path(__file__).parent / "test_ch_summary.txt" + with test_summary_file.open("r") as f: + test_summary_text = f.read() + + ch_summary_file = tmp_path / "ch_summary.txt" + with ch_summary_file.open("r") as f: + ch_summary_text = f.read() + + assert ch_summary_text == test_summary_text From b83b7d074c9741c3bc6b5e8a69e321a11fe66429 Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Sat, 13 Jul 2024 15:27:23 -0400 Subject: [PATCH 07/10] The updated chimera code is in chimera_copy --- chimerapy/chimera.py | 6 +- chimerapy/chimera_copy.py | 924 ++++++++++++++++++++++++++++++++ chimerapy/tests/test_chimera.py | 424 ++++++++------- 3 files changed, 1160 insertions(+), 194 deletions(-) create mode 100644 chimerapy/chimera_copy.py diff --git a/chimerapy/chimera.py b/chimerapy/chimera.py index 646a6e9..d5a80fe 100644 --- a/chimerapy/chimera.py +++ b/chimerapy/chimera.py @@ -23,6 +23,7 @@ file_path = "./" + im171 = glob.glob(file_path + "*171*.fts") im193 = glob.glob(file_path + "*193*.fts") im211 = glob.glob(file_path + "*211*.fts") @@ -197,17 +198,18 @@ def to_helio(image: np.array): 'np.array' """ + aia = sunpy.map.Map(image) adj = 4096 / aia.dimensions[0].value x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj + print(x, y) global hpc hpc = aia.pixel_to_world(x, y) global hg hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) global csys csys = wcs.WCS(hedb) - print(csys) - + to_helio(im171) diff --git a/chimerapy/chimera_copy.py b/chimerapy/chimera_copy.py new file mode 100644 index 0000000..68b0966 --- /dev/null +++ b/chimerapy/chimera_copy.py @@ -0,0 +1,924 @@ +"""Package for Coronal Hole Identification Algorithm""" +import glob +import sys + +import astropy.units as u +import cv2 +import mahotas +import matplotlib.pyplot as plt +import numpy as np +import scipy +from sunpy.map import Map +from sunpy.coordinates import frames +from sunpy.coordinates import propagate_with_solar_surface +import scipy.interpolate +import sunpy +import sunpy.map +from astropy import wcs +from astropy.io import fits +from astropy.modeling.models import Gaussian2D +from astropy.visualization import astropy_mpl_style +from skimage.util import img_as_ubyte +from astropy.wcs import WCS +from sunpy.map import all_coordinates_from_map +from sunpy.coordinates import HeliographicStonyhurst + + +#--noverify + +plt.style.use(astropy_mpl_style) + +"""Defining the paths for example files used to run program locally""" + +file_path = "./" + +INPUT_FILES = {"aia171": 'http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0171.fits', +"aia193": 'http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0193.fits', +"aia211": 'http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0211.fits', +"hmi_mag": 'http://jsoc.stanford.edu/data/hmi/fits/2016/09/22/hmi.M_720s.20160922_010000_TAI.fits', +} + +im171 = Map(INPUT_FILES['aia171']) +im193 = Map(INPUT_FILES['aia193']) +im211 = Map(INPUT_FILES['aia211']) +imhmi = Map(INPUT_FILES['hmi_mag']) + + +def rescale(proj_to: sunpy.map.Map, input_map: sunpy.map.Map): + """ + Rescale the input aia image dimensions. + + Parameters + ---------- + proj_to: 'sunpy.map.Map' + input_map: 'sunpy.map.Map + + Returns + ------- + array: 'np.array' + + """ + with propagate_with_solar_surface(): + map1 = proj_to.reproject_to(input_map.wcs) + new_x_scale = map1.scale[0].to(u.arcsec / u.pixel).value + new_y_scale = map1.scale[1].to(u.arcsec / u.pixel).value + map1.meta['cdelt1'] = new_x_scale + map1.meta['cdelt2'] = new_y_scale + map1.meta['cunit1'] = 'arcsec' + map1.meta['cunit2'] = 'arcsec' + return map1 + +im171 = rescale(im171, im171) +im193 = rescale(im171, im193) +im211 = rescale(im171, im211) +imhmi = rescale(im171, imhmi) + +def filter(map1: np.array, map2: np.array, map3: np.array): + """ + Defines headers and filters aia arrays to meet header requirements. Removes 0 values from each array. + + Parameters + ---------- + map1: 'sunpy.map.Map' + map2: 'sunpy.map.Map' + map3: 'sunpy.map.Map' + + Returns + ------- + map1: 'sunpy.map.Map' + map2: 'sunpy.map.Map' + map3: 'sunpy.map.Map' + + """ + map1.data[np.where(map1.data <= 0)] = 0 + map2.data[np.where(map2.data <= 0)] = 0 + map3.data[np.where(map3.data <= 0)] = 0 + + return map1, map2, map3 + +im171, im193, im211 = filter(im171, im193, im211) + +"""defines the shape of the arrays as "s" and "rs" as the solar radius""" +s = np.shape(im171.data) +#do we want the solar radius in arcsec or pixels? +rs = im171.rsun_obs +rs_pixels = im171.rsun_obs/im171.scale[0] + + + +def pix_arc(map: sunpy.map.Map): + ''' + Calculates the conversion value of pixel to arcsec + + Parameters + ---------- + map1: 'sunpy.map.Map' + map2: 'sunpy.map.Map' + + Returns + + ''' + dattoarc = map.scale[0].value + conver = ((s[0]) / 2) * dattoarc / map.meta["cdelt1"] - (s[1] / 2) + convermul = dattoarc / map.meta["cdelt1"] + return dattoarc, conver, convermul + + +dattoarc, conver, convermul = pix_arc(im171) + + +def to_helio(map: sunpy.map.Map): + """ + Converts arrays to the Heliographic Stonyhurst coordinate system + + Parameters + ---------- + map: 'sunpy.map.Map' + + Returns + ------- + x, y: 'astropy.units.quantity.Quantity' + hpc: 'astropy.coordinates.sky_coordinate.SkyCoord' + hg: 'astropy.coordinates.sky_coordinate.SkyCoord' + csys: 'astropy.wcs.wcs.WCS' + + + """ + hpc = all_coordinates_from_map(map) + hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) + csys = wcs.WCS(dict(map.meta)) + return hpc, hg, csys + +hpc, hg, csys = to_helio(im171) + +"""Setting up arrays to be used in later processing""" +ident = 1 +iarr = np.zeros((s[0], s[1]), dtype=np.byte) +bmcool = np.zeros((s[0], s[1]), dtype=np.float32) +offarr, slate = np.array(iarr), np.array(iarr) +cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) +circ = np.zeros((s[0], s[1]), dtype=int) + +"""creation of a 2d gaussian for magnetic cut offs""" +r = (s[1] / 2.0) - 450 +xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) +center = [int(s[1] / 2.0), int(s[1] / 2.0)] +w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) +y, x = np.mgrid[0:1024, 0:1024] +pix_size = (2000 * u.arcsec).value +garr = Gaussian2D(1, im171.reference_pixel.x.value, im171.reference_pixel.y.value, pix_size / im171.scale[0].value, pix_size / im171.scale[0].value)(x, y) +garr[w] = 1.0 + +"""creates sub-arrays of props to isolate column of index 0 and column of index 1""" +props = np.zeros((26, 30), dtype="", + "", + "", + "BMAX", + "BMIN", + "TOT_B+", + "TOT_B-", + "", + "", + "", +) +props[:, 1] = ( + "num", + '"', + '"', + "H°", + '"', + '"', + '"', + '"', + '"', + '"', + '"', + '"', + "H°", + "°", + "Mm^2", + "%", + "G", + "G", + "G", + "G", + "G", + "G", + "G", + "Mx", + "Mx", + "Mx", +) + +"""define threshold values in log space""" +def log_dat(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): + ''' + Takes the log base-10 of all sunpy map data + + Parameters + ---------- + map1: 'sunpy.map.Map' + map2: 'sunpy.map.Map' + map3: 'sunpy.map.Map' + + Returns + ------- + t0: 'np.array' + t1: 'np.array' + t2: 'np.array' + ''' + with np.errstate(divide="ignore"): + t0 = np.log10(map1.data) + t1 = np.log10(map2.data) + t2 = np.log10(map3.data) + return t0, t1, t2 + +t0, t1, t2 = log_dat(im171, im193, im211) + + +class Bounds: + """Class to change and define array boundaries and slopes""" + + def __init__(self, upper, lower, slope): + self.upper = upper + self.lower = lower + self.slope = slope + + def new_u(self, new_upper): + self.upper = new_upper + + def new_l(self, new_lower): + self.lower = new_lower + + def new_s(self, new_slope): + self.slope = new_slope + + +t0b = Bounds(0.8, 2.7, 255) +t1b = Bounds(1.4, 3.0, 255) +t2b = Bounds(1.2, 3.9, 255) + +#set to also take in boundaries +def set_contour(t0: np.array, t1: np.array, t2: np.array): + """ + Threshold arrays based on desired boundaries and sets contours. + + Parameters + ---------- + t0: 'np.array' + t1: 'np.array' + t2: 'np.array'' + + Returns + ------- + t0: 'np.array' + t1: 'np.array' + t2: 'np.array' + + """ + if t0 is not None and t1 is not None and t2 is not None: + #set the threshold and contours for t0 + t0[np.where(t0 < t0b.upper)] = t0b.upper + t0[np.where(t0 > t0b.lower)] = t0b.lower + t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) + #set the threshold and contours for t1 + t1[np.where(t1 < t1b.upper)] = t1b.upper + t1[np.where(t1 > t1b.lower)] = t2b.lower + t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) + #set the threshold and contours for t2 + t2[np.where(t2 < t2b.upper)] = t2b.upper + t2[np.where(t2 > t2b.lower)] = t2b.lower + t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) + else: + print("Must input valid logarithmic arrays") + return t0, t1, t2 + + +t0, t1, t2 = set_contour(t0, t1, t2) + +def create_mask(tm1: np.array, tm2: np.array, tm3: np.array, map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): + """ + Creates 3 segmented bitmasks + + Parameters + ------- + tm1: 'np.array' + tm2: 'np.array' + tm3: 'np.array' + map1: 'sunpy.map.Map' + map2: 'sunpy.map.Map' + map3: 'sunpy.map.Map' + + Returns + ------- + bmmix: 'np.array' + bmhot: 'np.array' + bmcool: 'np.array' + + """ + with np.errstate(divide="ignore", invalid="ignore"): + bmmix[np.where(tm3 / tm1 >= ((np.mean(map1.data) * 0.6357) / (np.mean(map3.data))))] = 1 + bmhot[np.where(tm1 + tm2 < (0.7 * (np.mean(map2.data) + np.mean(map3.data))))] = 1 + bmcool[np.where(tm3 / tm2 >= ((np.mean(map2.data) * 1.5102) / (np.mean(map2.data))))] = 1 + return bmmix, bmhot, bmcool + + +bmmix, bmhot, bmcool = create_mask(t0, t1, t2, im171, im193, im211) + +#conjunction of 3 bitmasks +cand = bmcool * bmmix * bmhot + +def misid(can: np.array, cir: np.array, xgir: np.array, ygir: np.array, thresh_rad: int): + """ + Removes off-detector mis-identification + + Parameters + ---------- + can: 'np.array' + cir: 'np.array' + xgir: 'np.array' + ygir: 'np.array' + + Returns + ------- + 'np.array' + + """ + #make r a function argument, give name and unit + r = thresh_rad + w = np.where((xgir - center[0]) ** 2 + (ygir - center[1]) ** 2 <= thresh_rad**2) + cir[w] = 1.0 + cand = can * cir + return r, w, cir, cand + + +r, w, cir, cand = misid(cand, circ, xgrid, ygrid, (s[1] / 2.0) - 100) + + +def on_off(cir: np.array, can: np.array): + """ + Seperates on-disk and off-limb coronal holes + + Parameters + ---------- + cir: 'np.array' + can: 'np.array' + + Returns + ------- + 'np.array' + + """ + cir[:] = 0 + r = (rs.value / dattoarc) - 10 + inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + cir[inside] = 1.0 + r = (rs.value / dattoarc) + 40 + outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) + cir[outside] = 1.0 + can = can * cir + plt.figure() + plt.imshow(cand, cmap='viridis') + plt.show + return can + +cand = on_off(circ, cand) + +def contour_data(cand: np.array): + """ + Contours the identified datapoints + + Parameters + ---------- + cand: 'np.array' + + Returns + ------- + cand: 'np.array' + cont: 'tuple' + heir: 'np.array' + + """ + cand = np.array(cand, dtype=np.uint8) + cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + #I think cont might be the x-y coordinates in pixels? + return cand, cont, heir + +cand, cont, heir = contour_data(cand) + +def sort(cont: tuple): + """ + Sorts the contours by size + + Parameters + ---------- + cont: 'tuple' + + Returns + ------- + reord: 'list' + tmp: 'list' + cont: 'list' + sizes: 'list' + + """ + sizes = [] + for i in range(len(cont)): + sizes = np.append(sizes, len(cont[i])) + reord = sizes.ravel().argsort()[::-1] + tmp = list(cont) + for i in range(len(cont)): + tmp[i] = cont[reord[i]] + cont = list(tmp) + return cont, sizes, reord, tmp + + +cont, sizes, reord, tmp = sort(cont) + + +# =====cycles through contours========= + + +def extent(map: sunpy.map.Map, cont: tuple): + """ + Finds coronal hole extent in latitude and longitude + + Parameters + ---------- + map: 'sunpy.map.Map' + cont: 'tuple' + + Returns + ------- + maxxlon: 'astropy.coordinates.angles.core.Longitude' + minxlon: 'astropy.coordinates.angles.core.Longitude' + centlat: 'astropy.coordinates.angles.core.Latitude' + centlon: 'astropy.coordinates.angles.core.Longitude' + + """ + + coord_hpc = map.world2pix(cont) + maxlat = coord_hpc.transform_to(HeliographicStonyhurst).lat.max() + maxlon = coord_hpc.transform_to(HeliographicStonyhurst).lon.max() + minlat = coord_hpc.transform_to(HeliographicStonyhurst).lat.min() + minlon = coord_hpc.transform_to(HeliographicStonyhurst).lat.min() + + # =====CH centroid in lat/lon======= + + centlat = hg.lat[int(ypos), int(xpos)] + centlon = hg.lon[int(ypos), int(xpos)] + return maxlat, maxlon, minlat, minlon, centlat, centlon + + + +def coords(i, csys, cont): + """ + Finds coordinates of CH boundaries + + Parameters + ---------- + i: 'int' + csys: 'astropy.wcs.wcs.WCS' + cont: 'list' + + Returns + ------- + Ywb: 'np.array' + Xwb: 'np.array' + Yeb: 'np.array' + Xeb: 'np.array' + Ynb: 'np.array' + Xnb: 'np.array' + Ysb: 'np.array' + Xsb: 'np.array' + """ + global Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb + Ywb, Xwb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + np.max(cont[i][:, 0, 0]), + 0, + ) + Yeb, Xeb = csys.all_pix2world( + cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], + np.min(cont[i][:, 0, 0]), + 0, + ) + Ynb, Xnb = csys.all_pix2world( + np.max(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + Ysb, Xsb = csys.all_pix2world( + np.min(cont[i][:, 0, 1]), + cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], + 0, + ) + + return Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb + + +def ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, +): + """ + Insertion of CH properties into property array + + Parameters + ---------- + datm: 'np.array' + rs: 'float' + ident: 'int' + props: 'np.array' + arcar: 'np.float64' + arccent: 'list' + pos: 'np.array' + npix: 'list' + trummar: 'np.float64' + centlat: 'str' + centlon: 'str' + mB: 'np.float64' + mBpos: 'np.float64' + mBneg: 'np.float64' + + Returns + ------- + props[0, ident + 1]: 'str' + props[1, ident + 1]: 'str' + props[2, ident + 1]: 'str' + props[3, ident + 1]: 'str' + props[4, ident + 1]: 'str' + props[5, ident + 1]: 'str' + props[6, ident + 1]: 'str' + props[7, ident + 1]: 'str' + props[8, ident + 1]: 'str' + props[9, ident + 1]: 'str' + props[10, ident + 1]: 'str' + props[11, ident + 1]: 'str' + props[12, ident + 1]: 'str' + props[13, ident + 1]: 'str' + props[14, ident + 1]: 'str' + props[15, ident + 1]: 'str' + props[16, ident + 1]: 'str' + props[17, ident + 1]: 'str' + props[18, ident + 1]: 'str' + props[19, ident + 1]: 'str' + props[20, ident + 1]: 'str' + tbpos: 'np.float64' + props[21, ident + 1]: 'str' + tbneg: 'np.float64' + props[22, ident + 1]: 'str' + props[23, ident + 1]: 'str' + props[24, ident + 1]: 'str' + props[25, ident + 1]: 'str' + + """ + props[0, ident + 1] = str(ident) + props[1, ident + 1] = str(np.round(arccent[0])) + props[2, ident + 1] = str(np.round(arccent[1])) + props[3, ident + 1] = str(centlon + centlat) + props[4, ident + 1] = str(np.round(Xeb)) + props[5, ident + 1] = str(np.round(Yeb)) + props[6, ident + 1] = str(np.round(Xwb)) + props[7, ident + 1] = str(np.round(Ywb)) + props[8, ident + 1] = str(np.round(Xnb)) + props[9, ident + 1] = str(np.round(Ynb)) + props[10, ident + 1] = str(np.round(Xsb)) + props[11, ident + 1] = str(np.round(Ysb)) + props[12, ident + 1] = str(eastl + "-" + westl) + props[13, ident + 1] = str(width) + props[14, ident + 1] = f"{trummar/1e+12:.1e}" + props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) + props[16, ident + 1] = str(np.round(mB, 1)) + props[17, ident + 1] = str(np.round(mBpos, 1)) + props[18, ident + 1] = str(np.round(mBneg, 1)) + props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) + props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) + tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) + props[21, ident + 1] = f"{tbpos:.1e}" + tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) + props[22, ident + 1] = f"{tbneg:.1e}" + props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" + props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" + props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" + + +"""Cycles through contours""" + +for i in range(len(cont)): + x = np.append(x, len(cont[i])) + + """only takes values of minimum surface length and calculates area""" + + if len(cont[i]) <= 100: + continue + area = 0.5 * np.abs( + np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) + - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) + ) + arcar = area * (dattoarc**2) + if arcar > 1000: + """finds centroid""" + + chpts = len(cont[i]) + cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] + + """remove quiet sun regions encompassed by coronal holes""" + if ( + cand[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[i][:, 0, 0]) + 1, + cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + print(slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 + + else: + """Create a simple centre point if coronal hole regions is not quiet""" + + arccent = csys.all_pix2world(cent[0], cent[1], 0) + + """classifies off limb CH regions""" + + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + else: + """classifies on disk coronal holes""" + + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + poslin = np.where(slate == 1) + slate[:] = 0 + print(poslin) + + """create an array for magnetic polarity""" + + pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) + pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + npix = list( + np.histogram( + datm[pos[:, 0], pos[:, 1]], + bins=np.arange( + np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, + np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, + 1, + ), + ) + ) + npix[0][np.where(npix[0] == 0)] = 1 + npix[1] = npix[1][:-1] + 0.5 + + wh1 = np.where(npix[1] > 0) + wh2 = np.where(npix[1] < 0) + + """Filters magnetic cutoff values by area""" + + if ( + np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) + <= 10 + and arcar < 9000 + ): + continue + if ( + np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] + and arcar < 40000 + ): + continue + iarr[poslin] = ident + + """create an accurate center point""" + + ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) + xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) + + arccent = csys.all_pix2world(xpos, ypos, 0) + + """calculate average angle coronal hole is subjected to""" + + dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) + ang = np.arcsin(dist / rs) + + """calculate area of CH with minimal projection effects""" + + trupixar = abs(area / np.cos(ang)) + truarcar = trupixar * (dattoarc**2) + trummar = truarcar * ((6.96e08 / rs) ** 2) + + """find CH extent in lattitude and longitude""" + + maxxlon, minxlon, centlat, centlon = extent(i, ypos, xpos, hg, cont) + + """caluclate the mean magnetic field""" + + mB = np.mean(datm[pos[:, 0], pos[:, 1]]) + mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) + mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) + + """finds coordinates of CH boundaries""" + + Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb = coords(i, csys, cont) + + width = round(maxxlon.value) - round(minxlon.value) + + if minxlon.value >= 0.0: + eastl = "W" + str(int(np.round(minxlon.value))) + else: + eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) + if maxxlon.value >= 0.0: + westl = "W" + str(int(np.round(maxxlon.value))) + else: + westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) + + if centlat >= 0.0: + centlat = "N" + str(int(np.round(centlat.value))) + else: + centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) + if centlon >= 0.0: + centlon = "W" + str(int(np.round(centlon.value))) + else: + centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) + + """insertions of CH properties into property array""" + + ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, + ) + """sets up code for next possible coronal hole""" + + ident = ident + 1 + +"""sets ident back to max value of iarr""" + +ident = ident - 1 + +"""stores all CH properties in a text file""" +np.savetxt("ch_summary.txt", props, fmt="%s") + + +def rescale01(arr, cmin=None, cmax=None, a=0, b=1): + """ + Rescales array + + Parameters + ---------- + arr: 'np.arr' + cmin: 'np.float' + cmax: 'np.float' + a: 'int' + b: 'int' + + Returns + ------- + np.array + + """ + if cmin or cmax: + arr = np.clip(arr, cmin, cmax) + return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + + +def plot_tricolor(): + """ + Plots a tricolor mask of image data + + Returns + ------- + plot: 'matplotlib.image.AxesImage' + + """ + + tricolorarray = np.zeros((4096, 4096, 3)) + + data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) + data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) + data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) + + tricolorarray[..., 0] = data_c / np.max(data_c) + tricolorarray[..., 1] = data_b / np.max(data_b) + tricolorarray[..., 2] = data_a / np.max(data_a) + + fig, ax = plt.subplots(figsize=(10, 10)) + + plt.imshow(tricolorarray, origin="lower") + plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) + plt.savefig("tricolor.png") + plt.close() + + +def plot_mask(slate=slate): + """ + Plots the contour mask + + Parameters + ---------- + slate: 'np.array' + + Returns + ------- + plot: 'matplotlib.image.AxesImage' + + """ + + chs = np.where(iarr > 0) + slate[chs] = 1 + slate = np.array(slate, dtype=np.uint8) + cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + circ[:] = 0 + r = rs / dattoarc + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + circ[w] = 1.0 + + plt.figure(figsize=(10, 10)) + plt.xlim(143, 4014) + plt.ylim(143, 4014) + plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) + plt.gca().set_aspect("equal", adjustable="box") + plt.axis("off") + plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) + plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) + + plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) + + +plot_tricolor() +plot_mask() + +if __name__ == "__main__": + import_functions(INPUT_FILES['aia171'], INPUT_FILES['aia193'], INPUT_FILES['aia211'], INPUT_FILES['hmi_mag']) \ No newline at end of file diff --git a/chimerapy/tests/test_chimera.py b/chimerapy/tests/test_chimera.py index 518ab50..c3da28b 100644 --- a/chimerapy/tests/test_chimera.py +++ b/chimerapy/tests/test_chimera.py @@ -7,22 +7,63 @@ from astropy.io import fits from astropy.modeling.models import Gaussian2D -from chimerapy.chimera import (Bounds, Xeb, Xnb, Xsb, Xwb, Yeb, Ynb, Ysb, Ywb, - ang, arcar, arccent, area, cent, centlat, - centlon, chpts, cont, coords, csys, data, datb, - datc, datm, dist, eastl, extent, filter, hg, - ins_prop, mB, mBneg, mBpos, npix, pos, - remove_neg, rescale_aia, rescale_hmi, - set_contour, sort, threshold, truarcar, trummar, - trupixar, westl, width, xpos, ypos) - -file_path = "./" - -im171 = glob.glob(file_path + "*171*.fts") -im193 = glob.glob(file_path + "*193*.fts") -im211 = glob.glob(file_path + "*211*.fts") -imhmi = glob.glob(file_path + "*hmi*.fts") +from chimerapy.chimera import ( + Bounds, + Xeb, + Xnb, + Xsb, + Xwb, + Yeb, + Ynb, + Ysb, + Ywb, + ang, + arcar, + arccent, + area, + cent, + centlat, + centlon, + chpts, + cont, + coords, + csys, + data, + datb, + datc, + datm, + dist, + eastl, + extent, + filter, + hg, + ins_prop, + mB, + mBneg, + mBpos, + npix, + pos, + remove_neg, + rescale_aia, + rescale_hmi, + set_contour, + sort, + threshold, + truarcar, + trummar, + trupixar, + westl, + width, + xpos, + ypos, +) +from chimerapy.chimera import * +INPUT_FILES = {"aia171": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00171_fd_20160922_103010.fts.gz", +"aia193": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00193_fd_20160922_103041.fts.gz", +"aia211": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00211_fd_20160922_103046.fts.gz", +"hmi_mag": "https://solarmonitor.org/data/2016/09/22/fits/shmi/shmi_maglc_fd_20160922_094640.fts.gz", +} def img_present(): assert im171 != [] or im193 != [] or im211 != [] or imhmi != [], "Not all required files present" @@ -110,8 +151,6 @@ def s_rs(): warnings.warn("Solar radius may be inconsistant with accepted value (~973)") -s_rs() - ident = 1 iarr = np.zeros((s[0], s[1]), dtype=np.byte) bmcool = np.zeros((s[0], s[1]), dtype=np.float32) @@ -242,182 +281,183 @@ def test_dupl(): for i in range(len(cont)): x = np.append(x, len(cont[i])) - def test_extent(): - i_maxxlat = None - i_maxxlon = None - i_maxylat = None - i_maxylon = None - i_minxlon = None - i_minylat = None - i_minylon = None - i_minxlat = None - extent(i, ypos, xpos, hg, cont) - global maxxlat, maxxlon, maxylat, maxylon, minxlon, minylat, minylon, minxlat - assert i_maxxlat != maxxlat, "maxxlat not created successfully" - assert i_maxxlon != maxxlon, "maxxlon not created successfully" - assert i_maxylat != maxylat, "maxylat not created successfully" - assert i_maxylon != maxylon, "maxylon not created successfully" - assert i_minxlon != minxlon, "minxlon not created successfully" - assert i_minylat != minylat, "minylat not created successfully" - assert i_minylon != minylon, "minylon not created successfully" - assert i_minxlat != minxlat, "minxlat not created successfully" - - def test_coords(): - i_Ywb = None - i_Xwb = None - i_Yeb = None - i_Xeb = None - i_Ynb = None - i_Xnb = None - i_Ysb = None - i_Xsb = None - coords(i, csys, cont) - assert i_Ywb != Ywb, "Ywb not created successfully" - assert i_Xwb != Xwb, "Xwb not created successfully" - assert i_Yeb != Yeb, "Yeb not created successfully" - assert i_Xeb != Xeb, "Xeb not created successfully" - assert i_Ynb != Ynb, "Ynb not created successfully" - assert i_Xnb != Xnb, "Xnb not created successfully" - assert i_Ysb != Ysb, "Ysb not created successfully" - assert i_Xsb != Xsb, "Xsb not created successfully" - - def test_props(): - ins_prop( - datm, - rs, - ident, - props, - arcar, - arccent, - pos, - npix, - trummar, - centlat, - centlon, - mB, - mBpos, - mBneg, - Ywb, - Xwb, - Yeb, - Xeb, - Ynb, - Xnb, - Ysb, - Xsb, - width, - eastl, - westl, - ) - assert np.any(props is None) is not False, "Property array should not contain empty entries" - assert np.all(props is None) is not False, "Property array should not be empty" - - def test_loop_variables(): - for i in range(len(cont)): - assert len(cont[i]) > 100, "Contour length should be greater than 100" - - def test_fill_polygon(): - # Example data similar to what might be used in your application - cand = np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) - - iarr = np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) - - # Example contour data - cont = np.array([[[[1, 2], [1, 3], [2, 3], [2, 2]]]]) # Example contour with vertices - - # Example slate array - slate = np.array(iarr) - - # Extracting the polygon vertices from cont[i] - polygon_vertices = np.array(list(zip(cont[0][:, 0, 1], cont[0][:, 0, 0]))) - - # Simulate conditions to enter the if statement - if ( - cand[ - np.max(cont[0][:, 0, 0]) + 1, - cont[0][np.where(cont[0][:, 0, 0] == np.max(cont[0][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ) and ( - iarr[ - np.max(cont[0][:, 0, 0]) + 1, - cont[0][np.where(cont[0][:, 0, 0] == np.max(cont[0][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ): - # Call fill_polygon with the polygon vertices and slate array - mahotas.polygon.fill_polygon(polygon_vertices, slate) - - # Perform actions after filling the polygon - print("After filling polygon:") - print(slate) - iarr[np.where(slate == 1)] = 0 - slate[:] = 0 # Reset slate array - - # Assertions to verify correctness - assert np.array_equal( - slate, - np.array([[0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]), - ), "Test failed: slate array does not match expected result" - - assert np.array_equal( - iarr, - np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]), - ), "Test failed: iarr array does not match expected result" - - def test_limb(): - arccent = csys.all_pix2world(cent[0], cent[1], 0) - if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( - np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) - assert np.sum(offarr) > 0, "Offarr was not modified successfully" - else: - # =====classifies on disk coronal holes======= - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - slate[:] = 0 - assert np.sum(slate) > 0, "Slate was not modified successfully" - - def test_magpol(): - assert npix[0][np.where(npix[0] == 0)] != 0, "Npix[0] should not be equal to zero at its zeros" - assert npix[1] != 0, "Npix[1] should not be equal to 0" - npixtest = [ - np.array([2, -1, 0, 3, -2, 1]), # Example npix[0] (bin counts) - np.array([1, -1, 0, 2, -2, 1]), # Example npix[1] (bin edges) + +def test_extent(): + i_maxxlat = None + i_maxxlon = None + i_maxylat = None + i_maxylon = None + i_minxlon = None + i_minylat = None + i_minylon = None + i_minxlat = None + extent(i, ypos, xpos, hg, cont) + global maxxlat, maxxlon, maxylat, maxylon, minxlon, minylat, minylon, minxlat + assert i_maxxlat != maxxlat, "maxxlat not created successfully" + assert i_maxxlon != maxxlon, "maxxlon not created successfully" + assert i_maxylat != maxylat, "maxylat not created successfully" + assert i_maxylon != maxylon, "maxylon not created successfully" + assert i_minxlon != minxlon, "minxlon not created successfully" + assert i_minylat != minylat, "minylat not created successfully" + assert i_minylon != minylon, "minylon not created successfully" + assert i_minxlat != minxlat, "minxlat not created successfully" + + +def test_coords(): + i_Ywb = None + i_Xwb = None + i_Yeb = None + i_Xeb = None + i_Ynb = None + i_Xnb = None + i_Ysb = None + i_Xsb = None + coords(i, csys, cont) + assert i_Ywb != Ywb, "Ywb not created successfully" + assert i_Xwb != Xwb, "Xwb not created successfully" + assert i_Yeb != Yeb, "Yeb not created successfully" + assert i_Xeb != Xeb, "Xeb not created successfully" + assert i_Ynb != Ynb, "Ynb not created successfully" + assert i_Xnb != Xnb, "Xnb not created successfully" + assert i_Ysb != Ysb, "Ysb not created successfully" + assert i_Xsb != Xsb, "Xsb not created successfully" + + +def test_props(): + ins_prop( + datm, + rs, + ident, + props, + arcar, + arccent, + pos, + npix, + trummar, + centlat, + centlon, + mB, + mBpos, + mBneg, + Ywb, + Xwb, + Yeb, + Xeb, + Ynb, + Xnb, + Ysb, + Xsb, + width, + eastl, + westl, + ) + assert np.any(props is None) is not False, "Property array should not contain empty entries" + assert np.all(props is None) is not False, "Property array should not be empty" + + +def test_loop_variables(): + for i in range(len(cont)): + assert len(cont[i]) > 100, "Contour length should be greater than 100" + + +def test_fill_polygon(): + cand = np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) + + iarr = np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) + + cont = np.array([[[[1, 2], [1, 3], [2, 3], [2, 2]]]]) + + slate = np.array(iarr) + + polygon_vertices = np.array(list(zip(cont[0][:, 0, 1], cont[0][:, 0, 0]))) + + if ( + cand[ + np.max(cont[0][:, 0, 0]) + 1, + cont[0][np.where(cont[0][:, 0, 0] == np.max(cont[0][:, 0, 0]))[0][0], 0, 1], + ] + > 0 + ) and ( + iarr[ + np.max(cont[0][:, 0, 0]) + 1, + cont[0][np.where(cont[0][:, 0, 0] == np.max(cont[0][:, 0, 0]))[0][0], 0, 1], ] - wh1_expected = np.where(npixtest[1] > 0) - wh1_actual = np.where(npixtest[1] > 0) - - wh2_expected = np.where(npixtest[1] < 0) - wh2_actual = np.where(npixtest[1] < 0) - - assert np.array_equal( - wh1_actual, wh1_expected - ), f"Test failed for wh1: Expected {wh1_expected}, got {wh1_actual}" - assert np.array_equal( - wh2_actual, wh2_expected - ), f"Test failed for wh1: Expected {wh2_expected}, got {wh2_actual}" - - assert area is not None, "Area variable not created successfully" - assert arcar is not None, "Arcar variable not created successfully" - assert chpts is not None, "Chpts variable not created successfully" - assert cent is not None, "Cent variable not created successfully" - assert arccent is not None, "Arccent variable not created successfully" - assert ypos is not None, "Ypos variable not created successfully" - assert xpos is not None, "Xpos variable not created suffessfully" - assert dist is not None, "Dist variable not created successfully" - assert ang is not None, "Ang variable not created successfully" - assert trupixar is not None, "Trupixar variable not created successfully" - assert truarcar is not None, "Truarcar variable not created successfully" - assert trummar is not None, "Trummar variable not created successfully" - assert mB is not None, "mB variable not created successfully" - assert mBpos is not None, "mBpos variable not created successfully" - assert mBneg is not None, "mBneg variable not created successfully" - assert width is not None, "width variable not created successfully" - assert eastl is not None, "eastl variable not created successfully" - assert westl is not None, "westl variable not created successfully" - assert centlat is not None, "centlat variable not created successfully" - assert centlon is not None, "centlon variable not created successfully" + > 0 + ): + + mahotas.polygon.fill_polygon(polygon_vertices, slate) + + + print("After filling polygon:") + print(slate) + iarr[np.where(slate == 1)] = 0 + slate[:] = 0 + + assert np.array_equal( + slate, + np.array([[0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]), + ), "Test failed: slate array does not match expected result" + + assert np.array_equal( + iarr, + np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]), + ), "Test failed: iarr array does not match expected result" + + +def test_limb(): + arccent = csys.all_pix2world(cent[0], cent[1], 0) + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( + np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + ): + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + assert np.sum(offarr) > 0, "Offarr was not modified successfully" + else: + mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + slate[:] = 0 + assert np.sum(slate) > 0, "Slate was not modified successfully" + + +def test_magpol(): + assert npix[0][np.where(npix[0] == 0)] != 0, "Npix[0] should not be equal to zero at its zeros" + assert npix[1] != 0, "Npix[1] should not be equal to 0" + npixtest = [ + np.array([2, -1, 0, 3, -2, 1]), + np.array([1, -1, 0, 2, -2, 1]), + ] + wh1_expected = np.where(npixtest[1] > 0) + wh1_actual = np.where(npixtest[1] > 0) + + wh2_expected = np.where(npixtest[1] < 0) + wh2_actual = np.where(npixtest[1] < 0) + + assert np.array_equal( + wh1_actual, wh1_expected + ), f"Test failed for wh1: Expected {wh1_expected}, got {wh1_actual}" + assert np.array_equal( + wh2_actual, wh2_expected + ), f"Test failed for wh1: Expected {wh2_expected}, got {wh2_actual}" + + +assert area is not None, "Area variable not created successfully" +assert arcar is not None, "Arcar variable not created successfully" +assert chpts is not None, "Chpts variable not created successfully" +assert cent is not None, "Cent variable not created successfully" +assert arccent is not None, "Arccent variable not created successfully" +assert ypos is not None, "Ypos variable not created successfully" +assert xpos is not None, "Xpos variable not created suffessfully" +assert dist is not None, "Dist variable not created successfully" +assert ang is not None, "Ang variable not created successfully" +assert trupixar is not None, "Trupixar variable not created successfully" +assert truarcar is not None, "Truarcar variable not created successfully" +assert trummar is not None, "Trummar variable not created successfully" +assert mB is not None, "mB variable not created successfully" +assert mBpos is not None, "mBpos variable not created successfully" +assert mBneg is not None, "mBneg variable not created successfully" +assert width is not None, "width variable not created successfully" +assert eastl is not None, "eastl variable not created successfully" +assert westl is not None, "westl variable not created successfully" +assert centlat is not None, "centlat variable not created successfully" +assert centlon is not None, "centlon variable not created successfully" assert os.path.exists("ch_summary.txt"), "Summary file not saved correctly" assert os.path.exists("tricolor.png"), "Tricolor image not saved correctly" From a67d8a821b423b6db0ee8ab4b8194665d0cc9349 Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:39:49 -0400 Subject: [PATCH 08/10] Here is the updated pull request with some changes I made and a large document attached at the end in a link that describes all of the important processes, functions, and variables --- CHIMERA_V2.py | 686 -------------------------------- CHIMERA_V3.py | 686 -------------------------------- chimerapy/chimera.py | 2 +- chimerapy/chimera_copy.py | 219 +++++----- chimerapy/tests/test_chimera.py | 81 +--- 5 files changed, 146 insertions(+), 1528 deletions(-) delete mode 100644 CHIMERA_V2.py delete mode 100644 CHIMERA_V3.py diff --git a/CHIMERA_V2.py b/CHIMERA_V2.py deleted file mode 100644 index 48b0111..0000000 --- a/CHIMERA_V2.py +++ /dev/null @@ -1,686 +0,0 @@ -# import required libraries -import glob -import sys - -import astropy.units as u -import cv2 -import mahotas -import matplotlib.pyplot as plt -import numpy as np -import scipy -import scipy.interpolate -import sunpy -import sunpy.map -from astropy import wcs -from astropy.io import fits -from astropy.modeling.models import Gaussian2D -from astropy.visualization import astropy_mpl_style - -plt.style.use(astropy_mpl_style) - -# loading in the images as fits files - -im171 = glob.glob("171.fts") -im193 = glob.glob("193.fts") -im211 = glob.glob("211.fts") -imhmi = glob.glob("hmi.fts") - -# ensure that all images are present - -if im171 == [] or im193 == [] or im211 == [] or imhmi == []: - print("Not all required files present") - sys.exit() - -# Two functions that rescale the aia and hmi images from any original size to any final size - -# didn't normalize by exposure time for hmi because it was equal to 0 - - -def rescale_aia(image: np.array, orig_res: int, desired_res: int): - hed = fits.getheader(image[0], 0) - dat = fits.getdata(image[0], 0) / (hed["EXPTIME"]) - if desired_res > orig_res: - scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - elif desired_res < orig_res: - scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - - -def rescale_hmi(image: np.array, orig_res: int, desired_res: int): - hdu_number = 0 - hed = fits.getheader(image[0], hdu_number) - dat = fits.getdata(image[0], ext=0) - if desired_res > orig_res: - scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - elif desired_res < orig_res: - scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - - -# defining data and headers which are used in later steps -hdu_number = 0 - -data = rescale_aia(im171, 1024, 4096) -datb = rescale_aia(im193, 1024, 4096) -datc = rescale_aia(im211, 1024, 4096) -datm = rescale_hmi(imhmi, 1024, 4096) - - -# rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 -# ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively -# rotates array if 'crota1' is greater than 90 degrees -def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): - global heda, hedb, hedc, hedm - heda = fits.getheader(aiaa[0], 0) - hedb = fits.getheader(aiab[0], 0) - hedc = fits.getheader(aiac[0], 0) - hedm = fits.getheader(aiam[0], 0) - if hedb["ctype1"] != "solar_x ": - hedb["ctype1"] = "solar_x " - hedb["ctype2"] = "solar_y " - if heda["cdelt1"] > 1: - heda["cdelt1"], heda["cdelt2"], heda["crpix1"], heda["crpix2"] = ( - heda["cdelt1"] / 4.0, - heda["cdelt2"] / 4.0, - heda["crpix1"] * 4.0, - heda["crpix2"] * 4.0, - ) - hedb["cdelt1"], hedb["cdelt2"], hedb["crpix1"], hedb["crpix2"] = ( - hedb["cdelt1"] / 4.0, - hedb["cdelt2"] / 4.0, - hedb["crpix1"] * 4.0, - hedb["crpix2"] * 4.0, - ) - hedc["cdelt1"], hedc["cdelt2"], hedc["crpix1"], hedc["crpix2"] = ( - hedc["cdelt1"] / 4.0, - hedc["cdelt2"] / 4.0, - hedc["crpix1"] * 4.0, - hedc["crpix2"] * 4.0, - ) - if hedm["crota1"] > 90: - datm = np.rot90(np.rot90(datm)) - - -filter(im171, im193, im211, imhmi) - - -# removes negative values from an array -def remove_neg(aiaa: np.array, aiab: np.array, aiac: np.array): - global data, datb, datc - data[np.where(data <= 0)] = 0 - datb[np.where(datb <= 0)] = 0 - datc[np.where(datc <= 0)] = 0 - if len(data[data < 0]) != 0: - print("data contains negative") - if len(datb[datb < 0]) != 0: - print("data contains negative") - if len(datc[datc < 0]) != 0: - print("datc contains negative") - - -remove_neg(im171, im193, im211) - -# defines the shape (length) of the array as "s" and the solar radius as "rs" -s = np.shape(data) -rs = heda["rsun"] - - -def pix_arc(aia: np.array): - global dattoarc - dattoarc = heda["cdelt1"] - global conver - conver = ((s[0]) / 2) * dattoarc / hedm["cdelt1"] - (s[1] / 2) - global convermul - convermul = dattoarc / hedm["cdelt1"] - - -pix_arc(im171) - -# converts to the Heliographic Stonyhurst coordinate system - - -def to_helio(image: np.array): - aia = sunpy.map.Map(image) - adj = 4096 / aia.dimensions[0].value - x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj - global hpc - hpc = aia.pixel_to_world(x, y) - global hg - hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) - global csys - csys = wcs.WCS(hedb) - - -to_helio(im171) - -# setting up arrays to be used in later processing -# only difference between iarr and bmcool is integer vs. float -ident = 1 -iarr = np.zeros((s[0], s[1]), dtype=np.byte) -bmcool = np.zeros((s[0], s[1]), dtype=np.float32) -offarr, slate = np.array(iarr), np.array(iarr) -cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) -circ = np.zeros((s[0], s[1]), dtype=int) - -# creation of a 2d gaussian for magnetic cut offs -r = (s[1] / 2.0) - 450 -xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) -center = [int(s[1] / 2.0), int(s[1] / 2.0)] -w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) -y, x = np.mgrid[0:4096, 0:4096] -garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) -# plt.plot(garr) -garr[w] = 1.0 - -# creates sub-arrays of props to isolate column of index 0 and column of index 1 -# what is props?? -props = np.zeros((26, 30), dtype="", - "", - "", - "BMAX", - "BMIN", - "TOT_B+", - "TOT_B-", - "", - "", - "", -) -props[:, 1] = ( - "num", - '"', - '"', - "H°", - '"', - '"', - '"', - '"', - '"', - '"', - '"', - '"', - "H°", - "°", - "Mm^2", - "%", - "G", - "G", - "G", - "G", - "G", - "G", - "G", - "Mx", - "Mx", - "Mx", -) -# define threshold values in log s - -with np.errstate(divide="ignore"): - t0 = np.log10(datc) - t1 = np.log10(datb) - t2 = np.log10(data) - - -class Bounds: - def __init__(self, upper, lower, slope): - self.upper = upper - self.lower = lower - self.slope = slope - - def new_u(self, new_upper): - self.upper = new_upper - - def new_l(self, new_lower): - self.lower = new_lower - - def new_s(self, new_slope): - self.slope = new_slope - - -t0b = Bounds(0.8, 2.7, 255) -t1b = Bounds(1.4, 3.0, 255) -t2b = Bounds(1.2, 3.9, 255) - - -def threshold(tval: np.array): - global t0, t1, t2 - if tval.all() == t0.all(): - t0[np.where(t0 < t0b.upper)] = t0b.upper - t0[np.where(t0 > t0b.lower)] = t0b.lower - if tval.all() == t1.all(): - t1[np.where(t1 < t1b.upper)] = t1b.upper - t1[np.where(t1 > t1b.lower)] = t2b.lower - if tval.all() == t2.all(): - t2[np.where(t2 < t2b.upper)] = t2b.upper - t2[np.where(t2 > t2b.lower)] = t2b.lower - - -threshold(t0) -threshold(t1) -threshold(t2) - - -def set_contour(tval: np.array): - global t0, t1, t2 - if tval.all() == t0.all(): - t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) - elif tval.all() == t1.all(): - t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) - elif tval.all() == t2.all(): - t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) - - -set_contour(t0) -set_contour(t1) -set_contour(t2) - - -def create_mask(): - global t0, t1, t2, bmmix, bmhot, bmcool - with np.errstate(divide="ignore", invalid="ignore"): - bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 - bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 - bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 - - -create_mask() - - -def conjunction(): - global bmhot, bmcool, bmmix, cand - cand = bmcool * bmmix * bmhot - - -conjunction() - - -def misid(): - global s, r, w, circ, cand - r = (s[1] / 2.0) - 100 - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[w] = 1.0 - cand = cand * circ - - -misid() - - -def on_off(): - global circ, cand - circ[:] = 0 - r = (rs / dattoarc) - 10 - inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[inside] = 1.0 - r = (rs / dattoarc) + 40 - outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) - circ[outside] = 1.0 - cand = cand * circ - - -on_off() - - -def contours(): - global cand, cont, heir - cand = np.array(cand, dtype=np.uint8) - cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - -contours() - - -def sort(): - global sizes, reord, tmp, cont - sizes = [] - for i in range(len(cont)): - sizes = np.append(sizes, len(cont[i])) - reord = sizes.ravel().argsort()[::-1] - tmp = list(cont) - for i in range(len(cont)): - tmp[i] = cont[reord[i]] - cont = list(tmp) - - -sort() - - -# =====cycles through contours========= - -for i in range(len(cont)): - x = np.append(x, len(cont[i])) - - # =====only takes values of minimum surface length and calculates area====== - - if len(cont[i]) <= 100: - continue - area = 0.5 * np.abs( - np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) - - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) - ) - arcar = area * (dattoarc**2) - if arcar > 1000: - # =====finds centroid======= - - chpts = len(cont[i]) - cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] - - # ===remove quiet sun regions encompassed by coronal holes====== - - if ( - cand[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ) and ( - iarr[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - iarr[np.where(slate == 1)] = 0 - slate[:] = 0 - - else: - # ====create a simple centre point====== - - arccent = csys.all_pix2world(cent[0], cent[1], 0) - - # ====classifies off limb CH regions======== - - if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( - np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) - else: - # =====classifies on disk coronal holes======= - - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - poslin = np.where(slate == 1) - slate[:] = 0 - print(poslin) - - # ====create an array for magnetic polarity======== - - pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) - pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - npix = list( - np.histogram( - datm[pos[:, 0], pos[:, 1]], - bins=np.arange( - np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, - np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, - 1, - ), - ) - ) - npix[0][np.where(npix[0] == 0)] = 1 - npix[1] = npix[1][:-1] + 0.5 - - wh1 = np.where(npix[1] > 0) - wh2 = np.where(npix[1] < 0) - - # =====magnetic cut offs dependant on area========= - - if ( - np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) - <= 10 - and arcar < 9000 - ): - continue - if ( - np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] - and arcar < 40000 - ): - continue - iarr[poslin] = ident - - # ====create an accurate center point======= - - ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) - xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) - - arccent = csys.all_pix2world(xpos, ypos, 0) - - # ======calculate average angle coronal hole is subjected to====== - - dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) - ang = np.arcsin(dist / rs) - - # =====calculate area of CH with minimal projection effects====== - - trupixar = abs(area / np.cos(ang)) - truarcar = trupixar * (dattoarc**2) - trummar = truarcar * ((6.96e08 / rs) ** 2) - - # ====find CH extent in lattitude and longitude======== - - maxxlat = hg.lat[ - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - ] - maxxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - ] - maxylat = hg.lat[ - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - maxylon = hg.lon[ - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - minxlat = hg.lat[ - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - ] - minxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - ] - minylat = hg.lat[ - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - minylon = hg.lon[ - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - - # =====CH centroid in lat/lon======= - - centlat = hg.lat[int(ypos), int(xpos)] - centlon = hg.lon[int(ypos), int(xpos)] - - # ====caluclate the mean magnetic field===== - - mB = np.mean(datm[pos[:, 0], pos[:, 1]]) - mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) - mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) - - # =====finds coordinates of CH boundaries======= - - Ywb, Xwb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - 0, - ) - Yeb, Xeb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - 0, - ) - Ynb, Xnb = csys.all_pix2world( - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - Ysb, Xsb = csys.all_pix2world( - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - - width = round(maxxlon.value) - round(minxlon.value) - - if minxlon.value >= 0.0: - eastl = "W" + str(int(np.round(minxlon.value))) - else: - eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) - if maxxlon.value >= 0.0: - westl = "W" + str(int(np.round(maxxlon.value))) - else: - westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) - - if centlat >= 0.0: - centlat = "N" + str(int(np.round(centlat.value))) - else: - centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) - if centlon >= 0.0: - centlon = "W" + str(int(np.round(centlon.value))) - else: - centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) - - # ====insertions of CH properties into property array===== - - props[0, ident + 1] = str(ident) - props[1, ident + 1] = str(np.round(arccent[0])) - props[2, ident + 1] = str(np.round(arccent[1])) - props[3, ident + 1] = str(centlon + centlat) - props[4, ident + 1] = str(np.round(Xeb)) - props[5, ident + 1] = str(np.round(Yeb)) - props[6, ident + 1] = str(np.round(Xwb)) - props[7, ident + 1] = str(np.round(Ywb)) - props[8, ident + 1] = str(np.round(Xnb)) - props[9, ident + 1] = str(np.round(Ynb)) - props[10, ident + 1] = str(np.round(Xsb)) - props[11, ident + 1] = str(np.round(Ysb)) - props[12, ident + 1] = str(eastl + "-" + westl) - props[13, ident + 1] = str(width) - props[14, ident + 1] = f"{trummar/1e+12:.1e}" - props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) - props[16, ident + 1] = str(np.round(mB, 1)) - props[17, ident + 1] = str(np.round(mBpos, 1)) - props[18, ident + 1] = str(np.round(mBneg, 1)) - props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) - props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) - tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) - props[21, ident + 1] = f"{tbpos:.1e}" - tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) - props[22, ident + 1] = f"{tbneg:.1e}" - props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" - props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" - props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" - - # =====sets up code for next possible coronal hole===== - - ident = ident + 1 - -# =====sets ident back to max value of iarr====== - -ident = ident - 1 -np.savetxt("ch_summary.txt", props, fmt="%s") - - -from skimage.util import img_as_ubyte - - -def rescale01(arr, cmin=None, cmax=None, a=0, b=1): - if cmin or cmax: - arr = np.clip(arr, cmin, cmax) - return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a - - -def plot_tricolor(): - tricolorarray = np.zeros((4096, 4096, 3)) - - data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) - data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) - data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) - - tricolorarray[..., 0] = data_c / np.max(data_c) - tricolorarray[..., 1] = data_b / np.max(data_b) - tricolorarray[..., 2] = data_a / np.max(data_a) - - fig, ax = plt.subplots(figsize=(10, 10)) - - plt.imshow(tricolorarray, origin="lower") # , extent = ) - cs = plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) - plt.savefig("tricolor.png") - plt.close() - - -def plot_mask(slate=slate): - chs = np.where(iarr > 0) - slate[chs] = 1 - slate = np.array(slate, dtype=np.uint8) - cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - circ[:] = 0 - r = rs / dattoarc - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[w] = 1.0 - - plt.figure(figsize=(10, 10)) - plt.xlim(143, 4014) - plt.ylim(143, 4014) - plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) - plt.gca().set_aspect("equal", adjustable="box") - plt.axis("off") - cs = plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) - cs = plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) - - plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) - # plt.close() - - -# ====stores all CH properties in a text file===== - -plot_tricolor() -plot_mask() - -# ====EOF==== diff --git a/CHIMERA_V3.py b/CHIMERA_V3.py deleted file mode 100644 index 48b0111..0000000 --- a/CHIMERA_V3.py +++ /dev/null @@ -1,686 +0,0 @@ -# import required libraries -import glob -import sys - -import astropy.units as u -import cv2 -import mahotas -import matplotlib.pyplot as plt -import numpy as np -import scipy -import scipy.interpolate -import sunpy -import sunpy.map -from astropy import wcs -from astropy.io import fits -from astropy.modeling.models import Gaussian2D -from astropy.visualization import astropy_mpl_style - -plt.style.use(astropy_mpl_style) - -# loading in the images as fits files - -im171 = glob.glob("171.fts") -im193 = glob.glob("193.fts") -im211 = glob.glob("211.fts") -imhmi = glob.glob("hmi.fts") - -# ensure that all images are present - -if im171 == [] or im193 == [] or im211 == [] or imhmi == []: - print("Not all required files present") - sys.exit() - -# Two functions that rescale the aia and hmi images from any original size to any final size - -# didn't normalize by exposure time for hmi because it was equal to 0 - - -def rescale_aia(image: np.array, orig_res: int, desired_res: int): - hed = fits.getheader(image[0], 0) - dat = fits.getdata(image[0], 0) / (hed["EXPTIME"]) - if desired_res > orig_res: - scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - elif desired_res < orig_res: - scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - - -def rescale_hmi(image: np.array, orig_res: int, desired_res: int): - hdu_number = 0 - hed = fits.getheader(image[0], hdu_number) - dat = fits.getdata(image[0], ext=0) - if desired_res > orig_res: - scaled_array = np.linspace(start=0, stop=desired_res, num=orig_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - elif desired_res < orig_res: - scaled_array = np.linspace(start=0, stop=orig_res, num=desired_res) - dn = scipy.interpolate.RectBivariateSpline(scaled_array, scaled_array, dat) - if len(dn(np.arange(0, desired_res), np.arange(0, desired_res))) != desired_res: - print("Incorrect image resolution") - sys.exit() - else: - return dn(np.arange(0, desired_res), np.arange(0, desired_res)) - - -# defining data and headers which are used in later steps -hdu_number = 0 - -data = rescale_aia(im171, 1024, 4096) -datb = rescale_aia(im193, 1024, 4096) -datc = rescale_aia(im211, 1024, 4096) -datm = rescale_hmi(imhmi, 1024, 4096) - - -# rescales 'cdelt1' 'cdelt2' 'cpix1' 'cipix2' if 'cdelt1' > 1 -# ensures 'ctype1' 'ctype2' are correctly defined as 'solar_x' and 'solar_y' respectively -# rotates array if 'crota1' is greater than 90 degrees -def filter(aiaa: np.array, aiab: np.array, aiac: np.array, aiam: np.array): - global heda, hedb, hedc, hedm - heda = fits.getheader(aiaa[0], 0) - hedb = fits.getheader(aiab[0], 0) - hedc = fits.getheader(aiac[0], 0) - hedm = fits.getheader(aiam[0], 0) - if hedb["ctype1"] != "solar_x ": - hedb["ctype1"] = "solar_x " - hedb["ctype2"] = "solar_y " - if heda["cdelt1"] > 1: - heda["cdelt1"], heda["cdelt2"], heda["crpix1"], heda["crpix2"] = ( - heda["cdelt1"] / 4.0, - heda["cdelt2"] / 4.0, - heda["crpix1"] * 4.0, - heda["crpix2"] * 4.0, - ) - hedb["cdelt1"], hedb["cdelt2"], hedb["crpix1"], hedb["crpix2"] = ( - hedb["cdelt1"] / 4.0, - hedb["cdelt2"] / 4.0, - hedb["crpix1"] * 4.0, - hedb["crpix2"] * 4.0, - ) - hedc["cdelt1"], hedc["cdelt2"], hedc["crpix1"], hedc["crpix2"] = ( - hedc["cdelt1"] / 4.0, - hedc["cdelt2"] / 4.0, - hedc["crpix1"] * 4.0, - hedc["crpix2"] * 4.0, - ) - if hedm["crota1"] > 90: - datm = np.rot90(np.rot90(datm)) - - -filter(im171, im193, im211, imhmi) - - -# removes negative values from an array -def remove_neg(aiaa: np.array, aiab: np.array, aiac: np.array): - global data, datb, datc - data[np.where(data <= 0)] = 0 - datb[np.where(datb <= 0)] = 0 - datc[np.where(datc <= 0)] = 0 - if len(data[data < 0]) != 0: - print("data contains negative") - if len(datb[datb < 0]) != 0: - print("data contains negative") - if len(datc[datc < 0]) != 0: - print("datc contains negative") - - -remove_neg(im171, im193, im211) - -# defines the shape (length) of the array as "s" and the solar radius as "rs" -s = np.shape(data) -rs = heda["rsun"] - - -def pix_arc(aia: np.array): - global dattoarc - dattoarc = heda["cdelt1"] - global conver - conver = ((s[0]) / 2) * dattoarc / hedm["cdelt1"] - (s[1] / 2) - global convermul - convermul = dattoarc / hedm["cdelt1"] - - -pix_arc(im171) - -# converts to the Heliographic Stonyhurst coordinate system - - -def to_helio(image: np.array): - aia = sunpy.map.Map(image) - adj = 4096 / aia.dimensions[0].value - x, y = (np.meshgrid(*[np.arange(adj * v.value) for v in aia.dimensions]) * u.pixel) / adj - global hpc - hpc = aia.pixel_to_world(x, y) - global hg - hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) - global csys - csys = wcs.WCS(hedb) - - -to_helio(im171) - -# setting up arrays to be used in later processing -# only difference between iarr and bmcool is integer vs. float -ident = 1 -iarr = np.zeros((s[0], s[1]), dtype=np.byte) -bmcool = np.zeros((s[0], s[1]), dtype=np.float32) -offarr, slate = np.array(iarr), np.array(iarr) -cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) -circ = np.zeros((s[0], s[1]), dtype=int) - -# creation of a 2d gaussian for magnetic cut offs -r = (s[1] / 2.0) - 450 -xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) -center = [int(s[1] / 2.0), int(s[1] / 2.0)] -w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) -y, x = np.mgrid[0:4096, 0:4096] -garr = Gaussian2D(1, s[0] / 2, s[1] / 2, 2000 / 2.3548, 2000 / 2.3548)(x, y) -# plt.plot(garr) -garr[w] = 1.0 - -# creates sub-arrays of props to isolate column of index 0 and column of index 1 -# what is props?? -props = np.zeros((26, 30), dtype="", - "", - "", - "BMAX", - "BMIN", - "TOT_B+", - "TOT_B-", - "", - "", - "", -) -props[:, 1] = ( - "num", - '"', - '"', - "H°", - '"', - '"', - '"', - '"', - '"', - '"', - '"', - '"', - "H°", - "°", - "Mm^2", - "%", - "G", - "G", - "G", - "G", - "G", - "G", - "G", - "Mx", - "Mx", - "Mx", -) -# define threshold values in log s - -with np.errstate(divide="ignore"): - t0 = np.log10(datc) - t1 = np.log10(datb) - t2 = np.log10(data) - - -class Bounds: - def __init__(self, upper, lower, slope): - self.upper = upper - self.lower = lower - self.slope = slope - - def new_u(self, new_upper): - self.upper = new_upper - - def new_l(self, new_lower): - self.lower = new_lower - - def new_s(self, new_slope): - self.slope = new_slope - - -t0b = Bounds(0.8, 2.7, 255) -t1b = Bounds(1.4, 3.0, 255) -t2b = Bounds(1.2, 3.9, 255) - - -def threshold(tval: np.array): - global t0, t1, t2 - if tval.all() == t0.all(): - t0[np.where(t0 < t0b.upper)] = t0b.upper - t0[np.where(t0 > t0b.lower)] = t0b.lower - if tval.all() == t1.all(): - t1[np.where(t1 < t1b.upper)] = t1b.upper - t1[np.where(t1 > t1b.lower)] = t2b.lower - if tval.all() == t2.all(): - t2[np.where(t2 < t2b.upper)] = t2b.upper - t2[np.where(t2 > t2b.lower)] = t2b.lower - - -threshold(t0) -threshold(t1) -threshold(t2) - - -def set_contour(tval: np.array): - global t0, t1, t2 - if tval.all() == t0.all(): - t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) - elif tval.all() == t1.all(): - t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) - elif tval.all() == t2.all(): - t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) - - -set_contour(t0) -set_contour(t1) -set_contour(t2) - - -def create_mask(): - global t0, t1, t2, bmmix, bmhot, bmcool - with np.errstate(divide="ignore", invalid="ignore"): - bmmix[np.where(t2 / t0 >= ((np.mean(data) * 0.6357) / (np.mean(datc))))] = 1 - bmhot[np.where(t0 + t1 < (0.7 * (np.mean(datb) + np.mean(datc))))] = 1 - bmcool[np.where(t2 / t1 >= ((np.mean(data) * 1.5102) / (np.mean(datb))))] = 1 - - -create_mask() - - -def conjunction(): - global bmhot, bmcool, bmmix, cand - cand = bmcool * bmmix * bmhot - - -conjunction() - - -def misid(): - global s, r, w, circ, cand - r = (s[1] / 2.0) - 100 - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[w] = 1.0 - cand = cand * circ - - -misid() - - -def on_off(): - global circ, cand - circ[:] = 0 - r = (rs / dattoarc) - 10 - inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[inside] = 1.0 - r = (rs / dattoarc) + 40 - outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) - circ[outside] = 1.0 - cand = cand * circ - - -on_off() - - -def contours(): - global cand, cont, heir - cand = np.array(cand, dtype=np.uint8) - cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - -contours() - - -def sort(): - global sizes, reord, tmp, cont - sizes = [] - for i in range(len(cont)): - sizes = np.append(sizes, len(cont[i])) - reord = sizes.ravel().argsort()[::-1] - tmp = list(cont) - for i in range(len(cont)): - tmp[i] = cont[reord[i]] - cont = list(tmp) - - -sort() - - -# =====cycles through contours========= - -for i in range(len(cont)): - x = np.append(x, len(cont[i])) - - # =====only takes values of minimum surface length and calculates area====== - - if len(cont[i]) <= 100: - continue - area = 0.5 * np.abs( - np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) - - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) - ) - arcar = area * (dattoarc**2) - if arcar > 1000: - # =====finds centroid======= - - chpts = len(cont[i]) - cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] - - # ===remove quiet sun regions encompassed by coronal holes====== - - if ( - cand[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ) and ( - iarr[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - iarr[np.where(slate == 1)] = 0 - slate[:] = 0 - - else: - # ====create a simple centre point====== - - arccent = csys.all_pix2world(cent[0], cent[1], 0) - - # ====classifies off limb CH regions======== - - if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( - np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) - else: - # =====classifies on disk coronal holes======= - - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - poslin = np.where(slate == 1) - slate[:] = 0 - print(poslin) - - # ====create an array for magnetic polarity======== - - pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) - pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - npix = list( - np.histogram( - datm[pos[:, 0], pos[:, 1]], - bins=np.arange( - np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, - np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, - 1, - ), - ) - ) - npix[0][np.where(npix[0] == 0)] = 1 - npix[1] = npix[1][:-1] + 0.5 - - wh1 = np.where(npix[1] > 0) - wh2 = np.where(npix[1] < 0) - - # =====magnetic cut offs dependant on area========= - - if ( - np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) - <= 10 - and arcar < 9000 - ): - continue - if ( - np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] - and arcar < 40000 - ): - continue - iarr[poslin] = ident - - # ====create an accurate center point======= - - ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) - xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) - - arccent = csys.all_pix2world(xpos, ypos, 0) - - # ======calculate average angle coronal hole is subjected to====== - - dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) - ang = np.arcsin(dist / rs) - - # =====calculate area of CH with minimal projection effects====== - - trupixar = abs(area / np.cos(ang)) - truarcar = trupixar * (dattoarc**2) - trummar = truarcar * ((6.96e08 / rs) ** 2) - - # ====find CH extent in lattitude and longitude======== - - maxxlat = hg.lat[ - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - ] - maxxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - ] - maxylat = hg.lat[ - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - maxylon = hg.lon[ - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - minxlat = hg.lat[ - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - ] - minxlon = hg.lon[ - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - ] - minylat = hg.lat[ - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - minylon = hg.lon[ - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - ] - - # =====CH centroid in lat/lon======= - - centlat = hg.lat[int(ypos), int(xpos)] - centlon = hg.lon[int(ypos), int(xpos)] - - # ====caluclate the mean magnetic field===== - - mB = np.mean(datm[pos[:, 0], pos[:, 1]]) - mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) - mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) - - # =====finds coordinates of CH boundaries======= - - Ywb, Xwb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - 0, - ) - Yeb, Xeb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - 0, - ) - Ynb, Xnb = csys.all_pix2world( - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - Ysb, Xsb = csys.all_pix2world( - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - - width = round(maxxlon.value) - round(minxlon.value) - - if minxlon.value >= 0.0: - eastl = "W" + str(int(np.round(minxlon.value))) - else: - eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) - if maxxlon.value >= 0.0: - westl = "W" + str(int(np.round(maxxlon.value))) - else: - westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) - - if centlat >= 0.0: - centlat = "N" + str(int(np.round(centlat.value))) - else: - centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) - if centlon >= 0.0: - centlon = "W" + str(int(np.round(centlon.value))) - else: - centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) - - # ====insertions of CH properties into property array===== - - props[0, ident + 1] = str(ident) - props[1, ident + 1] = str(np.round(arccent[0])) - props[2, ident + 1] = str(np.round(arccent[1])) - props[3, ident + 1] = str(centlon + centlat) - props[4, ident + 1] = str(np.round(Xeb)) - props[5, ident + 1] = str(np.round(Yeb)) - props[6, ident + 1] = str(np.round(Xwb)) - props[7, ident + 1] = str(np.round(Ywb)) - props[8, ident + 1] = str(np.round(Xnb)) - props[9, ident + 1] = str(np.round(Ynb)) - props[10, ident + 1] = str(np.round(Xsb)) - props[11, ident + 1] = str(np.round(Ysb)) - props[12, ident + 1] = str(eastl + "-" + westl) - props[13, ident + 1] = str(width) - props[14, ident + 1] = f"{trummar/1e+12:.1e}" - props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) - props[16, ident + 1] = str(np.round(mB, 1)) - props[17, ident + 1] = str(np.round(mBpos, 1)) - props[18, ident + 1] = str(np.round(mBneg, 1)) - props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) - props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) - tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) - props[21, ident + 1] = f"{tbpos:.1e}" - tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) - props[22, ident + 1] = f"{tbneg:.1e}" - props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" - props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" - props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" - - # =====sets up code for next possible coronal hole===== - - ident = ident + 1 - -# =====sets ident back to max value of iarr====== - -ident = ident - 1 -np.savetxt("ch_summary.txt", props, fmt="%s") - - -from skimage.util import img_as_ubyte - - -def rescale01(arr, cmin=None, cmax=None, a=0, b=1): - if cmin or cmax: - arr = np.clip(arr, cmin, cmax) - return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a - - -def plot_tricolor(): - tricolorarray = np.zeros((4096, 4096, 3)) - - data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) - data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) - data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) - - tricolorarray[..., 0] = data_c / np.max(data_c) - tricolorarray[..., 1] = data_b / np.max(data_b) - tricolorarray[..., 2] = data_a / np.max(data_a) - - fig, ax = plt.subplots(figsize=(10, 10)) - - plt.imshow(tricolorarray, origin="lower") # , extent = ) - cs = plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) - plt.savefig("tricolor.png") - plt.close() - - -def plot_mask(slate=slate): - chs = np.where(iarr > 0) - slate[chs] = 1 - slate = np.array(slate, dtype=np.uint8) - cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - circ[:] = 0 - r = rs / dattoarc - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - circ[w] = 1.0 - - plt.figure(figsize=(10, 10)) - plt.xlim(143, 4014) - plt.ylim(143, 4014) - plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) - plt.gca().set_aspect("equal", adjustable="box") - plt.axis("off") - cs = plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) - cs = plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) - - plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) - # plt.close() - - -# ====stores all CH properties in a text file===== - -plot_tricolor() -plot_mask() - -# ====EOF==== diff --git a/chimerapy/chimera.py b/chimerapy/chimera.py index d5a80fe..098bb9e 100644 --- a/chimerapy/chimera.py +++ b/chimerapy/chimera.py @@ -209,7 +209,7 @@ def to_helio(image: np.array): hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) global csys csys = wcs.WCS(hedb) - + to_helio(im171) diff --git a/chimerapy/chimera_copy.py b/chimerapy/chimera_copy.py index 68b0966..32ee6d2 100644 --- a/chimerapy/chimera_copy.py +++ b/chimerapy/chimera_copy.py @@ -1,50 +1,34 @@ """Package for Coronal Hole Identification Algorithm""" -import glob -import sys import astropy.units as u import cv2 import mahotas import matplotlib.pyplot as plt import numpy as np -import scipy -from sunpy.map import Map -from sunpy.coordinates import frames -from sunpy.coordinates import propagate_with_solar_surface -import scipy.interpolate import sunpy import sunpy.map from astropy import wcs -from astropy.io import fits from astropy.modeling.models import Gaussian2D from astropy.visualization import astropy_mpl_style from skimage.util import img_as_ubyte -from astropy.wcs import WCS -from sunpy.map import all_coordinates_from_map -from sunpy.coordinates import HeliographicStonyhurst - - -#--noverify - -plt.style.use(astropy_mpl_style) - -"""Defining the paths for example files used to run program locally""" - -file_path = "./" - -INPUT_FILES = {"aia171": 'http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0171.fits', -"aia193": 'http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0193.fits', -"aia211": 'http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0211.fits', -"hmi_mag": 'http://jsoc.stanford.edu/data/hmi/fits/2016/09/22/hmi.M_720s.20160922_010000_TAI.fits', +from sunpy.coordinates import (HeliographicStonyhurst, + propagate_with_solar_surface) +from sunpy.map import Map, all_coordinates_from_map + +INPUT_FILES = { + "aia171": "http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0171.fits", + "aia193": "http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0193.fits", + "aia211": "http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0211.fits", + "hmi_mag": "http://jsoc.stanford.edu/data/hmi/fits/2016/09/22/hmi.M_720s.20160922_010000_TAI.fits", } -im171 = Map(INPUT_FILES['aia171']) -im193 = Map(INPUT_FILES['aia193']) -im211 = Map(INPUT_FILES['aia211']) -imhmi = Map(INPUT_FILES['hmi_mag']) +im171 = Map(INPUT_FILES["aia171"]) +im193 = Map(INPUT_FILES["aia193"]) +im211 = Map(INPUT_FILES["aia211"]) +imhmi = Map(INPUT_FILES["hmi_mag"]) -def rescale(proj_to: sunpy.map.Map, input_map: sunpy.map.Map): +def reproject_diff_rot(target_wcs: wcs.wcs.WCS, input_map: sunpy.map.Map): """ Rescale the input aia image dimensions. @@ -56,26 +40,27 @@ def rescale(proj_to: sunpy.map.Map, input_map: sunpy.map.Map): Returns ------- array: 'np.array' - - """ + + """ with propagate_with_solar_surface(): - map1 = proj_to.reproject_to(input_map.wcs) - new_x_scale = map1.scale[0].to(u.arcsec / u.pixel).value - new_y_scale = map1.scale[1].to(u.arcsec / u.pixel).value - map1.meta['cdelt1'] = new_x_scale - map1.meta['cdelt2'] = new_y_scale - map1.meta['cunit1'] = 'arcsec' - map1.meta['cunit2'] = 'arcsec' - return map1 - -im171 = rescale(im171, im171) -im193 = rescale(im171, im193) -im211 = rescale(im171, im211) -imhmi = rescale(im171, imhmi) + amap = input_map.reproject_to(target_wcs.wcs) + new_x_scale = amap.scale[0].to(u.arcsec / u.pixel).value + new_y_scale = amap.scale[1].to(u.arcsec / u.pixel).value + amap.meta["cdelt1"] = new_x_scale + amap.meta["cdelt2"] = new_y_scale + amap.meta["cunit1"] = "arcsec" + amap.meta["cunit2"] = "arcsec" + return amap + + +im193 = reproject_diff_rot(im171, im193) +im211 = reproject_diff_rot(im171, im211) +imhmi = reproject_diff_rot(im171, imhmi) + def filter(map1: np.array, map2: np.array, map3: np.array): """ - Defines headers and filters aia arrays to meet header requirements. Removes 0 values from each array. + Removes negative values from each map by setting each equal to zero Parameters ---------- @@ -96,59 +81,83 @@ def filter(map1: np.array, map2: np.array, map3: np.array): return map1, map2, map3 + im171, im193, im211 = filter(im171, im193, im211) -"""defines the shape of the arrays as "s" and "rs" as the solar radius""" -s = np.shape(im171.data) -#do we want the solar radius in arcsec or pixels? -rs = im171.rsun_obs -rs_pixels = im171.rsun_obs/im171.scale[0] +def shape(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): + """ + defines the shape of the arrays as "s" and "rs" as the solar radius + + Parameters + ---------- + map1: 'sunpy.map.Map' + map2: 'sunpy.map.Map' + map3: 'sunpy.map.Map' + Returns + ------- + s: 'tuple' + rs: 'astropy.units.quantity.Quantity' + rs_pixels: 'astropy.units.quantity.Quantity' + + """ + + im171, im193, im211 = filter(map1, map2, map3) + # defines the shape of the arrays as "s" and "rs" as the solar radius + s = np.shape(im171.data) + rs = im171.rsun_obs + print(rs) + rs_pixels = im171.rsun_obs / im171.scale[0] + return s, rs, rs_pixels +s, rs, rs_pixels = shape(im171, im193, im211) -def pix_arc(map: sunpy.map.Map): - ''' - Calculates the conversion value of pixel to arcsec + +def pix_arc(amap: sunpy.map.Map): + """ + Defines conversion values between pixels and arcsec Parameters ---------- - map1: 'sunpy.map.Map' - map2: 'sunpy.map.Map' + amap: 'sunpy.map.Map' Returns - ''' - dattoarc = map.scale[0].value - conver = ((s[0]) / 2) * dattoarc / map.meta["cdelt1"] - (s[1] / 2) - convermul = dattoarc / map.meta["cdelt1"] + """ + dattoarc = amap.scale[0].value + s = amap.dimensions + conver = (s.x/2) * amap.scale[0].value / amap.meta['cdelt1'], (s.y / 2) + convermul = dattoarc / amap.meta["cdelt1"] return dattoarc, conver, convermul dattoarc, conver, convermul = pix_arc(im171) +print(conver) -def to_helio(map: sunpy.map.Map): + +def to_helio(amap: sunpy.map.Map): """ - Converts arrays to the Heliographic Stonyhurst coordinate system + Converts maps to the Heliographic Stonyhurst coordinate system Parameters ---------- - map: 'sunpy.map.Map' + amap: 'sunpy.map.Map' Returns ------- - x, y: 'astropy.units.quantity.Quantity' hpc: 'astropy.coordinates.sky_coordinate.SkyCoord' hg: 'astropy.coordinates.sky_coordinate.SkyCoord' csys: 'astropy.wcs.wcs.WCS' """ - hpc = all_coordinates_from_map(map) + hpc = all_coordinates_from_map(amap) hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) - csys = wcs.WCS(dict(map.meta)) + csys = wcs.WCS(dict(amap.meta)) return hpc, hg, csys + hpc, hg, csys = to_helio(im171) """Setting up arrays to be used in later processing""" @@ -165,8 +174,14 @@ def to_helio(map: sunpy.map.Map): center = [int(s[1] / 2.0), int(s[1] / 2.0)] w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) y, x = np.mgrid[0:1024, 0:1024] -pix_size = (2000 * u.arcsec).value -garr = Gaussian2D(1, im171.reference_pixel.x.value, im171.reference_pixel.y.value, pix_size / im171.scale[0].value, pix_size / im171.scale[0].value)(x, y) +width = (2000 * u.arcsec) +garr = Gaussian2D( + 1, + im171.reference_pixel.x.to_value(u.pix), + im171.reference_pixel.y.to_value(u.pix), + width / im171.scale[0], + width / im171.scale[1], +)(x, y) garr[w] = 1.0 """creates sub-arrays of props to isolate column of index 0 and column of index 1""" @@ -229,8 +244,10 @@ def to_helio(map: sunpy.map.Map): ) """define threshold values in log space""" + + def log_dat(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): - ''' + """ Takes the log base-10 of all sunpy map data Parameters @@ -244,13 +261,14 @@ def log_dat(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): t0: 'np.array' t1: 'np.array' t2: 'np.array' - ''' + """ with np.errstate(divide="ignore"): t0 = np.log10(map1.data) t1 = np.log10(map2.data) t2 = np.log10(map3.data) return t0, t1, t2 + t0, t1, t2 = log_dat(im171, im193, im211) @@ -276,7 +294,8 @@ def new_s(self, new_slope): t1b = Bounds(1.4, 3.0, 255) t2b = Bounds(1.2, 3.9, 255) -#set to also take in boundaries + +# set to also take in boundaries def set_contour(t0: np.array, t1: np.array, t2: np.array): """ Threshold arrays based on desired boundaries and sets contours. @@ -295,15 +314,15 @@ def set_contour(t0: np.array, t1: np.array, t2: np.array): """ if t0 is not None and t1 is not None and t2 is not None: - #set the threshold and contours for t0 + # set the threshold and contours for t0 t0[np.where(t0 < t0b.upper)] = t0b.upper t0[np.where(t0 > t0b.lower)] = t0b.lower t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) - #set the threshold and contours for t1 + # set the threshold and contours for t1 t1[np.where(t1 < t1b.upper)] = t1b.upper t1[np.where(t1 > t1b.lower)] = t2b.lower t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) - #set the threshold and contours for t2 + # set the threshold and contours for t2 t2[np.where(t2 < t2b.upper)] = t2b.upper t2[np.where(t2 > t2b.lower)] = t2b.lower t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) @@ -314,7 +333,10 @@ def set_contour(t0: np.array, t1: np.array, t2: np.array): t0, t1, t2 = set_contour(t0, t1, t2) -def create_mask(tm1: np.array, tm2: np.array, tm3: np.array, map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): + +def create_mask( + tm1: np.array, tm2: np.array, tm3: np.array, map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map +): """ Creates 3 segmented bitmasks @@ -326,7 +348,7 @@ def create_mask(tm1: np.array, tm2: np.array, tm3: np.array, map1: sunpy.map.Map map1: 'sunpy.map.Map' map2: 'sunpy.map.Map' map3: 'sunpy.map.Map' - + Returns ------- bmmix: 'np.array' @@ -343,9 +365,10 @@ def create_mask(tm1: np.array, tm2: np.array, tm3: np.array, map1: sunpy.map.Map bmmix, bmhot, bmcool = create_mask(t0, t1, t2, im171, im193, im211) -#conjunction of 3 bitmasks +# conjunction of 3 bitmasks cand = bmcool * bmmix * bmhot + def misid(can: np.array, cir: np.array, xgir: np.array, ygir: np.array, thresh_rad: int): """ Removes off-detector mis-identification @@ -362,7 +385,7 @@ def misid(can: np.array, cir: np.array, xgir: np.array, ygir: np.array, thresh_r 'np.array' """ - #make r a function argument, give name and unit + # make r a function argument, give name and unit r = thresh_rad w = np.where((xgir - center[0]) ** 2 + (ygir - center[1]) ** 2 <= thresh_rad**2) cir[w] = 1.0 @@ -395,17 +418,16 @@ def on_off(cir: np.array, can: np.array): outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) cir[outside] = 1.0 can = can * cir - plt.figure() - plt.imshow(cand, cmap='viridis') - plt.show return can + cand = on_off(circ, cand) + def contour_data(cand: np.array): """ Contours the identified datapoints - + Parameters ---------- cand: 'np.array' @@ -419,15 +441,16 @@ def contour_data(cand: np.array): """ cand = np.array(cand, dtype=np.uint8) cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - #I think cont might be the x-y coordinates in pixels? return cand, cont, heir + cand, cont, heir = contour_data(cand) + def sort(cont: tuple): """ Sorts the contours by size - + Parameters ---------- cont: 'tuple' @@ -457,14 +480,16 @@ def sort(cont: tuple): # =====cycles through contours========= -def extent(map: sunpy.map.Map, cont: tuple): +def extent(amap: sunpy.map.Map, cont: tuple, xpos: int, ypos: int): """ Finds coronal hole extent in latitude and longitude Parameters ---------- - map: 'sunpy.map.Map' + amap: 'sunpy.map.Map' cont: 'tuple' + xpos: 'int' + ypos: 'int' Returns ------- @@ -475,8 +500,8 @@ def extent(map: sunpy.map.Map, cont: tuple): """ - coord_hpc = map.world2pix(cont) - maxlat = coord_hpc.transform_to(HeliographicStonyhurst).lat.max() + coord_hpc = amap.world2pix(cont) + maxlat = coord_hpc.transform_to(HeliographicStonyhurst).lat.max() maxlon = coord_hpc.transform_to(HeliographicStonyhurst).lon.max() minlat = coord_hpc.transform_to(HeliographicStonyhurst).lat.min() minlon = coord_hpc.transform_to(HeliographicStonyhurst).lat.min() @@ -488,10 +513,9 @@ def extent(map: sunpy.map.Map, cont: tuple): return maxlat, maxlon, minlat, minlon, centlat, centlon - def coords(i, csys, cont): """ - Finds coordinates of CH boundaries + Finds coordinates of CH boundaries in world coordinates Parameters ---------- @@ -684,7 +708,7 @@ def ins_prop( slate[:] = 0 else: - """Create a simple centre point if coronal hole regions is not quiet""" + """Create a simple centre point if coronal hole region is not quiet""" arccent = csys.all_pix2world(cent[0], cent[1], 0) @@ -863,7 +887,7 @@ def plot_tricolor(): """ - tricolorarray = np.zeros((4096, 4096, 3)) + tricolorarray = np.zeros((1024, 1024, 3)) data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) @@ -921,4 +945,13 @@ def plot_mask(slate=slate): plot_mask() if __name__ == "__main__": - import_functions(INPUT_FILES['aia171'], INPUT_FILES['aia193'], INPUT_FILES['aia211'], INPUT_FILES['hmi_mag']) \ No newline at end of file + import_functions( + INPUT_FILES["aia171"], INPUT_FILES["aia193"], INPUT_FILES["aia211"], INPUT_FILES["hmi_mag"] + ) + +''' +Document detailing process summary and all functions/variables: + +https://docs.google.com/document/d/1V5LkZq_AAHdTrGsnCl2hoYhm_fvyjfuzODiEHbt4ebo/edit?usp=sharing + +''' diff --git a/chimerapy/tests/test_chimera.py b/chimerapy/tests/test_chimera.py index c3da28b..714c112 100644 --- a/chimerapy/tests/test_chimera.py +++ b/chimerapy/tests/test_chimera.py @@ -1,4 +1,3 @@ -import glob import os import warnings @@ -7,64 +6,24 @@ from astropy.io import fits from astropy.modeling.models import Gaussian2D -from chimerapy.chimera import ( - Bounds, - Xeb, - Xnb, - Xsb, - Xwb, - Yeb, - Ynb, - Ysb, - Ywb, - ang, - arcar, - arccent, - area, - cent, - centlat, - centlon, - chpts, - cont, - coords, - csys, - data, - datb, - datc, - datm, - dist, - eastl, - extent, - filter, - hg, - ins_prop, - mB, - mBneg, - mBpos, - npix, - pos, - remove_neg, - rescale_aia, - rescale_hmi, - set_contour, - sort, - threshold, - truarcar, - trummar, - trupixar, - westl, - width, - xpos, - ypos, -) from chimerapy.chimera import * - -INPUT_FILES = {"aia171": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00171_fd_20160922_103010.fts.gz", -"aia193": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00193_fd_20160922_103041.fts.gz", -"aia211": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00211_fd_20160922_103046.fts.gz", -"hmi_mag": "https://solarmonitor.org/data/2016/09/22/fits/shmi/shmi_maglc_fd_20160922_094640.fts.gz", +from chimerapy.chimera import (Bounds, Xeb, Xnb, Xsb, Xwb, Yeb, Ynb, Ysb, Ywb, + ang, arcar, arccent, area, cent, centlat, + centlon, chpts, cont, coords, csys, data, datb, + datc, datm, dist, eastl, extent, filter, hg, + ins_prop, mB, mBneg, mBpos, npix, pos, + remove_neg, rescale_aia, rescale_hmi, + set_contour, sort, threshold, truarcar, trummar, + trupixar, westl, width, xpos, ypos) + +INPUT_FILES = { + "aia171": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00171_fd_20160922_103010.fts.gz", + "aia193": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00193_fd_20160922_103041.fts.gz", + "aia211": "https://solarmonitor.org/data/2016/09/22/fits/saia/saia_00211_fd_20160922_103046.fts.gz", + "hmi_mag": "https://solarmonitor.org/data/2016/09/22/fits/shmi/shmi_maglc_fd_20160922_094640.fts.gz", } + def img_present(): assert im171 != [] or im193 != [] or im211 != [] or imhmi != [], "Not all required files present" @@ -365,7 +324,7 @@ def test_fill_polygon(): iarr = np.array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) - cont = np.array([[[[1, 2], [1, 3], [2, 3], [2, 2]]]]) + cont = np.array([[[[1, 2], [1, 3], [2, 3], [2, 2]]]]) slate = np.array(iarr) @@ -384,14 +343,12 @@ def test_fill_polygon(): ] > 0 ): - mahotas.polygon.fill_polygon(polygon_vertices, slate) - print("After filling polygon:") print(slate) iarr[np.where(slate == 1)] = 0 - slate[:] = 0 + slate[:] = 0 assert np.array_equal( slate, @@ -421,8 +378,8 @@ def test_magpol(): assert npix[0][np.where(npix[0] == 0)] != 0, "Npix[0] should not be equal to zero at its zeros" assert npix[1] != 0, "Npix[1] should not be equal to 0" npixtest = [ - np.array([2, -1, 0, 3, -2, 1]), - np.array([1, -1, 0, 2, -2, 1]), + np.array([2, -1, 0, 3, -2, 1]), + np.array([1, -1, 0, 2, -2, 1]), ] wh1_expected = np.where(npixtest[1] > 0) wh1_actual = np.where(npixtest[1] > 0) From 1c12edbeff5b6170c1a7353509b934b9ae3173d3 Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:58:39 -0400 Subject: [PATCH 09/10] Updated pull request with a few changes --- chimerapy/chimera_copy.py | 192 +++++++++++++++++++------------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/chimerapy/chimera_copy.py b/chimerapy/chimera_copy.py index 32ee6d2..7232d68 100644 --- a/chimerapy/chimera_copy.py +++ b/chimerapy/chimera_copy.py @@ -9,17 +9,19 @@ import sunpy.map from astropy import wcs from astropy.modeling.models import Gaussian2D -from astropy.visualization import astropy_mpl_style +from astropy.wcs import WCS from skimage.util import img_as_ubyte from sunpy.coordinates import (HeliographicStonyhurst, propagate_with_solar_surface) from sunpy.map import Map, all_coordinates_from_map +from sunpy.coordinates import frames + INPUT_FILES = { - "aia171": "http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0171.fits", - "aia193": "http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0193.fits", - "aia211": "http://jsoc.stanford.edu/data/aia/synoptic/2016/09/22/H1000/AIA20160922_1030_0211.fits", - "hmi_mag": "http://jsoc.stanford.edu/data/hmi/fits/2016/09/22/hmi.M_720s.20160922_010000_TAI.fits", + "aia171": "http://jsoc.stanford.edu/data/aia/synoptic/2024/01/31/H1000/AIA20240131_1000_0171.fits", + "aia193": "http://jsoc.stanford.edu/data/aia/synoptic/2024/01/31/H1000/AIA20240131_1000_0193.fits", + "aia211": "http://jsoc.stanford.edu/data/aia/synoptic/2024/01/31/H1000/AIA20240131_1000_0211.fits", + "hmi_mag": "http://jsoc.stanford.edu/data/hmi/fits/2024/01/31/hmi.M_720s.20240131_010000_TAI.fits", } im171 = Map(INPUT_FILES["aia171"]) @@ -27,6 +29,10 @@ im211 = Map(INPUT_FILES["aia211"]) imhmi = Map(INPUT_FILES["hmi_mag"]) +im171.plot +im193.plot +im211.plot +imhmi.plot def reproject_diff_rot(target_wcs: wcs.wcs.WCS, input_map: sunpy.map.Map): """ @@ -42,22 +48,22 @@ def reproject_diff_rot(target_wcs: wcs.wcs.WCS, input_map: sunpy.map.Map): array: 'np.array' """ - with propagate_with_solar_surface(): - amap = input_map.reproject_to(target_wcs.wcs) - new_x_scale = amap.scale[0].to(u.arcsec / u.pixel).value - new_y_scale = amap.scale[1].to(u.arcsec / u.pixel).value - amap.meta["cdelt1"] = new_x_scale - amap.meta["cdelt2"] = new_y_scale - amap.meta["cunit1"] = "arcsec" - amap.meta["cunit2"] = "arcsec" - return amap + with frames.Helioprojective.assume_spherical_screen(target_wcs.observer_coordinate): + with propagate_with_solar_surface(): + amap = input_map.reproject_to(target_wcs.wcs) + new_x_scale = amap.scale[0].to(u.arcsec / u.pixel).value + new_y_scale = amap.scale[1].to(u.arcsec / u.pixel).value + amap.meta["cdelt1"] = new_x_scale + amap.meta["cdelt2"] = new_y_scale + amap.meta["cunit1"] = "arcsec" + amap.meta["cunit2"] = "arcsec" + return amap im193 = reproject_diff_rot(im171, im193) im211 = reproject_diff_rot(im171, im211) imhmi = reproject_diff_rot(im171, imhmi) - def filter(map1: np.array, map2: np.array, map3: np.array): """ Removes negative values from each map by setting each equal to zero @@ -84,10 +90,13 @@ def filter(map1: np.array, map2: np.array, map3: np.array): im171, im193, im211 = filter(im171, im193, im211) + + + def shape(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): """ defines the shape of the arrays as "s" and "rs" as the solar radius - + Parameters ---------- map1: 'sunpy.map.Map' @@ -99,9 +108,9 @@ def shape(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): s: 'tuple' rs: 'astropy.units.quantity.Quantity' rs_pixels: 'astropy.units.quantity.Quantity' - + """ - + im171, im193, im211 = filter(map1, map2, map3) # defines the shape of the arrays as "s" and "rs" as the solar radius s = np.shape(im171.data) @@ -110,6 +119,7 @@ def shape(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): rs_pixels = im171.rsun_obs / im171.scale[0] return s, rs, rs_pixels + s, rs, rs_pixels = shape(im171, im193, im211) @@ -126,7 +136,7 @@ def pix_arc(amap: sunpy.map.Map): """ dattoarc = amap.scale[0].value s = amap.dimensions - conver = (s.x/2) * amap.scale[0].value / amap.meta['cdelt1'], (s.y / 2) + conver = (s.x / 2) * amap.scale[0].value / amap.meta["cdelt1"], (s.y / 2) convermul = dattoarc / amap.meta["cdelt1"] return dattoarc, conver, convermul @@ -154,7 +164,9 @@ def to_helio(amap: sunpy.map.Map): """ hpc = all_coordinates_from_map(amap) hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) - csys = wcs.WCS(dict(amap.meta)) + # Filter the header to contain only ASCII characters and exclude specified keys + filtered_header = {key: value for key, value in amap.meta.items() if key not in ['keycomments', 'comment']} + csys = wcs.WCS(dict(filtered_header)) return hpc, hg, csys @@ -174,13 +186,13 @@ def to_helio(amap: sunpy.map.Map): center = [int(s[1] / 2.0), int(s[1] / 2.0)] w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) y, x = np.mgrid[0:1024, 0:1024] -width = (2000 * u.arcsec) +width = 2000 * u.arcsec garr = Gaussian2D( 1, im171.reference_pixel.x.to_value(u.pix), im171.reference_pixel.y.to_value(u.pix), - width / im171.scale[0], - width / im171.scale[1], + width.value / im171.scale[0].value, + width.value / im171.scale[1].value, )(x, y) garr[w] = 1.0 @@ -393,7 +405,7 @@ def misid(can: np.array, cir: np.array, xgir: np.array, ygir: np.array, thresh_r return r, w, cir, cand -r, w, cir, cand = misid(cand, circ, xgrid, ygrid, (s[1] / 2.0) - 100) +r, w, cir_off, cand_off = misid(cand, circ, xgrid, ygrid, (s[1] / 2.0) - 100) def on_off(cir: np.array, can: np.array): @@ -421,7 +433,7 @@ def on_off(cir: np.array, can: np.array): return can -cand = on_off(circ, cand) +#cand = on_off(circ, cand) def contour_data(cand: np.array): @@ -463,7 +475,7 @@ def sort(cont: tuple): sizes: 'list' """ - sizes = [] + sizes = np.array([]) for i in range(len(cont)): sizes = np.append(sizes, len(cont[i])) reord = sizes.ravel().argsort()[::-1] @@ -534,33 +546,31 @@ def coords(i, csys, cont): Ysb: 'np.array' Xsb: 'np.array' """ - global Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb - Ywb, Xwb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - np.max(cont[i][:, 0, 0]), - 0, - ) - Yeb, Xeb = csys.all_pix2world( - cont[i][np.where(cont[i][:, 0, 0] == np.min(cont[i][:, 0, 0]))[0][0], 0, 1], - np.min(cont[i][:, 0, 0]), - 0, - ) - Ynb, Xnb = csys.all_pix2world( - np.max(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.max(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - Ysb, Xsb = csys.all_pix2world( - np.min(cont[i][:, 0, 1]), - cont[i][np.where(cont[i][:, 0, 1] == np.min(cont[i][:, 0, 1]))[0][0], 0, 0], - 0, - ) - + + #exctracting coordinates for contours + contour = cont[i] + x_coords = contour[:, 0, 0] + y_coords = contour[:, 0, 1] + + #finding max and min of x coordinates + max_x = np.argmax(x_coords) + min_x = np.argmin(x_coords) + + #finding manx and min of y coordinates + max_y = np.argmax(y_coords) + min_y = np.argmin(y_coords) + + #Pixel coordinates to world coordinates (in arcsec) + Ywb, Xwb = (csys.pixel_to_world(x_coords[max_x], y_coords[max_x])).Tx, (csys.pixel_to_world(x_coords[max_x], y_coords[max_x])).Ty + Yeb, Xeb = (csys.pixel_to_world(x_coords[min_x], y_coords[min_x])).Tx, (csys.pixel_to_world(x_coords[min_x], y_coords[min_x])).Ty + Ynb, Xnb = (csys.pixel_to_world(y_coords[max_y], x_coords[max_y])).Tx, (csys.pixel_to_world(y_coords[max_y], x_coords[max_y])).Ty + Ysb, Xsb = (csys.pixel_to_world(y_coords[min_y], x_coords[min_y])).Tx, (csys.pixel_to_world(y_coords[min_y], x_coords[min_y])).Ty + return Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb def ins_prop( - datm, + imhmi, rs, ident, props, @@ -591,7 +601,7 @@ def ins_prop( Parameters ---------- - datm: 'np.array' + imhmi: 'np.array' rs: 'float' ident: 'int' props: 'np.array' @@ -659,9 +669,9 @@ def ins_prop( props[18, ident + 1] = str(np.round(mBneg, 1)) props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) - tbpos = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] > 0)]) + tbpos = np.sum(imhmi.data[pos[:, 0], pos[:, 1]][np.where(imhmi.data[pos[:, 0], pos[:, 1]] > 0)]) props[21, ident + 1] = f"{tbpos:.1e}" - tbneg = np.sum(datm[pos[:, 0], pos[:, 1]][np.where(datm[pos[:, 0], pos[:, 1]] < 0)]) + tbneg = np.sum(imhmi.data[pos[:, 0], pos[:, 1]][np.where(imhmi.data[pos[:, 0], pos[:, 1]] < 0)]) props[22, ident + 1] = f"{tbneg:.1e}" props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" @@ -672,7 +682,16 @@ def ins_prop( for i in range(len(cont)): x = np.append(x, len(cont[i])) - + #exctracting coordinates for contours + contour = cont[i] + lengths = [] + # Iterate through each element in cont and calculate its length + for elem in cont: + lengths.append(len(elem)) + x_coords = contour[:, 0, 0] + y_coords = contour[:, 0, 1] + max_x = np.max(x_coords) + max_y = np.max(y_coords) """only takes values of minimum surface length and calculates area""" if len(cont[i]) <= 100: @@ -686,24 +705,11 @@ def ins_prop( """finds centroid""" chpts = len(cont[i]) - cent = [np.mean(cont[i][:, 0, 0]), np.mean(cont[i][:, 0, 1])] + cent = [np.mean(x_coords), np.mean(y_coords)] """remove quiet sun regions encompassed by coronal holes""" - if ( - cand[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ) and ( - iarr[ - np.max(cont[i][:, 0, 0]) + 1, - cont[i][np.where(cont[i][:, 0, 0] == np.max(cont[i][:, 0, 0]))[0][0], 0, 1], - ] - > 0 - ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) - print(slate) + if (cand[max_x + 1, contour[np.where(x_coords == max_x)[0][0], 0, 1]] > 0) and (iarr[max_x + 1, cont[i][np.where(x_coords == max_x)[0][0], 0, 1]] > 0): + mahotas.polygon.fill_polygon(np.array(list(zip(y_coords, x_coords))), slate) iarr[np.where(slate == 1)] = 0 slate[:] = 0 @@ -714,29 +720,27 @@ def ins_prop( """classifies off limb CH regions""" - if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs**2)) or ( - np.sum(np.array(csys.all_pix2world(cont[i][0, 0, 0], cont[i][0, 0, 1], 0)) ** 2) > (rs**2) + if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs.value**2)) or ( + np.sum(np.array(csys.all_pix2world(x_coords, y_coords, 0)) ** 2) > (rs.value**2) ): - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), offarr) + mahotas.polygon.fill_polygon(np.array(list(zip(y_coords, x_coords))), offarr) else: """classifies on disk coronal holes""" - mahotas.polygon.fill_polygon(np.array(list(zip(cont[i][:, 0, 1], cont[i][:, 0, 0]))), slate) + mahotas.polygon.fill_polygon(np.array(list(zip(y_coords, x_coords))), slate) poslin = np.where(slate == 1) - slate[:] = 0 print(poslin) - + slate[:] = 0 """create an array for magnetic polarity""" - - pos = np.zeros((len(poslin[0]), 2), dtype=np.uint) - pos[:, 0] = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - pos[:, 1] = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos_x = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos_y = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) + pos = np.column_stack((pos_x, pos_y)) npix = list( np.histogram( - datm[pos[:, 0], pos[:, 1]], + imhmi.data[pos_x, pos_y], bins=np.arange( - np.round(np.min(datm[pos[:, 0], pos[:, 1]])) - 0.5, - np.round(np.max(datm[pos[:, 0], pos[:, 1]])) + 0.6, + np.round(np.min(imhmi.data[pos_x, pos_y])) - 0.5, + np.round(np.max(imhmi.data[pos_x, pos_y])) + 0.6, 1, ), ) @@ -756,7 +760,7 @@ def ins_prop( ): continue if ( - np.absolute(np.mean(datm[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] + np.absolute(np.mean(imhmi.data[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] and arcar < 40000 ): continue @@ -786,7 +790,7 @@ def ins_prop( """caluclate the mean magnetic field""" - mB = np.mean(datm[pos[:, 0], pos[:, 1]]) + mB = np.mean(imhmi.data[pos[:, 0], pos[:, 1]]) mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) @@ -817,7 +821,7 @@ def ins_prop( """insertions of CH properties into property array""" ins_prop( - datm, + imhmi, rs, ident, props, @@ -889,9 +893,9 @@ def plot_tricolor(): tricolorarray = np.zeros((1024, 1024, 3)) - data_a = img_as_ubyte(rescale01(np.log10(data), cmin=1.2, cmax=3.9)) - data_b = img_as_ubyte(rescale01(np.log10(datb), cmin=1.4, cmax=3.0)) - data_c = img_as_ubyte(rescale01(np.log10(datc), cmin=0.8, cmax=2.7)) + data_a = img_as_ubyte(rescale01(np.log10(im171.data), cmin=1.2, cmax=3.9)) + data_b = img_as_ubyte(rescale01(np.log10(im193.data), cmin=1.4, cmax=3.0)) + data_c = img_as_ubyte(rescale01(np.log10(imhmi.data), cmin=0.8, cmax=2.7)) tricolorarray[..., 0] = data_c / np.max(data_c) tricolorarray[..., 1] = data_b / np.max(data_b) @@ -926,7 +930,7 @@ def plot_mask(slate=slate): circ[:] = 0 r = rs / dattoarc - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) + w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r.value**2) circ[w] = 1.0 plt.figure(figsize=(10, 10)) @@ -938,20 +942,16 @@ def plot_mask(slate=slate): plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) - plt.savefig("CH_mask_" + hedb["DATE"] + ".png", transparent=True) + plt.savefig("CH_mask_" + im193.meta["date-obs"] + ".png", transparent=True) plot_tricolor() plot_mask() -if __name__ == "__main__": - import_functions( - INPUT_FILES["aia171"], INPUT_FILES["aia193"], INPUT_FILES["aia211"], INPUT_FILES["hmi_mag"] - ) -''' +""" Document detailing process summary and all functions/variables: https://docs.google.com/document/d/1V5LkZq_AAHdTrGsnCl2hoYhm_fvyjfuzODiEHbt4ebo/edit?usp=sharing -''' +""" From 5c87bc50beecd02c26cc31c5c694263d3156598c Mon Sep 17 00:00:00 2001 From: imogenagle <157685743+imogenagle@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:51:29 -0400 Subject: [PATCH 10/10] new clip/scale function --- chimerapy/chimera_copy.py | 1053 +++++-------------------------------- 1 file changed, 128 insertions(+), 925 deletions(-) diff --git a/chimerapy/chimera_copy.py b/chimerapy/chimera_copy.py index 7232d68..1e4bc71 100644 --- a/chimerapy/chimera_copy.py +++ b/chimerapy/chimera_copy.py @@ -1,957 +1,160 @@ -"""Package for Coronal Hole Identification Algorithm""" +import copy + -import astropy.units as u -import cv2 -import mahotas -import matplotlib.pyplot as plt import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import LogNorm import sunpy import sunpy.map -from astropy import wcs -from astropy.modeling.models import Gaussian2D -from astropy.wcs import WCS -from skimage.util import img_as_ubyte -from sunpy.coordinates import (HeliographicStonyhurst, - propagate_with_solar_surface) -from sunpy.map import Map, all_coordinates_from_map -from sunpy.coordinates import frames - - -INPUT_FILES = { - "aia171": "http://jsoc.stanford.edu/data/aia/synoptic/2024/01/31/H1000/AIA20240131_1000_0171.fits", - "aia193": "http://jsoc.stanford.edu/data/aia/synoptic/2024/01/31/H1000/AIA20240131_1000_0193.fits", - "aia211": "http://jsoc.stanford.edu/data/aia/synoptic/2024/01/31/H1000/AIA20240131_1000_0211.fits", - "hmi_mag": "http://jsoc.stanford.edu/data/hmi/fits/2024/01/31/hmi.M_720s.20240131_010000_TAI.fits", -} - -im171 = Map(INPUT_FILES["aia171"]) -im193 = Map(INPUT_FILES["aia193"]) -im211 = Map(INPUT_FILES["aia211"]) -imhmi = Map(INPUT_FILES["hmi_mag"]) - -im171.plot -im193.plot -im211.plot -imhmi.plot - -def reproject_diff_rot(target_wcs: wcs.wcs.WCS, input_map: sunpy.map.Map): - """ - Rescale the input aia image dimensions. - - Parameters - ---------- - proj_to: 'sunpy.map.Map' - input_map: 'sunpy.map.Map - - Returns - ------- - array: 'np.array' - - """ - with frames.Helioprojective.assume_spherical_screen(target_wcs.observer_coordinate): - with propagate_with_solar_surface(): - amap = input_map.reproject_to(target_wcs.wcs) - new_x_scale = amap.scale[0].to(u.arcsec / u.pixel).value - new_y_scale = amap.scale[1].to(u.arcsec / u.pixel).value - amap.meta["cdelt1"] = new_x_scale - amap.meta["cdelt2"] = new_y_scale - amap.meta["cunit1"] = "arcsec" - amap.meta["cunit2"] = "arcsec" - return amap - - -im193 = reproject_diff_rot(im171, im193) -im211 = reproject_diff_rot(im171, im211) -imhmi = reproject_diff_rot(im171, imhmi) - -def filter(map1: np.array, map2: np.array, map3: np.array): - """ - Removes negative values from each map by setting each equal to zero - - Parameters - ---------- - map1: 'sunpy.map.Map' - map2: 'sunpy.map.Map' - map3: 'sunpy.map.Map' - - Returns - ------- - map1: 'sunpy.map.Map' - map2: 'sunpy.map.Map' - map3: 'sunpy.map.Map' - - """ - map1.data[np.where(map1.data <= 0)] = 0 - map2.data[np.where(map2.data <= 0)] = 0 - map3.data[np.where(map3.data <= 0)] = 0 - - return map1, map2, map3 - - -im171, im193, im211 = filter(im171, im193, im211) - - - - -def shape(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): - """ - defines the shape of the arrays as "s" and "rs" as the solar radius - - Parameters - ---------- - map1: 'sunpy.map.Map' - map2: 'sunpy.map.Map' - map3: 'sunpy.map.Map' - - Returns - ------- - s: 'tuple' - rs: 'astropy.units.quantity.Quantity' - rs_pixels: 'astropy.units.quantity.Quantity' - - """ - - im171, im193, im211 = filter(map1, map2, map3) - # defines the shape of the arrays as "s" and "rs" as the solar radius - s = np.shape(im171.data) - rs = im171.rsun_obs - print(rs) - rs_pixels = im171.rsun_obs / im171.scale[0] - return s, rs, rs_pixels - - -s, rs, rs_pixels = shape(im171, im193, im211) - - -def pix_arc(amap: sunpy.map.Map): - """ - Defines conversion values between pixels and arcsec - - Parameters - ---------- - amap: 'sunpy.map.Map' - - Returns - - """ - dattoarc = amap.scale[0].value - s = amap.dimensions - conver = (s.x / 2) * amap.scale[0].value / amap.meta["cdelt1"], (s.y / 2) - convermul = dattoarc / amap.meta["cdelt1"] - return dattoarc, conver, convermul - - -dattoarc, conver, convermul = pix_arc(im171) - -print(conver) - - -def to_helio(amap: sunpy.map.Map): - """ - Converts maps to the Heliographic Stonyhurst coordinate system - - Parameters - ---------- - amap: 'sunpy.map.Map' - - Returns - ------- - hpc: 'astropy.coordinates.sky_coordinate.SkyCoord' - hg: 'astropy.coordinates.sky_coordinate.SkyCoord' - csys: 'astropy.wcs.wcs.WCS' - - - """ - hpc = all_coordinates_from_map(amap) - hg = hpc.transform_to(sunpy.coordinates.frames.HeliographicStonyhurst) - # Filter the header to contain only ASCII characters and exclude specified keys - filtered_header = {key: value for key, value in amap.meta.items() if key not in ['keycomments', 'comment']} - csys = wcs.WCS(dict(filtered_header)) - return hpc, hg, csys - - -hpc, hg, csys = to_helio(im171) - -"""Setting up arrays to be used in later processing""" -ident = 1 -iarr = np.zeros((s[0], s[1]), dtype=np.byte) -bmcool = np.zeros((s[0], s[1]), dtype=np.float32) -offarr, slate = np.array(iarr), np.array(iarr) -cand, bmmix, bmhot = np.array(bmcool), np.array(bmcool), np.array(bmcool) -circ = np.zeros((s[0], s[1]), dtype=int) - -"""creation of a 2d gaussian for magnetic cut offs""" -r = (s[1] / 2.0) - 450 -xgrid, ygrid = np.meshgrid(np.arange(s[0]), np.arange(s[1])) -center = [int(s[1] / 2.0), int(s[1] / 2.0)] -w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 > r**2) -y, x = np.mgrid[0:1024, 0:1024] -width = 2000 * u.arcsec -garr = Gaussian2D( - 1, - im171.reference_pixel.x.to_value(u.pix), - im171.reference_pixel.y.to_value(u.pix), - width.value / im171.scale[0].value, - width.value / im171.scale[1].value, -)(x, y) -garr[w] = 1.0 - -"""creates sub-arrays of props to isolate column of index 0 and column of index 1""" -props = np.zeros((26, 30), dtype="", - "", - "", - "BMAX", - "BMIN", - "TOT_B+", - "TOT_B-", - "", - "", - "", -) -props[:, 1] = ( - "num", - '"', - '"', - "H°", - '"', - '"', - '"', - '"', - '"', - '"', - '"', - '"', - "H°", - "°", - "Mm^2", - "%", - "G", - "G", - "G", - "G", - "G", - "G", - "G", - "Mx", - "Mx", - "Mx", -) - -"""define threshold values in log space""" - - -def log_dat(map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map): - """ - Takes the log base-10 of all sunpy map data - - Parameters - ---------- - map1: 'sunpy.map.Map' - map2: 'sunpy.map.Map' - map3: 'sunpy.map.Map' - - Returns - ------- - t0: 'np.array' - t1: 'np.array' - t2: 'np.array' - """ - with np.errstate(divide="ignore"): - t0 = np.log10(map1.data) - t1 = np.log10(map2.data) - t2 = np.log10(map3.data) - return t0, t1, t2 - - -t0, t1, t2 = log_dat(im171, im193, im211) - - -class Bounds: - """Class to change and define array boundaries and slopes""" - - def __init__(self, upper, lower, slope): - self.upper = upper - self.lower = lower - self.slope = slope - - def new_u(self, new_upper): - self.upper = new_upper - - def new_l(self, new_lower): - self.lower = new_lower - - def new_s(self, new_slope): - self.slope = new_slope - - -t0b = Bounds(0.8, 2.7, 255) -t1b = Bounds(1.4, 3.0, 255) -t2b = Bounds(1.2, 3.9, 255) - - -# set to also take in boundaries -def set_contour(t0: np.array, t1: np.array, t2: np.array): - """ - Threshold arrays based on desired boundaries and sets contours. - - Parameters - ---------- - t0: 'np.array' - t1: 'np.array' - t2: 'np.array'' - - Returns - ------- - t0: 'np.array' - t1: 'np.array' - t2: 'np.array' - - """ - if t0 is not None and t1 is not None and t2 is not None: - # set the threshold and contours for t0 - t0[np.where(t0 < t0b.upper)] = t0b.upper - t0[np.where(t0 > t0b.lower)] = t0b.lower - t0 = np.array(((t0 - t0b.upper) / (t0b.lower - t0b.upper)) * t0b.slope, dtype=np.float32) - # set the threshold and contours for t1 - t1[np.where(t1 < t1b.upper)] = t1b.upper - t1[np.where(t1 > t1b.lower)] = t2b.lower - t1 = np.array(((t1 - t1b.upper) / (t1b.lower - t1b.upper)) * t1b.slope, dtype=np.float32) - # set the threshold and contours for t2 - t2[np.where(t2 < t2b.upper)] = t2b.upper - t2[np.where(t2 > t2b.lower)] = t2b.lower - t2 = np.array(((t2 - t2b.upper) / (t2b.lower - t2b.upper)) * t2b.slope, dtype=np.float32) - else: - print("Must input valid logarithmic arrays") - return t0, t1, t2 - - -t0, t1, t2 = set_contour(t0, t1, t2) - - -def create_mask( - tm1: np.array, tm2: np.array, tm3: np.array, map1: sunpy.map.Map, map2: sunpy.map.Map, map3: sunpy.map.Map -): - """ - Creates 3 segmented bitmasks - - Parameters - ------- - tm1: 'np.array' - tm2: 'np.array' - tm3: 'np.array' - map1: 'sunpy.map.Map' - map2: 'sunpy.map.Map' - map3: 'sunpy.map.Map' - - Returns - ------- - bmmix: 'np.array' - bmhot: 'np.array' - bmcool: 'np.array' - - """ - with np.errstate(divide="ignore", invalid="ignore"): - bmmix[np.where(tm3 / tm1 >= ((np.mean(map1.data) * 0.6357) / (np.mean(map3.data))))] = 1 - bmhot[np.where(tm1 + tm2 < (0.7 * (np.mean(map2.data) + np.mean(map3.data))))] = 1 - bmcool[np.where(tm3 / tm2 >= ((np.mean(map2.data) * 1.5102) / (np.mean(map2.data))))] = 1 - return bmmix, bmhot, bmcool - - -bmmix, bmhot, bmcool = create_mask(t0, t1, t2, im171, im193, im211) - -# conjunction of 3 bitmasks -cand = bmcool * bmmix * bmhot - - -def misid(can: np.array, cir: np.array, xgir: np.array, ygir: np.array, thresh_rad: int): - """ - Removes off-detector mis-identification - - Parameters - ---------- - can: 'np.array' - cir: 'np.array' - xgir: 'np.array' - ygir: 'np.array' - - Returns - ------- - 'np.array' - - """ - # make r a function argument, give name and unit - r = thresh_rad - w = np.where((xgir - center[0]) ** 2 + (ygir - center[1]) ** 2 <= thresh_rad**2) - cir[w] = 1.0 - cand = can * cir - return r, w, cir, cand - - -r, w, cir_off, cand_off = misid(cand, circ, xgrid, ygrid, (s[1] / 2.0) - 100) - - -def on_off(cir: np.array, can: np.array): - """ - Seperates on-disk and off-limb coronal holes - - Parameters - ---------- - cir: 'np.array' - can: 'np.array' - - Returns - ------- - 'np.array' - - """ - cir[:] = 0 - r = (rs.value / dattoarc) - 10 - inside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r**2) - cir[inside] = 1.0 - r = (rs.value / dattoarc) + 40 - outside = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 >= r**2) - cir[outside] = 1.0 - can = can * cir - return can - - -#cand = on_off(circ, cand) - - -def contour_data(cand: np.array): - """ - Contours the identified datapoints - - Parameters - ---------- - cand: 'np.array' - - Returns - ------- - cand: 'np.array' - cont: 'tuple' - heir: 'np.array' - - """ - cand = np.array(cand, dtype=np.uint8) - cont, heir = cv2.findContours(cand, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - return cand, cont, heir - - -cand, cont, heir = contour_data(cand) - - -def sort(cont: tuple): - """ - Sorts the contours by size - - Parameters - ---------- - cont: 'tuple' - - Returns - ------- - reord: 'list' - tmp: 'list' - cont: 'list' - sizes: 'list' - - """ - sizes = np.array([]) - for i in range(len(cont)): - sizes = np.append(sizes, len(cont[i])) - reord = sizes.ravel().argsort()[::-1] - tmp = list(cont) - for i in range(len(cont)): - tmp[i] = cont[reord[i]] - cont = list(tmp) - return cont, sizes, reord, tmp - - -cont, sizes, reord, tmp = sort(cont) - - -# =====cycles through contours========= - - -def extent(amap: sunpy.map.Map, cont: tuple, xpos: int, ypos: int): - """ - Finds coronal hole extent in latitude and longitude - - Parameters - ---------- - amap: 'sunpy.map.Map' - cont: 'tuple' - xpos: 'int' - ypos: 'int' - - Returns - ------- - maxxlon: 'astropy.coordinates.angles.core.Longitude' - minxlon: 'astropy.coordinates.angles.core.Longitude' - centlat: 'astropy.coordinates.angles.core.Latitude' - centlon: 'astropy.coordinates.angles.core.Longitude' - - """ +import astropy.units as u +from astropy.coordinates import SkyCoord +from astropy.units import UnitsError +from astropy.visualization import make_lupton_rgb +from sunpy.map import Map, all_coordinates_from_map, pixelate_coord_path, sample_at_coords + +m171 = Map("https://jsoc1.stanford.edu/data/aia/synoptic/2016/10/31/H0200/AIA20161031_0232_0171.fits") +m193 = Map("https://jsoc1.stanford.edu/data/aia/synoptic/2016/10/31/H0200/AIA20161031_0232_0193.fits") +m211 = Map("https://jsoc1.stanford.edu/data/aia/synoptic/2016/10/31/H0200/AIA20161031_0232_0211.fits") + +def segmenting_plots(scale_cold: float, scale_warm: float, scale_hot: float): + m171 = Map("https://jsoc1.stanford.edu/data/aia/synoptic/2016/10/31/H0200/AIA20161031_0232_0171.fits") + m193 = Map("https://jsoc1.stanford.edu/data/aia/synoptic/2016/10/31/H0200/AIA20161031_0232_0193.fits") + m211 = Map("https://jsoc1.stanford.edu/data/aia/synoptic/2016/10/31/H0200/AIA20161031_0232_0211.fits") + + m171_orig = copy.deepcopy(m171) + m193_orig = copy.deepcopy(m193) + m211_orig = copy.deepcopy(m211) - coord_hpc = amap.world2pix(cont) - maxlat = coord_hpc.transform_to(HeliographicStonyhurst).lat.max() - maxlon = coord_hpc.transform_to(HeliographicStonyhurst).lon.max() - minlat = coord_hpc.transform_to(HeliographicStonyhurst).lat.min() - minlon = coord_hpc.transform_to(HeliographicStonyhurst).lat.min() - - # =====CH centroid in lat/lon======= + # Since the data are taken at similar times neglect any coordinate changes so just use 171 maps coordinates + coords = all_coordinates_from_map(m171) + disk_mask = (coords.Tx**2 + coords.Ty**2) ** 0.5 < m171.rsun_obs - centlat = hg.lat[int(ypos), int(xpos)] - centlon = hg.lon[int(ypos), int(xpos)] - return maxlat, maxlon, minlat, minlon, centlat, centlon + m171 = m171 / m171.exposure_time + m193 = m193 / m193.exposure_time + m211 = m211 / m211.exposure_time + + xx = np.linspace(0, 300, 500) + fig, axes = plt.subplot_mosaic( + [["cool_hist"], ["warm_hist"], ["hot_hist"]], + layout="constrained", + figsize=(3, 5), + ) -def coords(i, csys, cont): - """ - Finds coordinates of CH boundaries in world coordinates + # 171 v 193 + cool_counts, *cool_bins = axes["cool_hist"].hist2d( + m171.data[disk_mask].flatten(), + m193.data[disk_mask].flatten(), + bins=150, + range=[[0, 300], [0, 300]], + norm=LogNorm(), + density=True, + ) + #Finding the indices of nonzero counts + non_zero_cool = np.where(cool_counts > 0) - Parameters - ---------- - i: 'int' - csys: 'astropy.wcs.wcs.WCS' - cont: 'list' + #Finding the corresponding minimum y-index + min_y_index = np.min(non_zero_cool[1]) - Returns - ------- - Ywb: 'np.array' - Xwb: 'np.array' - Yeb: 'np.array' - Xeb: 'np.array' - Ynb: 'np.array' - Xnb: 'np.array' - Ysb: 'np.array' - Xsb: 'np.array' - """ + #Map the index to the actual y-value + min_y_cool= cool_bins[1][min_y_index] + axes["cool_hist"].set_facecolor("k") + axes["cool_hist"].plot(xx, (xx**scale_cold) + min_y_cool, "w") - #exctracting coordinates for contours - contour = cont[i] - x_coords = contour[:, 0, 0] - y_coords = contour[:, 0, 1] - - #finding max and min of x coordinates - max_x = np.argmax(x_coords) - min_x = np.argmin(x_coords) - - #finding manx and min of y coordinates - max_y = np.argmax(y_coords) - min_y = np.argmin(y_coords) - - #Pixel coordinates to world coordinates (in arcsec) - Ywb, Xwb = (csys.pixel_to_world(x_coords[max_x], y_coords[max_x])).Tx, (csys.pixel_to_world(x_coords[max_x], y_coords[max_x])).Ty - Yeb, Xeb = (csys.pixel_to_world(x_coords[min_x], y_coords[min_x])).Tx, (csys.pixel_to_world(x_coords[min_x], y_coords[min_x])).Ty - Ynb, Xnb = (csys.pixel_to_world(y_coords[max_y], x_coords[max_y])).Tx, (csys.pixel_to_world(y_coords[max_y], x_coords[max_y])).Ty - Ysb, Xsb = (csys.pixel_to_world(y_coords[min_y], x_coords[min_y])).Tx, (csys.pixel_to_world(y_coords[min_y], x_coords[min_y])).Ty - - return Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb - - -def ins_prop( - imhmi, - rs, - ident, - props, - arcar, - arccent, - pos, - npix, - trummar, - centlat, - centlon, - mB, - mBpos, - mBneg, - Ywb, - Xwb, - Yeb, - Xeb, - Ynb, - Xnb, - Ysb, - Xsb, - width, - eastl, - westl, -): - """ - Insertion of CH properties into property array - - Parameters - ---------- - imhmi: 'np.array' - rs: 'float' - ident: 'int' - props: 'np.array' - arcar: 'np.float64' - arccent: 'list' - pos: 'np.array' - npix: 'list' - trummar: 'np.float64' - centlat: 'str' - centlon: 'str' - mB: 'np.float64' - mBpos: 'np.float64' - mBneg: 'np.float64' - - Returns - ------- - props[0, ident + 1]: 'str' - props[1, ident + 1]: 'str' - props[2, ident + 1]: 'str' - props[3, ident + 1]: 'str' - props[4, ident + 1]: 'str' - props[5, ident + 1]: 'str' - props[6, ident + 1]: 'str' - props[7, ident + 1]: 'str' - props[8, ident + 1]: 'str' - props[9, ident + 1]: 'str' - props[10, ident + 1]: 'str' - props[11, ident + 1]: 'str' - props[12, ident + 1]: 'str' - props[13, ident + 1]: 'str' - props[14, ident + 1]: 'str' - props[15, ident + 1]: 'str' - props[16, ident + 1]: 'str' - props[17, ident + 1]: 'str' - props[18, ident + 1]: 'str' - props[19, ident + 1]: 'str' - props[20, ident + 1]: 'str' - tbpos: 'np.float64' - props[21, ident + 1]: 'str' - tbneg: 'np.float64' - props[22, ident + 1]: 'str' - props[23, ident + 1]: 'str' - props[24, ident + 1]: 'str' - props[25, ident + 1]: 'str' - - """ - props[0, ident + 1] = str(ident) - props[1, ident + 1] = str(np.round(arccent[0])) - props[2, ident + 1] = str(np.round(arccent[1])) - props[3, ident + 1] = str(centlon + centlat) - props[4, ident + 1] = str(np.round(Xeb)) - props[5, ident + 1] = str(np.round(Yeb)) - props[6, ident + 1] = str(np.round(Xwb)) - props[7, ident + 1] = str(np.round(Ywb)) - props[8, ident + 1] = str(np.round(Xnb)) - props[9, ident + 1] = str(np.round(Ynb)) - props[10, ident + 1] = str(np.round(Xsb)) - props[11, ident + 1] = str(np.round(Ysb)) - props[12, ident + 1] = str(eastl + "-" + westl) - props[13, ident + 1] = str(width) - props[14, ident + 1] = f"{trummar/1e+12:.1e}" - props[15, ident + 1] = str(np.round((arcar * 100 / (np.pi * (rs**2))), 1)) - props[16, ident + 1] = str(np.round(mB, 1)) - props[17, ident + 1] = str(np.round(mBpos, 1)) - props[18, ident + 1] = str(np.round(mBneg, 1)) - props[19, ident + 1] = str(np.round(np.max(npix[1]), 1)) - props[20, ident + 1] = str(np.round(np.min(npix[1]), 1)) - tbpos = np.sum(imhmi.data[pos[:, 0], pos[:, 1]][np.where(imhmi.data[pos[:, 0], pos[:, 1]] > 0)]) - props[21, ident + 1] = f"{tbpos:.1e}" - tbneg = np.sum(imhmi.data[pos[:, 0], pos[:, 1]][np.where(imhmi.data[pos[:, 0], pos[:, 1]] < 0)]) - props[22, ident + 1] = f"{tbneg:.1e}" - props[23, ident + 1] = f"{mB*trummar*1e+16:.1e}" - props[24, ident + 1] = f"{mBpos*trummar*1e+16:.1e}" - props[25, ident + 1] = f"{mBneg*trummar*1e+16:.1e}" - - -"""Cycles through contours""" - -for i in range(len(cont)): - x = np.append(x, len(cont[i])) - #exctracting coordinates for contours - contour = cont[i] - lengths = [] - # Iterate through each element in cont and calculate its length - for elem in cont: - lengths.append(len(elem)) - x_coords = contour[:, 0, 0] - y_coords = contour[:, 0, 1] - max_x = np.max(x_coords) - max_y = np.max(y_coords) - """only takes values of minimum surface length and calculates area""" - - if len(cont[i]) <= 100: - continue - area = 0.5 * np.abs( - np.dot(cont[i][:, 0, 0], np.roll(cont[i][:, 0, 1], 1)) - - np.dot(cont[i][:, 0, 1], np.roll(cont[i][:, 0, 0], 1)) + # 171 v 211 + warm_counts, *warm_bins = axes["warm_hist"].hist2d( + m171.data[disk_mask].flatten(), + m211.data[disk_mask].flatten(), + bins=250, + range=[[0, 300], [0, 300]], + norm=LogNorm(), + density=True, ) - arcar = area * (dattoarc**2) - if arcar > 1000: - """finds centroid""" - - chpts = len(cont[i]) - cent = [np.mean(x_coords), np.mean(y_coords)] - - """remove quiet sun regions encompassed by coronal holes""" - if (cand[max_x + 1, contour[np.where(x_coords == max_x)[0][0], 0, 1]] > 0) and (iarr[max_x + 1, cont[i][np.where(x_coords == max_x)[0][0], 0, 1]] > 0): - mahotas.polygon.fill_polygon(np.array(list(zip(y_coords, x_coords))), slate) - iarr[np.where(slate == 1)] = 0 - slate[:] = 0 - - else: - """Create a simple centre point if coronal hole region is not quiet""" - - arccent = csys.all_pix2world(cent[0], cent[1], 0) - - """classifies off limb CH regions""" - - if (((arccent[0] ** 2) + (arccent[1] ** 2)) > (rs.value**2)) or ( - np.sum(np.array(csys.all_pix2world(x_coords, y_coords, 0)) ** 2) > (rs.value**2) - ): - mahotas.polygon.fill_polygon(np.array(list(zip(y_coords, x_coords))), offarr) - else: - """classifies on disk coronal holes""" - - mahotas.polygon.fill_polygon(np.array(list(zip(y_coords, x_coords))), slate) - poslin = np.where(slate == 1) - print(poslin) - slate[:] = 0 - """create an array for magnetic polarity""" - pos_x = np.array((poslin[0] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - pos_y = np.array((poslin[1] - (s[0] / 2)) * convermul + (s[1] / 2), dtype=np.uint) - pos = np.column_stack((pos_x, pos_y)) - npix = list( - np.histogram( - imhmi.data[pos_x, pos_y], - bins=np.arange( - np.round(np.min(imhmi.data[pos_x, pos_y])) - 0.5, - np.round(np.max(imhmi.data[pos_x, pos_y])) + 0.6, - 1, - ), - ) - ) - npix[0][np.where(npix[0] == 0)] = 1 - npix[1] = npix[1][:-1] + 0.5 - - wh1 = np.where(npix[1] > 0) - wh2 = np.where(npix[1] < 0) - - """Filters magnetic cutoff values by area""" - - if ( - np.absolute((np.sum(npix[0][wh1]) - np.sum(npix[0][wh2])) / np.sqrt(np.sum(npix[0]))) - <= 10 - and arcar < 9000 - ): - continue - if ( - np.absolute(np.mean(imhmi.data[pos[:, 0], pos[:, 1]])) < garr[int(cent[0]), int(cent[1])] - and arcar < 40000 - ): - continue - iarr[poslin] = ident - - """create an accurate center point""" - - ypos = np.sum((poslin[0]) * np.absolute(hg.lat[poslin])) / np.sum(np.absolute(hg.lat[poslin])) - xpos = np.sum((poslin[1]) * np.absolute(hg.lon[poslin])) / np.sum(np.absolute(hg.lon[poslin])) - - arccent = csys.all_pix2world(xpos, ypos, 0) - - """calculate average angle coronal hole is subjected to""" - - dist = np.sqrt((arccent[0] ** 2) + (arccent[1] ** 2)) - ang = np.arcsin(dist / rs) - - """calculate area of CH with minimal projection effects""" - - trupixar = abs(area / np.cos(ang)) - truarcar = trupixar * (dattoarc**2) - trummar = truarcar * ((6.96e08 / rs) ** 2) - - """find CH extent in lattitude and longitude""" - - maxxlon, minxlon, centlat, centlon = extent(i, ypos, xpos, hg, cont) - - """caluclate the mean magnetic field""" - - mB = np.mean(imhmi.data[pos[:, 0], pos[:, 1]]) - mBpos = np.sum(npix[0][wh1] * npix[1][wh1]) / np.sum(npix[0][wh1]) - mBneg = np.sum(npix[0][wh2] * npix[1][wh2]) / np.sum(npix[0][wh2]) - - """finds coordinates of CH boundaries""" - - Ywb, Xwb, Yeb, Xeb, Ynb, Xnb, Ysb, Xsb = coords(i, csys, cont) - - width = round(maxxlon.value) - round(minxlon.value) - - if minxlon.value >= 0.0: - eastl = "W" + str(int(np.round(minxlon.value))) - else: - eastl = "E" + str(np.absolute(int(np.round(minxlon.value)))) - if maxxlon.value >= 0.0: - westl = "W" + str(int(np.round(maxxlon.value))) - else: - westl = "E" + str(np.absolute(int(np.round(maxxlon.value)))) - - if centlat >= 0.0: - centlat = "N" + str(int(np.round(centlat.value))) - else: - centlat = "S" + str(np.absolute(int(np.round(centlat.value)))) - if centlon >= 0.0: - centlon = "W" + str(int(np.round(centlon.value))) - else: - centlon = "E" + str(np.absolute(int(np.round(centlon.value)))) - - """insertions of CH properties into property array""" - - ins_prop( - imhmi, - rs, - ident, - props, - arcar, - arccent, - pos, - npix, - trummar, - centlat, - centlon, - mB, - mBpos, - mBneg, - Ywb, - Xwb, - Yeb, - Xeb, - Ynb, - Xnb, - Ysb, - Xsb, - width, - eastl, - westl, - ) - """sets up code for next possible coronal hole""" - - ident = ident + 1 - -"""sets ident back to max value of iarr""" - -ident = ident - 1 - -"""stores all CH properties in a text file""" -np.savetxt("ch_summary.txt", props, fmt="%s") - - -def rescale01(arr, cmin=None, cmax=None, a=0, b=1): - """ - Rescales array - - Parameters - ---------- - arr: 'np.arr' - cmin: 'np.float' - cmax: 'np.float' - a: 'int' - b: 'int' - - Returns - ------- - np.array + #Finding the indices of nonzero counts + non_zero_warm = np.where(warm_counts > 0) + + #Finding the corresponding minimum y-index + min_y_index = np.min(non_zero_warm[1]) + + #Map the index to the actual y-value + min_y_warm= warm_bins[1][min_y_index] + axes["warm_hist"].set_ylim(0, 100) + axes["warm_hist"].set_facecolor("k") + axes["warm_hist"].plot(xx, (xx**scale_warm) + min_y_warm, "w") + + # 193 v 311 + hot_counts, *hot_bins = axes["hot_hist"].hist2d( + m193.data[disk_mask].flatten(), + m211.data[disk_mask].flatten(), + bins=250, + range=[[0, 300], [0, 300]], + norm=LogNorm(), + density=True, + ) + #Finding the indices of nonzero counts + non_zero_hot = np.where(hot_counts > 0) - """ - if cmin or cmax: - arr = np.clip(arr, cmin, cmax) - return (b - a) * ((arr - np.min(arr)) / (np.max(arr) - np.min(arr))) + a + #Finding the corresponding minimum y-index + min_y_index = np.min(non_zero_hot[1]) + #Map the index to the actual y-value + min_y_hot= hot_bins[1][min_y_index] -def plot_tricolor(): - """ - Plots a tricolor mask of image data + axes["hot_hist"].set_ylim(0, 100) + axes["hot_hist"].set_facecolor("k") + axes["hot_hist"].plot(xx, (xx**scale_hot) + min_y_hot, "w") - Returns - ------- - plot: 'matplotlib.image.AxesImage' + plt.show() - """ - - tricolorarray = np.zeros((1024, 1024, 3)) - - data_a = img_as_ubyte(rescale01(np.log10(im171.data), cmin=1.2, cmax=3.9)) - data_b = img_as_ubyte(rescale01(np.log10(im193.data), cmin=1.4, cmax=3.0)) - data_c = img_as_ubyte(rescale01(np.log10(imhmi.data), cmin=0.8, cmax=2.7)) - - tricolorarray[..., 0] = data_c / np.max(data_c) - tricolorarray[..., 1] = data_b / np.max(data_b) - tricolorarray[..., 2] = data_a / np.max(data_a) - - fig, ax = plt.subplots(figsize=(10, 10)) - - plt.imshow(tricolorarray, origin="lower") - plt.contour(xgrid, ygrid, slate, colors="white", linewidths=0.5) - plt.savefig("tricolor.png") - plt.close() +segmenting_plots(.7, .6, .7) +def clip_scale(map1: sunpy.map, clip1: float, clip2: float, clip3: float, scale: float): + map_clipped = np.clip(np.log10(map1.data), clip1, clip3) + map_clipped_scaled = ((map_clipped - clip1) / clip2) * scale + return map_clipped_scaled -def plot_mask(slate=slate): - """ - Plots the contour mask +cs_171 = clip_scale(m171, 1.2, 2.7, 3.9, 255) +cs_193 = clip_scale(m193, 1.4, 1.6, 3.0, 255) +cs_211 = clip_scale(m211, .8, 1.9, 2.7, 255) - Parameters - ---------- - slate: 'np.array' - Returns - ------- - plot: 'matplotlib.image.AxesImage' +def create_mask(scale1: float, scale2: float, scale3: float): + mask1 = (cs_171 / cs_211) >= (np.mean(m171.data) * scale1) / np.mean(m211.data) + mask2 = (cs_211 + cs_193) < (scale2 * (np.mean(m193.data) + np.mean(m211.data))) + mask3 = (cs_171 / cs_193) >= ((np.mean(m171.data) * scale3) / np.mean(m193.data)) + return mask1, mask2, mask3 - """ +mask211_171, mask211_193, mask171_193 = create_mask(.6357, .7, 1.5102) - chs = np.where(iarr > 0) - slate[chs] = 1 - slate = np.array(slate, dtype=np.uint8) - cont, heir = cv2.findContours(slate, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) +tri_color_img = make_lupton_rgb(cs_171, cs_193, cs_211, Q=10, stretch=50) +comb_mask = mask211_171 * mask211_193 * mask171_193 - circ[:] = 0 - r = rs / dattoarc - w = np.where((xgrid - center[0]) ** 2 + (ygrid - center[1]) ** 2 <= r.value**2) - circ[w] = 1.0 - plt.figure(figsize=(10, 10)) - plt.xlim(143, 4014) - plt.ylim(143, 4014) - plt.scatter(chs[1], chs[0], marker="s", s=0.0205, c="black", cmap="viridis", edgecolor="none", alpha=0.2) - plt.gca().set_aspect("equal", adjustable="box") - plt.axis("off") - plt.contour(xgrid, ygrid, slate, colors="black", linewidths=0.5) - plt.contour(xgrid, ygrid, circ, colors="black", linewidths=1.0) +fig, axes = plt.subplot_mosaic([["tri", "comb_mask"]], layout="constrained", figsize=(6, 3)) - plt.savefig("CH_mask_" + im193.meta["date-obs"] + ".png", transparent=True) +axes["tri"].imshow(tri_color_img, origin="lower") +axes["comb_mask"].imshow(comb_mask, origin="lower") +plt.show() -plot_tricolor() -plot_mask() +def create_contours(mask1: np.ndarray, mask2: np.ndarray, mask3: np.ndarray): + mask_map = Map(((mask1 * mask2 * mask3).astype(int), m171.meta)) + try: + contours = mask_map.contour(0.5 / u.s) + except UnitsError: + contours = mask_map.contour(50 * u.percent) + contours = sorted(contours, key=lambda x: x.size, reverse=True) -""" -Document detailing process summary and all functions/variables: + fig, axes = plt.subplot_mosaic( + [["seg"]], layout="constrained", figsize=(6, 3), subplot_kw={"projection": m171} + ) + m171.plot(axes=axes["seg"]) + axes["seg"].imshow(tri_color_img) -https://docs.google.com/document/d/1V5LkZq_AAHdTrGsnCl2hoYhm_fvyjfuzODiEHbt4ebo/edit?usp=sharing + # For the moment just plot to top 5 contours based on "size" for contour + for contour in contours[:6]: + axes["seg"].plot_coord(contour, color="w", linewidth=0.5) + plt.show() -""" +create_contours(mask211_171, mask211_193, mask171_193) \ No newline at end of file