├── ecopv ├── __init__.py ├── data │ ├── paper_colors.csv │ ├── Macbeth_XYZ_from_R.txt │ ├── ColorChecker_R_BabelColor.csv │ ├── CIE_std_illum_D65.csv │ ├── CIE_std_illum_D50.csv │ └── Macbeth_ColorChecker_R.csv ├── plot_utilities.py ├── spectrum_functions.py └── optimization_functions.py ├── README.md ├── examples ├── black_cell_optim.py ├── db_numerical_comparison.py ├── DBR_example.py ├── compare_integration.py ├── plot_colorchecker_gamut.py ├── run_cell_color_optim.py ├── run_cell_color_optim_loop_spd_compareJ0.py ├── intro_figure.py ├── plot_incident_spectra.py ├── gamut_luminance_colors.py ├── plot_pareto_front.py ├── run_cell_color_optim_loop_bb.py ├── plot_pareto_front_animation.py ├── run_cell_color_optim_loop_spd.py ├── gamut_luminance_spd.py └── gamut_luminance_fixedEg.py └── .gitignore /ecopv/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coloured PV 2 | 3 | Package requirements: 4 | - numpy 5 | - scipy 6 | - pygmo2 7 | - solcore 8 | - colour-science 9 | - matplotlib 10 | - colormath 11 | - xarray 12 | - seaborn 13 | 14 | Due to the [pygmo2](https://esa.github.io/pygmo2/install.html) requirement, Anaconda Python is recommended 15 | so it can be installed simply as `conda install pygmo`. -------------------------------------------------------------------------------- /ecopv/data/paper_colors.csv: -------------------------------------------------------------------------------- 1 | index,Colour,x,y,Y,eta,Voc,Jsc,FF,Eg 2 | 0,darkskin,0.4336,0.3787,0.1029,32.74,1.0794,34.12,88.94,1.337 3 | 1,lightskin,0.4187,0.3749,0.354,30.6,0.8899,39.46,87.18,1.135 4 | 2,bluesky,0.2757,0.2996,0.1849,31.81,0.8909,40.97,87.19,1.135 5 | 3,foliage,0.3688,0.4501,0.1329,32.63,0.892,41.96,87.2,1.136 6 | 4,blueflower,0.3016,0.2871,0.2328,31.28,0.8904,40.3,87.19,1.135 7 | 5,bluishgreen,0.2856,0.3905,0.4175,30.2,0.8891,38.98,87.17,1.135 8 | 6,orange,0.5291,0.4081,0.3106,30.95,0.8902,39.89,87.18,1.135 9 | 7,purplishblue,0.2335,0.2157,0.1136,32.18,0.8912,41.42,87.2,1.135 10 | 8,moderatered,0.5002,0.3295,0.1989,31.6,0.8907,40.71,87.19,1.135 11 | 9,purple,0.3316,0.2544,0.0639,32.92,1.0796,34.3,88.94,1.337 12 | 10,yellowgreen,0.3986,0.5002,0.444,30.54,0.889,39.38,87.18,1.135 13 | 11,orangeyellow,0.496,0.4426,0.436,30.15,0.8844,39.14,87.12,1.13 14 | 12,blue,0.2042,0.1676,0.057,32.71,1.0794,34.08,88.94,1.337 15 | 13,green,0.3262,0.504,0.2297,32.03,0.891,41.25,87.19,1.135 16 | 14,red,0.5734,0.3284,0.1262,32.2,0.8912,41.45,87.2,1.135 17 | 15,yellow,0.4693,0.473,0.6072,28.91,0.8834,37.58,87.11,1.13 18 | 16,magenta,0.4175,0.2702,0.1998,31.36,0.8905,40.41,87.19,1.135 19 | 17,cyan,0.2146,0.3028,0.1889,31.73,0.8908,40.87,87.19,1.135 20 | 18,white9.5,0.3486,0.3625,0.9094,24.09,0.8668,31.98,86.92,1.117 21 | 19,neutral8,0.3451,0.3593,0.585,28.44,0.8816,37.06,87.09,1.128 22 | 20,neutral6.5,0.3447,0.3588,0.3571,30.55,0.8809,39.4,87.18,1.135 23 | 21,neutral5,0.3433,0.3586,0.1912,31.98,0.891,41.18,87.19,1.135 24 | 22,neutral3.5,0.3425,0.3577,0.0887,32.87,1.0795,34.24,88.94,1.337 25 | 23,black2,0.3436,0.3562,0.0317,33.45,1.09,34.83,88.94,1.337 -------------------------------------------------------------------------------- /ecopv/data/Macbeth_XYZ_from_R.txt: -------------------------------------------------------------------------------- 1 | 1.164748390557839436e-01 1.024982246737323327e-01 5.828954236237425535e-02 2 | 3.890728843876249399e-01 3.508343477139626576e-01 2.181199190065905569e-01 3 | 1.730102375897495104e-01 1.861533334949187768e-01 2.941770088102621528e-01 4 | 1.086057795074152066e-01 1.326947076788630719e-01 5.982178069653009550e-02 5 | 2.467826163348497692e-01 2.336399838241325633e-01 3.744352612555759685e-01 6 | 3.072377228564256990e-01 4.198697009744628561e-01 3.883039863348786702e-01 7 | 3.961438692147240537e-01 3.071754018457988344e-01 5.456004623681590809e-02 8 | 1.287439781300764585e-01 1.160339592374753925e-01 3.297154351364084968e-01 9 | 2.951629230603333154e-01 1.952420498112141523e-01 1.153359550755259783e-01 10 | 8.413459508168896761e-02 6.416106745596440730e-02 1.175208715729872538e-01 11 | 3.486687104825836880e-01 4.411322342463760982e-01 1.000033760597544702e-01 12 | 4.784174238655030109e-01 4.299286093098949046e-01 6.718845794488581546e-02 13 | 7.390010999319350515e-02 5.959303266281568839e-02 2.421433659426643348e-01 14 | 1.488588132903133354e-01 2.310872133113834603e-01 8.689786578604617728e-02 15 | 2.130943399090616253e-01 1.242293832015953503e-01 4.299651334740872688e-02 16 | 5.909640116757689521e-01 6.017722322685660208e-01 8.193303545868334259e-02 17 | 3.070288587142407377e-01 1.991525852614976777e-01 2.622498539571501142e-01 18 | 1.395982922033962570e-01 1.940833508939157348e-01 3.398845092396400869e-01 19 | 8.766667151492290033e-01 9.126326876616479566e-01 8.179385357166680803e-01 20 | 5.648355028049598658e-01 5.884951905516391246e-01 5.451727692056675956e-01 21 | 3.451637919404363375e-01 3.594844472398375790e-01 3.348057322471617026e-01 22 | 1.832283306023577141e-01 1.912051019649676775e-01 1.787777645606897781e-01 23 | 8.561330134831263150e-02 8.942390955901871608e-02 8.454025089310351904e-02 24 | 3.094963491464037750e-02 3.200668636074693874e-02 3.027654022704581380e-02 25 | -------------------------------------------------------------------------------- /examples/black_cell_optim.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ecopv.main_optimization import cell_optimization 3 | 4 | import pygmo as pg 5 | from solcore.light_source import LightSource 6 | from time import time 7 | 8 | 9 | interval = 0.01 10 | wl_cell = np.arange(300, 4000, interval) 11 | 12 | light_source = LightSource( 13 | source_type="standard", 14 | version="AM1.5g", 15 | x=wl_cell, 16 | output_units="photon_flux_per_nm", 17 | ) 18 | 19 | photon_flux = np.array(light_source.spectrum(wl_cell)) 20 | 21 | close_guesses = [ 22 | # [1.337], 23 | # [0.96159811, 1.63271755], 24 | # [0.93351, 1.36649659, 1.89924796], 25 | [0.71523332, 1.11437635, 1.49345409, 2.00180808], 26 | [0.70973005, 1.0107807, 1.33084337, 1.66754078, 2.14036662], 27 | [0.75716868, 1.02538943, 1.31171125, 1.5692605, 1.87432925, 2.3132805], 28 | ] 29 | 30 | n_junctions = [1, 2, 3, 4, 5, 6] 31 | n_junctions = [4, 5, 6] 32 | 33 | for i1, n_juncs in enumerate(n_junctions): 34 | start = time() 35 | print(n_juncs, "Junctions") 36 | 37 | p_init = cell_optimization( 38 | n_juncs, photon_flux, power_in=light_source.power_density, eta_ext=1 39 | ) 40 | 41 | def get_bounds(): 42 | Eg_bounds_lower = [0.9*Eg for Eg in close_guesses[i1]] 43 | Eg_bounds_upper = [1.1*Eg for Eg in close_guesses[i1]] 44 | 45 | return [Eg_bounds_lower, Eg_bounds_upper] 46 | 47 | p_init.get_bounds = get_bounds 48 | 49 | prob = pg.problem(p_init) 50 | algo = pg.algorithm( 51 | pg.de( 52 | # gen=1000*n_juncs, 53 | gen=500, 54 | F=1, 55 | CR=1, 56 | xtol=0, 57 | ftol=0, 58 | ) 59 | ) 60 | 61 | pop = pg.population(prob, 50 * n_juncs) 62 | pop = algo.evolve(pop) 63 | print("Optimized efficiency: ", -pop.champion_f[0] * 100) 64 | print("Optimized bandgaps: ", np.sort(pop.champion_x)) 65 | print("Time taken (s): ", time() - start) 66 | 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .DS_Store 132 | plots/* 133 | color_data/* 134 | .Rproj.user 135 | 136 | .obsidian -------------------------------------------------------------------------------- /examples/db_numerical_comparison.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from ecopv.optimization_functions import db_cell_calculation_noR, getPmax, getIVmax 4 | from solcore.solar_cell import SolarCell, Junction 5 | from solcore.solar_cell_solver import solar_cell_solver 6 | from solcore.light_source import LightSource 7 | from solcore.constants import kb, q 8 | 9 | k = kb / q 10 | T = 300 11 | kbT = k * T 12 | 13 | from solcore import si 14 | 15 | interval = 0.1 16 | wavelengths = si(np.arange(300, 4000, interval), "nm") 17 | wl_cell = np.arange( 18 | 300, 4000, interval 19 | ) 20 | 21 | black_cell_Eg = [ 22 | [1.34], 23 | [0.96, 1.63], 24 | [0.93, 1.37, 1.90], 25 | [0.72, 1.11, 1.49, 2.00], 26 | [0.70, 1.01, 1.33, 1.67, 2.14], 27 | [0.70, 0.96, 1.20, 1.47, 1.79, 2.24], 28 | ] 29 | 30 | opts = {'light_iv': True, 'wavelength': wavelengths, 'mpp': True, 'T': 300} 31 | 32 | light_source = LightSource( 33 | source_type="standard", 34 | version= "AM1.5g", 35 | x=wl_cell, 36 | output_units="photon_flux_per_nm", 37 | ) 38 | 39 | photon_flux_cell = np.array(light_source.spectrum(wl_cell)) 40 | 41 | for Egs in black_cell_Eg: 42 | 43 | V = np.arange(0, np.sum(Egs), 0.001) 44 | opts['voltages'] = V 45 | 46 | db_junctions = [Junction(kind='DB', Eg=Eg, A=1, R_shunt=1e30, n=1) for 47 | Eg in 48 | Egs[::-1]] 49 | solar_cell_db_A1 = SolarCell(db_junctions) 50 | 51 | solar_cell_solver(solar_cell_db_A1, 'iv', user_options=opts) 52 | 53 | analytic_eta = getPmax(Egs[::-1], 54 | photon_flux_cell[1], 55 | photon_flux_cell[0], 56 | interval, 57 | None, 58 | 1, 59 | 4.43, 60 | "no_R", 61 | 2)/10 62 | 63 | j01s, jscs, Vmaxs, Imaxs = db_cell_calculation_noR(Egs[::-1], 64 | photon_flux_cell[1], 65 | photon_flux_cell[0], 66 | interval 67 | ) 68 | 69 | minImax = np.amin(Imaxs) 70 | vsubcell = kbT * np.log((jscs - minImax) / j01s) 71 | 72 | vTandem = np.sum(vsubcell) 73 | 74 | numerical_eta = solar_cell_db_A1.iv["Eta"]*100 75 | print(analytic_eta, numerical_eta) 76 | print(100*(analytic_eta - numerical_eta)/analytic_eta) 77 | print("V", vTandem, solar_cell_db_A1.iv["Vmpp"]) 78 | print("I", minImax/10, solar_cell_db_A1.iv["Impp"]/10) 79 | 80 | plt.figure() 81 | plt.plot(solar_cell_db_A1.iv["IV"][0], solar_cell_db_A1.iv["IV"][1]/10) 82 | plt.xlim(0, 1.1*solar_cell_db_A1.iv["Voc"]) 83 | plt.ylim(0, 1.1*np.max(solar_cell_db_A1.iv["IV"][1]/10)) 84 | plt.axhline(y=minImax/10) 85 | plt.axvline(x=vTandem) 86 | plt.plot(solar_cell_db_A1.iv["Vmpp"], solar_cell_db_A1.iv["Impp"]/10, 'o') 87 | plt.plot() 88 | plt.show() 89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/DBR_example.py: -------------------------------------------------------------------------------- 1 | from rayflare.transfer_matrix_method import tmm_structure 2 | from solcore import material, si 3 | import matplotlib.pyplot as plt 4 | from solcore.structure import Layer 5 | import numpy as np 6 | from rayflare.options import default_options 7 | from solcore.light_source import LightSource 8 | from ecopv.spectrum_functions import spec_to_XYZ, load_cmf 9 | from ecopv.optimization_functions import getPmax 10 | from ecopv.main_optimization import plot_outcome 11 | from solcore.constants import h, c 12 | 13 | interval = 0.01 14 | wavelengths = np.arange(300, 1200, interval) * 1e-9 15 | opts = default_options() 16 | 17 | opts.wavelengths = wavelengths 18 | opts.pol = "s" 19 | 20 | cmf = load_cmf(wavelengths * 1e9) 21 | 22 | # Use AM1.5G spectrum: 23 | light_source = LightSource( 24 | source_type="black body", 25 | x=wavelengths * 1e9, 26 | output_units="photon_flux_per_nm", 27 | entendue="Sun", 28 | T=5778, 29 | ) 30 | 31 | photon_flux_cell = np.array(light_source.spectrum(wavelengths * 1e9)) 32 | 33 | photon_flux_color = photon_flux_cell[ 34 | :, np.all((photon_flux_cell[0] >= 380, photon_flux_cell[0] <= 780), axis=0) 35 | ] 36 | 37 | Egs = np.linspace(1, 1.8, 100) 38 | plt.figure() 39 | for rad_eff in [0.01, 0.1, 1]: 40 | eff = np.zeros_like(Egs) 41 | 42 | for i1, eg in enumerate(Egs): 43 | 44 | eff[i1] = getPmax( 45 | [eg], photon_flux_cell[1], wavelengths * 1e9, interval, rad_eff 46 | ) 47 | 48 | argm = np.argmax(eff) 49 | print(Egs[argm]) 50 | 51 | plt.plot(Egs, eff, label=str(rad_eff)) 52 | plt.legend() 53 | plt.show() 54 | 55 | # define the materials 56 | SiN = material("Si3N4")() 57 | TiO2 = material("TiO2b")() 58 | Si = material("Si")() 59 | Ag = material("Ag")() 60 | Air = material("Air")() 61 | 62 | plt.figure() 63 | plt.plot(wavelengths * 1e9, SiN.n(wavelengths), label="SiN") 64 | plt.plot(wavelengths * 1e9, TiO2.n(wavelengths), label="TiO2") 65 | plt.legend() 66 | plt.show() 67 | 68 | target_wavelength = 575 * 1e-9 69 | target_wavelength_2 = 450 * 1e-9 70 | n_DBR_reps = 2 71 | 72 | ARC_layer = Layer(width=si("75nm"), material=SiN) 73 | 74 | cell_layer = Layer(width=si("300um"), material=Si) 75 | 76 | DBR_layers = [ 77 | Layer(3 * target_wavelength / (4 * TiO2.n(target_wavelength)), material=TiO2), 78 | Layer(3 * target_wavelength / (4 * SiN.n(target_wavelength)), material=SiN), 79 | ] * n_DBR_reps 80 | 81 | DBR_layers_2 = [ 82 | Layer(target_wavelength_2 / (4 * TiO2.n(target_wavelength_2)), material=TiO2), 83 | Layer(target_wavelength_2 / (4 * SiN.n(target_wavelength_2)), material=SiN), 84 | ] * n_DBR_reps 85 | 86 | 87 | struct = tmm_structure( 88 | [ARC_layer] + DBR_layers_2 + DBR_layers, incidence=Air, transmission=Si 89 | ) 90 | 91 | RAT = struct.calculate(opts) 92 | 93 | plt.figure() 94 | plt.plot(wavelengths * 1e9, RAT["R"], label="R") 95 | plt.show() 96 | 97 | XYZ = spec_to_XYZ(RAT["R"], h * c * photon_flux_cell[1] / wavelengths, cmf, interval) 98 | 99 | plot_outcome(RAT["R"], photon_flux_cell, XYZ, "test") 100 | plt.show() 101 | -------------------------------------------------------------------------------- /examples/compare_integration.py: -------------------------------------------------------------------------------- 1 | # compare integration methods: just calculation area of rectangles vs. trapezoid rule 2 | 3 | import numpy as np 4 | from scipy.special import lambertw 5 | from solcore.constants import kb, q, h, c 6 | from solcore.light_source import LightSource 7 | from time import time 8 | 9 | 10 | def indefinite_integral_J0(eg1, eg2): 11 | # eg1 is lower Eg, eg2 is higher Eg 12 | p1 = -(eg1 ** 2 + 2 * eg1 * (kbT) + 2 * (kbT) ** 2)* np.exp(-(eg1) / (kbT)) 13 | p2 = -(eg2 ** 2 + 2 * eg2 * (kbT) + 2 * (kbT) ** 2)* np.exp(-(eg2) / (kbT)) 14 | 15 | return (pref * kbT)*(p2 - p1)/rad_eff 16 | 17 | interval = 0.1 18 | wl = np.arange(1240/4.43, 4000, interval) 19 | 20 | k = kb / q 21 | h_eV = h / q 22 | e = np.exp(1) 23 | T = 298 24 | kbT = k * T 25 | pref = ((2 * np.pi * q) / (h_eV**3 * c**2)) 26 | 27 | pref_wl = 1e27*2*np.pi*q*c 28 | wl_exp_const = 1e9*h*c/(kb*T) 29 | 30 | egs = [2, 1.5, 1.1] 31 | rad_eff = 1 32 | 33 | photon_flux_cell = np.array( 34 | LightSource( 35 | source_type="standard", 36 | version="AM1.5g", 37 | x=wl, 38 | output_units="photon_flux_per_nm", 39 | ).spectrum(wl) 40 | ) 41 | 42 | flux = photon_flux_cell[1] 43 | 44 | # Since we need previous Eg info have to iterate the Jsc array 45 | 46 | jscs = np.empty_like(egs) # Quick way of defining jscs with same dimensions as egs 47 | j01s = np.empty_like(egs) 48 | jscs_trapz = np.empty_like(egs) # Quick way of defining jscs with same dimensions as egs 49 | j01s_all = np.empty_like(egs) 50 | j01s_wl = np.empty_like(egs) 51 | j01s_wl_rect = np.empty_like(egs) 52 | 53 | R = np.random.rand(len(wl)) 54 | # R = R*0 55 | flux = flux * (1 - R) 56 | 57 | start = time() 58 | j = 0 59 | while j < 1000: 60 | upperE = 4.43 61 | for i, eg in enumerate(egs): 62 | j01s_all[i] = ( 63 | (pref * kbT / rad_eff) 64 | * (eg ** 2 + 2 * eg * (kbT) + 2 * (kbT) ** 2) 65 | * np.exp(-(eg) / (kbT)) 66 | ) 67 | 68 | 69 | 70 | wl_inds = np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0) 71 | wl_slice = wl[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)] 72 | j01s[i] = (pref_wl/rad_eff) * np.trapz((1-R[wl_inds])*np.exp(-wl_exp_const/wl_slice)/(wl_slice)**4, wl_slice) 73 | 74 | # j01s[i] = (pref_wl/rad_eff) * np.sum((1-R[wl_inds])*np.exp(-wl_exp_const/wl_slice)/(wl_slice)**4)*interval 75 | 76 | jscs[i] = q * np.trapz(flux[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)], dx=interval) 77 | 78 | # plt.figure() 79 | # plt.plot(wl_cell[np.all((wl_cell < 1240 / eg, wl_cell > 1240 / upperE), axis=0)], flux[np.all((wl_cell < 1240 / eg, wl_cell > 1240 / upperE), axis=0)]) 80 | # plt.show() 81 | upperE = eg 82 | 83 | Vmaxs_all = kbT * (lambertw(e * (jscs / j01s_all)) - 1) 84 | Vmaxs = (kbT * (lambertw(e * (jscs / j01s)) - 1)).real 85 | Imaxs_all = jscs - j01s_all * np.exp(Vmaxs_all / (kbT)) 86 | Imaxs = jscs - j01s * np.exp(Vmaxs / (kbT)) 87 | 88 | j += 1 89 | 90 | 91 | print(time()-start, j01s) 92 | print(Vmaxs, Vmaxs_all) 93 | print(Imaxs, Imaxs_all) 94 | 95 | # jscs[i] = ( 96 | # q 97 | # * np.sum(flux[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)]) 98 | # * interval 99 | # ) 100 | # 101 | # jscs_trapz[i] = q*np.trapz(flux[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)], wl[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)]) 102 | # 103 | # upperE = eg 104 | # 105 | # Vmaxs = (kbT * (lambertw(e * (jscs / j01s)) - 1)).real 106 | # Imaxs = jscs - j01s * np.exp(Vmaxs / (kbT)) 107 | # 108 | # print("rectangle sum Jscs:", jscs) 109 | # print("trapz Jscs:", jscs_trapz) 110 | # 111 | # print("rectangle sum J01s:", j01s) 112 | # print("trapz J01s:", j01s_trapz) 113 | # print("wl J01s:", j01s_wl) 114 | # print("wl J01s rec:", j01s_wl_rect) 115 | # print(np.array(j01s_wl)/np.array(j01s)) 116 | 117 | from ecopv.optimization_functions import db_cell_calculation_numericalR, \ 118 | db_cell_calculation_noR, db_cell_calculation_perfectR 119 | from ecopv.spectrum_functions import gen_spectrum_ndip 120 | import matplotlib.pyplot as plt 121 | 122 | x = np.array([450, 575, 50, 100]) 123 | x = np.array([450, 575, 800, 75, 50, 100]) 124 | egs = [1.1] 125 | 126 | R_array = gen_spectrum_ndip(x, 3, wl, 1, 0) 127 | 128 | plt.figure() 129 | plt.plot(1240/wl, R_array) 130 | for eg in egs: 131 | plt.axvline(eg, color='r') 132 | plt.show() 133 | 134 | flux_with_R = flux * (1-R_array) 135 | 136 | res_numerical = db_cell_calculation_numericalR(egs, flux_with_R, wl, interval, 1, 137 | 4.43, x, n_peaks=3) 138 | 139 | res_noR = db_cell_calculation_noR(egs, flux_with_R, wl, interval, 1, 4.43, x) 140 | res_perfectR = db_cell_calculation_perfectR(egs, flux_with_R, wl, interval, 1, 141 | 4.43, x, n_peaks=3) -------------------------------------------------------------------------------- /examples/plot_colorchecker_gamut.py: -------------------------------------------------------------------------------- 1 | from colormath.color_objects import xyYColor, XYZColor 2 | from ecopv.plot_utilities import * 3 | from colour import wavelength_to_XYZ 4 | 5 | Y = 0.5 6 | 7 | wl_vis = np.linspace(360, 780, 500) 8 | 9 | XYZ = wavelength_to_XYZ(wl_vis) 10 | 11 | sumXYZ = np.sum(XYZ, axis=1) 12 | 13 | xg = XYZ[:, 0] / sumXYZ 14 | yg = XYZ[:, 1] / sumXYZ 15 | 16 | 17 | color_names, color_xyY = load_colorchecker( 18 | output_coords="xyY" 19 | ) # load the names and XYZ coordinates of the 24 default Babel colors 20 | 21 | xs = np.arange(np.min(xg), np.max(xg), 0.005) 22 | ys = np.arange(np.min(yg), np.max(yg), 0.005) 23 | 24 | width = np.diff(xs)[0] 25 | height = np.diff(ys)[0] 26 | 27 | is_inside = np.full((len(xs), len(ys)), False) 28 | 29 | peak = np.argmax(yg) 30 | 31 | left_edge = [xg[:peak], yg[:peak]] 32 | right_edge = [xg[peak:], yg[peak:]] 33 | 34 | # now check if the points are inside the gamut defined by the spectral locus 35 | 36 | for j, yc in enumerate(ys): 37 | left_y = np.argmin(np.abs(left_edge[1] - yc)) 38 | right_y = np.argmin(np.abs(right_edge[1] - yc)) 39 | 40 | left_x = left_edge[0][left_y] 41 | right_x = right_edge[0][right_y] 42 | is_inside[np.all((xs > left_x, xs < right_x), axis=0), j] = True 43 | 44 | # eliminate everything below the line of purples: 45 | 46 | # equation for line of purples: 47 | 48 | slope = (yg[-1] - yg[0]) / (xg[-1] - xg[0]) 49 | c = yg[0] - slope * xg[0] 50 | 51 | for j, yc in enumerate(ys): 52 | above = yc > slope * xs + c 53 | is_inside[:, j] = np.all((above, is_inside[:, j]), axis=0) 54 | 55 | 56 | standard_illuminant = [0.3128, 0.3290, Y] 57 | XYZ = convert_color(xyYColor(*standard_illuminant), XYZColor) 58 | s_i_RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 59 | s_i_RGB[s_i_RGB > 1] = 1 60 | 61 | label_wls = np.arange(440, 660, 20) 62 | 63 | XYZlab = wavelength_to_XYZ(label_wls) 64 | 65 | sumXYZlab = np.sum(XYZlab, axis=1) 66 | 67 | xgl = XYZlab[:, 0] / sumXYZlab 68 | ygl = XYZlab[:, 1] / sumXYZlab 69 | 70 | tick_orig = np.zeros((len(label_wls), 2)) 71 | tick_dir = np.zeros((len(label_wls), 2)) 72 | # create ticks 73 | for m1, lwl in enumerate(label_wls): 74 | p0 = wavelength_to_XYZ(lwl) 75 | p1 = wavelength_to_XYZ(lwl - 1) 76 | p2 = wavelength_to_XYZ(lwl + 1) 77 | 78 | p0 = p0 / np.sum(p0) 79 | p1 = p1 / np.sum(p1) 80 | p2 = p2 / np.sum(p2) 81 | 82 | m = np.array([p2[0] - p1[0], p2[1] - p1[1]]) 83 | mp = np.array([-m[1], m[0]]) 84 | mp = mp / np.linalg.norm(mp) 85 | # b = p1[1] + (1/m) * p1[0] 86 | 87 | tick_orig[m1] = p0[:2] 88 | tick_dir[m1] = p0[:2] + 0.02 * mp 89 | 90 | fig, ax = plt.subplots(1, figsize=(3.7, 4.3)) 91 | 92 | # ax.set_aspect('equal') 93 | ax.set_facecolor(s_i_RGB) 94 | ax.plot(xg, yg, "k") 95 | ax.plot([xg[0], xg[-1]], [yg[0], yg[-1]], "k") 96 | 97 | for m1, lwl in enumerate(label_wls): 98 | ax.plot( 99 | [tick_orig[m1, 0], tick_dir[m1, 0]], [tick_orig[m1, 1], tick_dir[m1, 1]], "-k" 100 | ) 101 | 102 | if lwl > 520: 103 | ax.text(*tick_dir[m1], str(lwl)) 104 | 105 | elif lwl == 520: 106 | ax.text(*tick_dir[m1], str(lwl), horizontalalignment="center") 107 | 108 | else: 109 | ax.text( 110 | *tick_dir[m1], 111 | str(lwl), 112 | horizontalalignment="right", 113 | verticalalignment="center" 114 | ) 115 | 116 | ax.set_xlim(-0.09, 0.8) 117 | ax.set_ylim(-0.07, 0.9) 118 | ax.set_xlabel("x") 119 | ax.set_ylabel("y") 120 | 121 | for j1, x in enumerate(xs): 122 | for k1, y in enumerate(ys): 123 | 124 | if is_inside[j1, k1]: 125 | XYZ = convert_color(xyYColor(x, y, Y), XYZColor) 126 | RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 127 | 128 | RGB[RGB > 1] = 1 129 | # plt.plot(x, y, '.') 130 | ax.add_patch( 131 | Rectangle( 132 | xy=(x - width / 2, y - height / 2), 133 | width=width, 134 | height=height, 135 | facecolor=RGB, 136 | ) 137 | ) 138 | 139 | 140 | for m1, c in enumerate(color_xyY[:18]): 141 | ax.scatter(c[0], c[1], s=4, facecolor="none", edgecolor="k", linewidth=0.5) 142 | ax.text(c[0], c[1], str(m1 + 1), size=8) 143 | 144 | ax.scatter( 145 | color_xyY[19, 0], color_xyY[19, 1], s=8, facecolor="k", edgecolor="k", linewidth=0.5 146 | ) 147 | ax.text(color_xyY[19, 0], color_xyY[19, 1], "19-24", size=8) 148 | 149 | ax.yaxis.set_minor_locator(tck.AutoMinorLocator()) 150 | ax.xaxis.set_minor_locator(tck.AutoMinorLocator()) 151 | ax.grid(axis="both", color="0.4", alpha=0.5) 152 | ax.tick_params(direction="in", which="both", top=True, right=True) 153 | # ax.set_axisbelow(True) 154 | 155 | fig.savefig("gamut_colorchecker.pdf", bbox_inches="tight") 156 | plt.tight_layout() 157 | plt.show() 158 | -------------------------------------------------------------------------------- /ecopv/data/ColorChecker_R_BabelColor.csv: -------------------------------------------------------------------------------- 1 | 380,390,400,410,420,430,440,450,460,470,480,490,500,510,520,530,540,550,560,570,580,590,600,610,620,630,640,650,660,670,680,690,700,710,720,730 2 | 0.055,0.058,0.061,0.062,0.062,0.062,0.062,0.062,0.062,0.062,0.062,0.063,0.065,0.07,0.076,0.079,0.081,0.084,0.091,0.103,0.119,0.134,0.143,0.147,0.151,0.158,0.168,0.179,0.188,0.19,0.186,0.181,0.182,0.187,0.196,0.209 3 | 0.117,0.143,0.175,0.191,0.196,0.199,0.204,0.213,0.228,0.251,0.28,0.309,0.329,0.333,0.315,0.286,0.273,0.276,0.277,0.289,0.339,0.42,0.488,0.525,0.546,0.562,0.578,0.595,0.612,0.625,0.638,0.656,0.678,0.7,0.717,0.734 4 | 0.13,0.177,0.251,0.306,0.324,0.33,0.333,0.331,0.323,0.311,0.298,0.285,0.269,0.25,0.231,0.214,0.199,0.185,0.169,0.157,0.149,0.145,0.142,0.141,0.141,0.141,0.143,0.147,0.152,0.154,0.15,0.144,0.136,0.132,0.135,0.147 5 | 0.051,0.054,0.056,0.057,0.058,0.059,0.06,0.061,0.062,0.063,0.065,0.067,0.075,0.101,0.145,0.178,0.184,0.17,0.149,0.133,0.122,0.115,0.109,0.105,0.104,0.106,0.109,0.112,0.114,0.114,0.112,0.112,0.115,0.12,0.125,0.13 6 | 0.144,0.198,0.294,0.375,0.408,0.421,0.426,0.426,0.419,0.403,0.379,0.346,0.311,0.281,0.254,0.229,0.214,0.208,0.202,0.194,0.193,0.2,0.214,0.23,0.241,0.254,0.279,0.313,0.348,0.366,0.366,0.359,0.358,0.365,0.377,0.398 7 | 0.136,0.179,0.247,0.297,0.32,0.337,0.355,0.381,0.419,0.466,0.51,0.546,0.567,0.574,0.569,0.551,0.524,0.488,0.445,0.4,0.35,0.299,0.252,0.221,0.204,0.196,0.191,0.188,0.191,0.199,0.212,0.223,0.232,0.233,0.229,0.229 8 | 0.054,0.054,0.053,0.054,0.054,0.055,0.055,0.055,0.056,0.057,0.058,0.061,0.068,0.089,0.125,0.154,0.174,0.199,0.248,0.335,0.444,0.538,0.587,0.595,0.591,0.587,0.584,0.584,0.59,0.603,0.62,0.639,0.655,0.663,0.663,0.667 9 | 0.122,0.164,0.229,0.286,0.327,0.361,0.388,0.4,0.392,0.362,0.316,0.26,0.209,0.168,0.138,0.117,0.104,0.096,0.09,0.086,0.084,0.084,0.084,0.084,0.084,0.085,0.09,0.098,0.109,0.123,0.143,0.169,0.205,0.244,0.287,0.332 10 | 0.096,0.115,0.131,0.135,0.133,0.132,0.13,0.128,0.125,0.12,0.115,0.11,0.105,0.1,0.095,0.093,0.092,0.093,0.096,0.108,0.156,0.265,0.399,0.5,0.556,0.579,0.588,0.591,0.593,0.594,0.598,0.602,0.607,0.609,0.609,0.61 11 | 0.092,0.116,0.146,0.169,0.178,0.173,0.158,0.139,0.119,0.101,0.087,0.075,0.066,0.06,0.056,0.053,0.051,0.051,0.052,0.052,0.051,0.052,0.058,0.073,0.096,0.119,0.141,0.166,0.194,0.227,0.265,0.309,0.355,0.396,0.436,0.478 12 | 0.061,0.061,0.062,0.063,0.064,0.066,0.069,0.075,0.085,0.105,0.139,0.192,0.271,0.376,0.476,0.531,0.549,0.546,0.528,0.504,0.471,0.428,0.381,0.347,0.327,0.318,0.312,0.31,0.314,0.327,0.345,0.363,0.376,0.381,0.378,0.379 13 | 0.063,0.063,0.063,0.064,0.064,0.064,0.065,0.066,0.067,0.068,0.071,0.076,0.087,0.125,0.206,0.305,0.383,0.431,0.469,0.518,0.568,0.607,0.628,0.637,0.64,0.642,0.645,0.648,0.651,0.653,0.657,0.664,0.673,0.68,0.684,0.688 14 | 0.066,0.079,0.102,0.146,0.2,0.244,0.282,0.309,0.308,0.278,0.231,0.178,0.13,0.094,0.07,0.054,0.046,0.042,0.039,0.038,0.038,0.038,0.038,0.039,0.039,0.04,0.041,0.042,0.044,0.045,0.046,0.046,0.048,0.052,0.057,0.065 15 | 0.052,0.053,0.054,0.055,0.057,0.059,0.061,0.066,0.075,0.093,0.125,0.178,0.246,0.307,0.337,0.334,0.317,0.293,0.262,0.23,0.198,0.165,0.135,0.115,0.104,0.098,0.094,0.092,0.093,0.097,0.102,0.108,0.113,0.115,0.114,0.114 16 | 0.05,0.049,0.048,0.047,0.047,0.047,0.047,0.047,0.046,0.045,0.044,0.044,0.045,0.046,0.047,0.048,0.049,0.05,0.054,0.06,0.072,0.104,0.178,0.312,0.467,0.581,0.644,0.675,0.69,0.698,0.706,0.715,0.724,0.73,0.734,0.738 17 | 0.058,0.054,0.052,0.052,0.053,0.054,0.056,0.059,0.067,0.081,0.107,0.152,0.225,0.336,0.462,0.559,0.616,0.65,0.672,0.694,0.71,0.723,0.731,0.739,0.746,0.752,0.758,0.764,0.769,0.771,0.776,0.782,0.79,0.796,0.799,0.804 18 | 0.145,0.195,0.283,0.346,0.362,0.354,0.334,0.306,0.276,0.248,0.218,0.19,0.168,0.149,0.127,0.107,0.1,0.102,0.104,0.109,0.137,0.2,0.29,0.4,0.516,0.615,0.687,0.732,0.76,0.774,0.783,0.793,0.803,0.812,0.817,0.825 19 | 0.108,0.141,0.192,0.236,0.261,0.286,0.317,0.353,0.39,0.426,0.446,0.444,0.423,0.385,0.337,0.283,0.231,0.185,0.146,0.118,0.101,0.09,0.082,0.076,0.074,0.073,0.073,0.074,0.076,0.077,0.076,0.075,0.073,0.072,0.074,0.079 20 | 0.189,0.255,0.423,0.66,0.811,0.862,0.877,0.884,0.891,0.896,0.899,0.904,0.907,0.909,0.911,0.91,0.911,0.914,0.913,0.916,0.915,0.916,0.914,0.915,0.918,0.919,0.921,0.923,0.924,0.922,0.922,0.925,0.927,0.93,0.93,0.933 21 | 0.171,0.232,0.365,0.507,0.567,0.583,0.588,0.59,0.591,0.59,0.588,0.588,0.589,0.589,0.591,0.59,0.59,0.59,0.589,0.591,0.59,0.59,0.587,0.585,0.583,0.58,0.578,0.576,0.574,0.572,0.571,0.569,0.568,0.568,0.566,0.566 22 | 0.144,0.192,0.272,0.331,0.35,0.357,0.361,0.363,0.363,0.361,0.359,0.358,0.358,0.359,0.36,0.36,0.361,0.361,0.36,0.362,0.362,0.361,0.359,0.358,0.355,0.352,0.35,0.348,0.345,0.343,0.34,0.338,0.335,0.334,0.332,0.331 23 | 0.105,0.131,0.163,0.18,0.186,0.19,0.193,0.194,0.194,0.192,0.191,0.191,0.191,0.192,0.192,0.192,0.192,0.192,0.192,0.193,0.192,0.192,0.191,0.189,0.188,0.186,0.184,0.182,0.181,0.179,0.178,0.176,0.174,0.173,0.172,0.171 24 | 0.068,0.077,0.084,0.087,0.089,0.09,0.092,0.092,0.091,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.089,0.089,0.088,0.087,0.086,0.086,0.085,0.084,0.084,0.083,0.083,0.082,0.081,0.081,0.081 25 | 0.031,0.032,0.032,0.033,0.033,0.033,0.033,0.033,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.032,0.033 -------------------------------------------------------------------------------- /examples/run_cell_color_optim.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import load_colorchecker, multiple_color_cells 2 | import numpy as np 3 | from solcore.light_source import LightSource 4 | import matplotlib.pyplot as plt 5 | from time import time 6 | import os 7 | 8 | col_thresh = 0.004 # for a wavelength interval of 0.1, minimum achievable color error deltaXYZ will be ~ 0.001. 9 | # (deltaXYZ = maximum fractional error in X, Y, Z colour coordinates) 10 | acceptable_eff_change = ( 11 | 1e-4 # how much can the efficiency (in %) change between iteration sets? 12 | ) 13 | n_trials = 10 # number of islands which will run concurrently 14 | interval = 0.1 # wavelength interval (in nm) 15 | wl_cell = np.arange(280, 4000, interval) # wavelengths 16 | 17 | initial_iters = 100 # number of initial evolutions for the archipelago 18 | add_iters = 100 # additional evolutions added each time if color threshold/convergence condition not met 19 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 20 | 21 | max_trials_col = 5 * add_iters 22 | # how many population evolutions happen before giving up if there are no populations 23 | # which meet the color threshold 24 | 25 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 26 | fixed_height = True # fixed height peaks (will be at the value of max_height) or not 27 | j01_method = "perfect_R" 28 | 29 | max_height = 1 # maximum height of reflection peaks 30 | base = 0 # baseline fixed reflection 31 | 32 | n_junctions = 1 # number of junctions in the cell 33 | 34 | n_peaks = 2 # number of reflection peaks 35 | 36 | color_names, color_XYZ = load_colorchecker() # load the 24 default ColorChecker colors 37 | 38 | single_junction_data = np.loadtxt(os.path.join(os.path.dirname(os.path.dirname( 39 | __file__)), 'ecopv', 'data', 40 | 'paper_colors.csv'), skiprows=1, delimiter=',', 41 | usecols=np.arange(2,10)) 42 | # Define the incident photon flux. This should be a 2D array with the first row being the wavelengths and the second row 43 | # being the photon flux at each wavelength. The wavelengths should be in nm and the photon flux in photons/m^2/s/nm. 44 | photon_flux_cell = np.array( 45 | LightSource( 46 | source_type="standard", 47 | version="AM1.5g", 48 | x=wl_cell, 49 | output_units="photon_flux_per_nm", 50 | ).spectrum(wl_cell) 51 | ) 52 | 53 | # Use only the visible range of wavelengths (380-780 nm) for color calculations: 54 | photon_flux_color = photon_flux_cell[ 55 | :, np.all((photon_flux_cell[0] >= 380, photon_flux_cell[0] <= 780), axis=0) 56 | ] 57 | 58 | shapes = ["+", "o", "^", ".", "*", "v", "s", "x"] 59 | 60 | loop_n = 0 61 | 62 | if __name__ == "__main__": 63 | 64 | start = time() 65 | # Need this because otherwise the parallel running of the different islands (n_trials) may throw an error 66 | 67 | fig1 = plt.figure(1, figsize=(7.5, 3)) 68 | 69 | # Run for the selected peak shape, with both fixed and non-fixed height 70 | result = multiple_color_cells( 71 | color_XYZ, 72 | color_names, 73 | photon_flux_cell, 74 | n_peaks=n_peaks, 75 | n_junctions=n_junctions, 76 | R_type=R_type, 77 | fixed_height=fixed_height, 78 | n_trials=n_trials, 79 | initial_iters=initial_iters, 80 | add_iters=add_iters, 81 | col_thresh=col_thresh, 82 | acceptable_eff_change=acceptable_eff_change, 83 | max_trials_col=max_trials_col, 84 | base=base, 85 | max_height=max_height, 86 | plot=True, 87 | j01_method=j01_method, 88 | ) 89 | 90 | plt.figure() 91 | plt.plot( 92 | color_names, 93 | result["champion_eff"], 94 | marker=shapes[0], 95 | mfc="none", 96 | linestyle="none", 97 | ) 98 | 99 | plt.plot(color_names, single_junction_data[:,3], 100 | marker=shapes[1], mfc='none', linestyle='none') 101 | 102 | plt.xticks(rotation=45) 103 | plt.legend() 104 | plt.ylabel("Efficiency (%)") 105 | # plt.title("Pop:" + str(pop_size) + "Iters:" + str(n_iters) + "Time:" + str(time_taken)) 106 | plt.tight_layout() 107 | plt.show() 108 | 109 | plt.figure() 110 | plt.plot( 111 | color_names, 112 | result["champion_pop"][:, -n_junctions:], 113 | marker=shapes[0], 114 | mfc="none", 115 | linestyle="none", 116 | ) 117 | 118 | plt.plot(color_names, single_junction_data[:,7], 119 | marker=shapes[1], mfc='none', linestyle='none') 120 | 121 | plt.xticks(rotation=45) 122 | plt.legend() 123 | plt.ylabel("Bandgap (eV)") 124 | plt.tight_layout() 125 | plt.show() 126 | 127 | # champion_pop = np.array([reorder_peaks(x, n_peaks) for x in champion_pop]) 128 | # np.save("results/champion_eff_tcheb_adaptpopsize_gauss_vheight2" + str(n_peaks) + '_' + str(n_junctions) + '_' + str(ntest), champion_eff) 129 | # np.save("results/champion_pop_tcheb_adaptpopsize_gauss_vheight2" + str(n_peaks) + '_' + str(n_junctions) + '_' + str(ntest), champion_pop) 130 | # np.save("results/niters_tcheb_adaptpopsize_gauss_vheight2" + str(n_peaks) + '_' + str(n_junctions) + '_' + str(ntest), iters_needed) 131 | print(time() - start) -------------------------------------------------------------------------------- /examples/run_cell_color_optim_loop_spd_compareJ0.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import ( 2 | load_colorchecker, 3 | multiple_color_cells, 4 | cell_optimization, 5 | ) 6 | import numpy as np 7 | from solcore.light_source import LightSource 8 | import matplotlib.pyplot as plt 9 | import pygmo as pg 10 | from os import path 11 | import pandas as pd 12 | 13 | 14 | col_thresh = 0.004 # for a wavelength interval of 0.1, minimum achievable color error will be (very rough estimate!) ~ 0.001. 15 | # This is the maximum allowed fractional error in X, Y, or Z colour coordinates. 16 | 17 | acceptable_eff_change = 1e-4 # how much can the efficiency (in %) change between iteration sets? Stop when have reached 18 | # col_thresh and efficiency change is less than this. 19 | 20 | n_trials = 10 # number of islands which will run concurrently 21 | interval = 0.1 # wavelength interval (in nm) 22 | wl_cell = np.arange( 23 | 300, 4000, interval 24 | ) # wavelengths used for cell calculations (range of wavelengths in AM1.5G solar 25 | # spectrum. For calculations relating to colour perception, only the visible range (380-730 nm) will be used. 26 | 27 | single_J_result = pd.read_csv("../ecopv/data/paper_colors.csv") 28 | 29 | initial_iters = 100 # number of initial evolutions for the archipelago 30 | add_iters = 100 # additional evolutions added each time if color threshold/convergence condition not met 31 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 32 | 33 | max_trials_col = 3 * add_iters 34 | # how many population evolutions happen before giving up if there are no populations 35 | # which meet the color threshold 36 | 37 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 38 | fixed_height = True # fixed height peaks (will be at the value of max_height) if True, or peak height is an optimization 39 | # variable if False 40 | light_source_name = "AM1.5g" 41 | j01_methods = ["no_R", "perfect_R"] 42 | 43 | max_height = 1 44 | # maximum height of reflection peaks; fixed at this value of if fixed_height = True 45 | 46 | base = 0 47 | # baseline fixed reflection (fixed at this value for both fixed_height = True and False). 48 | 49 | n_junc_loop = [1, 2, 3, 4, 5, 6] # loop through these numbers of junctions 50 | n_peak_loop = [2] # loop through these numbers of reflection peaks 51 | 52 | color_names, color_XYZ = load_colorchecker() 53 | # load the names and XYZ coordinates of the 24 default Babel colors 54 | start_ind = 0 55 | end_ind = len(color_names) 56 | color_names = color_names[start_ind:end_ind] 57 | color_XYZ = color_XYZ[start_ind:end_ind] 58 | 59 | # Use AM1.5G spectrum: 60 | light_source = LightSource( 61 | source_type="standard", 62 | version=light_source_name, 63 | x=wl_cell, 64 | output_units="photon_flux_per_nm", 65 | ) 66 | 67 | photon_flux_cell = np.array(light_source.spectrum(wl_cell)) 68 | 69 | photon_flux_color = photon_flux_cell[ 70 | :, np.all((photon_flux_cell[0] >= 380, photon_flux_cell[0] <= 730), axis=0) 71 | ] 72 | 73 | shapes = ["+", "o", "^", ".", "*", "v", "s", "x"] 74 | 75 | loop_n = 0 76 | 77 | # precalculate optimal bandgaps for junctions: 78 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 79 | 80 | # Need this __main__ construction because otherwise the parallel running of the different islands (n_trials) may throw an error 81 | 82 | for n_peaks in n_peak_loop: 83 | for n_junctions in n_junc_loop: 84 | plt.figure() 85 | champion_effs = np.zeros((len(j01_methods), len(color_names))) 86 | for l1, j01_method in enumerate(j01_methods): 87 | 88 | save_loc = ( 89 | "results/champion_eff_" 90 | + R_type 91 | + str(n_peaks) 92 | + "_" 93 | + str(n_junctions) 94 | + "_" 95 | + str(fixed_height) 96 | + str(max_height) 97 | + "_" 98 | + str(base) 99 | + "_" 100 | + j01_method + ".txt" 101 | ) 102 | 103 | 104 | champion_effs[l1] = np.loadtxt( 105 | "results/champion_eff_" 106 | + R_type 107 | + str(n_peaks) 108 | + "_" 109 | + str(n_junctions) 110 | + "_" 111 | + str(fixed_height) 112 | + str(max_height) 113 | + "_" 114 | + str(base) 115 | + "_" + j01_method + ".txt", 116 | ) 117 | 118 | champion_pops = np.loadtxt( 119 | "results/champion_pop_" 120 | + R_type 121 | + str(n_peaks) 122 | + "_" 123 | + str(n_junctions) 124 | + "_" 125 | + str(fixed_height) 126 | + str(max_height) 127 | + "_" 128 | + str(base) 129 | + "_" + j01_method + ".txt", 130 | ) 131 | champion_bandgaps = champion_pops[:, -n_junctions:] 132 | 133 | plt.plot( 134 | color_names, 135 | champion_effs[l1], 136 | marker=shapes[l1], 137 | mfc="none", 138 | linestyle="none", 139 | ) 140 | 141 | # plt.plot( 142 | # color_names, 143 | # 100*(champion_effs[1]-champion_effs[0])/champion_effs[1], 144 | # marker=shapes[l1], 145 | # mfc="none", 146 | # linestyle="none", 147 | # ) 148 | 149 | plt.xticks(rotation=45) 150 | plt.legend() 151 | plt.ylabel("Efficiency (%)") 152 | plt.title("Peaks:" + str(n_peaks) + "Junctions:" + str(n_junctions)) 153 | plt.tight_layout() 154 | 155 | plt.show() 156 | 157 | -------------------------------------------------------------------------------- /examples/intro_figure.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from os.path import join, dirname 4 | import pathlib 5 | from solcore.light_source import LightSource 6 | from ecopv.spectrum_functions import gen_spectrum_ndip, load_cmf, spec_to_XYZ 7 | from ecopv.plot_utilities import * 8 | 9 | current_path = pathlib.Path(__file__).parent.resolve() 10 | 11 | color_names = np.array( 12 | [ 13 | "DarkSkin", 14 | "LightSkin", 15 | "BlueSky", 16 | "Foliage", 17 | "BlueFlower", 18 | "BluishGreen", 19 | "Orange", 20 | "PurplishBlue", 21 | "ModerateRed", 22 | "Purple", 23 | "YellowGreen", 24 | "OrangeYellow", 25 | "Blue", 26 | "Green", 27 | "Red", 28 | "Yellow", 29 | "Magenta", 30 | "Cyan", 31 | "White-9-5", 32 | "Neutral-8", 33 | "Neutral-6-5", 34 | "Neutral-5", 35 | "Neutral-3-5", 36 | "Black-2", 37 | ] 38 | ) 39 | 40 | ind = np.where(color_names == "Orange")[0][0] 41 | 42 | color_R = np.loadtxt(join(dirname(current_path), "ecopv", "data", 43 | "ColorChecker_R_BabelColor.csv"), delimiter=',', 44 | encoding='utf-8-sig') 45 | interval = 0.1 46 | wl_cell = np.arange(300, 1200, interval) 47 | 48 | light_source = LightSource( 49 | source_type="standard", 50 | version="AM1.5g", 51 | x=wl_cell, 52 | output_units="photon_flux_per_nm", 53 | ) 54 | 55 | R_type = "sharp" 56 | n_peaks = 2 57 | n_junctions = 1 58 | fixed_height = True 59 | max_height = 1 60 | base = 0 61 | j01_method = "perfect_R" 62 | light_source_name = "AM1.5g" 63 | 64 | champion_pops = np.loadtxt( 65 | "results/champion_pop_" 66 | + R_type 67 | + str(n_peaks) 68 | + "_" 69 | + str(n_junctions) 70 | + "_" 71 | + str(fixed_height) 72 | + str(max_height) 73 | + "_" 74 | + str(base) 75 | + "_" + j01_method + light_source_name + ".txt", 76 | ) 77 | 78 | pop = champion_pops[ind] 79 | R_optim = gen_spectrum_ndip(pop, 2, wl_cell) 80 | 81 | color_list = sRGB_color_list(order="unsort") 82 | 83 | cmf = load_cmf(wl_cell) 84 | 85 | wl_colors = np.arange(380, 780.1, 0.5) 86 | RGBA = wavelength_to_rgb(wl_colors) 87 | 88 | fig, (ax, ax2) = plt.subplots(2,1, figsize=(5, 5.5), 89 | gridspec_kw={ 90 | "height_ratios": [2, 1.2], 91 | "hspace": 0.2, 92 | "wspace": 0.05, 93 | }, 94 | ) 95 | 96 | ax.plot(color_R[0], color_R[ind+1], '--', color=color_list[ind], label=r"$R_{real}$") 97 | ax.set_xlim(min(color_R[0]), max(color_R[0])) 98 | ax2.set_xlim(min(color_R[0]), max(color_R[0])) 99 | ax.plot(wl_cell, R_optim, '-', color=color_list[ind], label=r"$R_{ideal}$") 100 | ax.plot(wl_cell, cmf[:,0], '-.', color='r', alpha=0.7, label=r"$\bar{x}$") 101 | ax.plot(wl_cell, cmf[:,1], '-.', color='g', alpha=0.7, label=r"$\bar{y}$") 102 | ax.plot(wl_cell, cmf[:,2], '-.', color='b', alpha=0.7, label=r"$\bar{z}$") 103 | # ax.plot(0, 0, '-k', label="AM1.5g") 104 | # ax.plot(0, 0, '-', color=color_list[ind], label=r"$R_{ideal}$") 105 | # ax.plot(0, 0, '--', color=color_list[ind], label=r"$R_{real}$") 106 | ax.grid() 107 | ax2.grid() 108 | ax2.set_xlabel("Wavelength (nm)") 109 | ax.set_ylabel("Spectral sensitivity / Reflectance") 110 | ax2.set_ylabel(r"SPD ($\times 10^{18}$ W m$^{-2}$ nm$^{-1}$)") 111 | ax2.set_ylim(0, 5) 112 | ax.set_ylim(0, 1.85) 113 | ax.legend(loc="upper right") 114 | # ax2.set_yticks([0, 0.25, 0.5, 0.75, 1]) 115 | ax.xaxis.set_ticklabels([]) 116 | 117 | ax2.plot(wl_colors, light_source.spectrum(wl_colors)[1]/1e18, '-k', 118 | alpha=0.5) 119 | 120 | for i1 in range(len(RGBA)): 121 | ax2.add_patch( 122 | Rectangle( 123 | xy=(wl_colors[i1], 0), 124 | height=light_source.spectrum(wl_colors)[1][i1]/1e18, 125 | width=0.5, 126 | facecolor=RGBA[i1, :3], 127 | alpha=RGBA[i1, 3], 128 | ) 129 | ) 130 | plt.tight_layout() 131 | plt.show() 132 | 133 | 134 | from matplotlib import rc 135 | 136 | 137 | rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"], "size": 14}) 138 | 139 | params = np.array( [438.44042242, 610.9970763, 18.43156718, 67.74693666]) 140 | params_schr = np.array([ 440.13933368, 580.93374126]) 141 | R = gen_spectrum_ndip(params, 2, wl_cell) 142 | 143 | R_schr = np.zeros_like(R) 144 | R_schr[wl_cell <= params_schr[0]] = 1 145 | R_schr[wl_cell >= params_schr[1]] = 1 146 | 147 | illuminant = np.array( 148 | LightSource( 149 | source_type="standard", 150 | version="AM1.5g", 151 | x=wl_cell, 152 | output_units="power_density_per_nm", 153 | ).spectrum(wl_cell)[1] 154 | ) 155 | 156 | XYZ_schr = spec_to_XYZ(R_schr, illuminant, cmf, interval) 157 | XYZ = spec_to_XYZ(R, illuminant, cmf, interval) 158 | 159 | color_xyz_t = XYZColor(*XYZ_schr) 160 | color_srgb_t = convert_color(color_xyz_t, sRGBColor, 161 | target_illuminant="d65")#.get_value_tuple() 162 | color_srgb_t_schr = [ 163 | color_srgb_t.clamped_rgb_r, 164 | color_srgb_t.clamped_rgb_g, 165 | color_srgb_t.clamped_rgb_b, 166 | ] 167 | 168 | color_xyz_t = XYZColor(*XYZ) 169 | color_srgb_t = convert_color(color_xyz_t, sRGBColor, 170 | target_illuminant="d65")#.get_value_tuple() 171 | color_srgb_t = [ 172 | color_srgb_t.clamped_rgb_r, 173 | color_srgb_t.clamped_rgb_g, 174 | color_srgb_t.clamped_rgb_b, 175 | ] 176 | 177 | fig, ax = plt.subplots(1,1, figsize=(7, 4)) 178 | 179 | ax.plot(wl_cell, cmf[:,0], '-.', color='r', alpha=0.7, label=r"$\bar{x}$") 180 | ax.plot(wl_cell, cmf[:,1], '-.', color='g', alpha=0.7, label=r"$\bar{y}$") 181 | ax.plot(wl_cell, cmf[:,2], '-.', color='b', alpha=0.7, label=r"$\bar{z}$") 182 | 183 | # ax.plot(0, 0, '-k', label="AM1.5g") 184 | # ax.plot(0, 0, '-', color=color_list[ind], label=r"$R_{ideal}$") 185 | # ax.plot(0, 0, '--', color=color_list[ind], label=r"$R_{real}$") 186 | # ax.grid() 187 | ax2.set_ylim(0,1) 188 | ax2.set_xlabel("Wavelength (nm)") 189 | ax.set_ylabel("Spectral sensitivity") 190 | ax.set_ylim(0, 2.04) 191 | ax2 = ax.twinx() 192 | ax2.set_ylabel("Reflectance") 193 | ax2.set_yticks([0, 0.25, 0.5, 0.75, 1]) 194 | ax2.plot(wl_cell, R_schr, '-k') 195 | ax2.plot(wl_cell, R, '--r') 196 | ax.legend(loc="upper right") 197 | # ax2.set_yticks([0, 0.25, 0.5, 0.75, 1]) 198 | ax.set_xlim(380, 730) 199 | ax.set_xlabel("Wavelength (nm)") 200 | ax2.set_ylim(0,1.02) 201 | 202 | ax.add_patch( 203 | Rectangle( 204 | xy=(670, 0.8), 205 | width=40, 206 | height=0.2, 207 | facecolor=color_srgb_t, 208 | ) 209 | ) 210 | ax.add_patch( 211 | Rectangle( 212 | xy=(670, 0.5), 213 | width=40, 214 | height=0.2, 215 | facecolor=color_srgb_t_schr, 216 | ) 217 | ) 218 | 219 | plt.tight_layout() 220 | plt.show() 221 | -------------------------------------------------------------------------------- /examples/plot_incident_spectra.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from solcore.light_source import LightSource 3 | import matplotlib.pyplot as plt 4 | from matplotlib import rc 5 | from ecopv.spectrum_functions import load_cmf, spec_to_XYZ 6 | from ecopv.main_optimization import load_colorchecker 7 | from os import path 8 | from cycler import cycler 9 | import seaborn as sns 10 | from colormath.color_objects import sRGBColor, XYZColor 11 | from colormath.color_conversions import convert_color 12 | 13 | pal = sns.husl_palette(6, s=0.6) 14 | 15 | rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"]}) 16 | 17 | interval = 0.1 # wavelength interval (in nm) 18 | wl_cell = np.arange(150, 4000, interval) # wavelengths 19 | 20 | # Define the incident photon flux. This should be a 2D array with the first row being the wavelengths and the second row 21 | # being the photon flux at each wavelength. The wavelengths should be in nm and the photon flux in photons/m^2/s/nm. 22 | photon_flux_AM15 = np.array( 23 | LightSource( 24 | source_type="standard", 25 | version="AM1.5g", 26 | x=wl_cell, 27 | output_units="photon_flux_per_nm", 28 | ).spectrum(wl_cell) 29 | ) 30 | 31 | photon_flux_bb = np.array(LightSource( 32 | source_type="black body", 33 | x=wl_cell, 34 | output_units="photon_flux_per_nm", 35 | entendue="Sun", 36 | T=5778, 37 | ).spectrum(wl_cell)) 38 | 39 | fig, ax = plt.subplots(1,1) 40 | ax.plot(wl_cell, photon_flux_AM15[1]/1e18, '-k', label="AM1.5G") 41 | ax.plot(wl_cell, photon_flux_bb[1]/1e18, '--r', label="5778K black body") 42 | ax.set_xlabel("Wavelength (nm)") 43 | ax.set_ylabel(r"Photon flux ($\times 10^{18}$ photons m$^{-2}$s$^{-1}$ nm$^{-1}$)") 44 | ax.grid(axis="both", color="0.9") 45 | ax.legend() 46 | plt.tight_layout() 47 | ax.set_xlim(min(wl_cell), max(wl_cell)) 48 | ax.set_ylim(-0.05, 5.2) 49 | plt.show() 50 | 51 | import numpy as np 52 | import matplotlib.pyplot as plt 53 | from solcore.light_source import LightSource 54 | import pathlib 55 | 56 | wl_cell = np.linspace(300, 1800, 400) 57 | 58 | SPD = np.array( 59 | LightSource( 60 | source_type="standard", 61 | version="AM1.5g", 62 | x=wl_cell, 63 | output_units="power_density_per_nm", 64 | ).spectrum(wl_cell) 65 | )[1] 66 | 67 | cmf = load_cmf(wl_cell) 68 | 69 | SPD_bb = np.array( 70 | LightSource( 71 | source_type="black body", 72 | x=wl_cell, 73 | output_units="power_density_per_nm", 74 | entendue="Sun", 75 | T=5778, 76 | ).spectrum(wl_cell) 77 | )[1] 78 | 79 | current_path = pathlib.Path(__file__).parent.resolve() 80 | 81 | 82 | D50 = np.loadtxt(path.join(path.dirname(current_path), "ecopv", "data", 83 | "CIE_std_illum_D50.csv"), delimiter=',').T 84 | D65 = np.loadtxt(path.join(path.dirname(current_path), "ecopv", "data", 85 | "CIE_std_illum_D65.csv"), delimiter=',').T 86 | pal2 = ["r", "g", "b"] 87 | cols = cycler("color", pal2) 88 | params = {"axes.prop_cycle": cols} 89 | plt.rcParams.update(params) 90 | 91 | fig, ax = plt.subplots(1,1) 92 | ax.plot(wl_cell, SPD/max(SPD), '-', label="AM1.5G", color=pal[5]) 93 | ax.plot(wl_cell, SPD_bb/max(SPD_bb), '--', label="5778K black body", color="k") 94 | ax.plot(D50[0], D50[1]/max(D50[1]), '-.', label="D50 illuminant", color=pal[1]) 95 | ax.plot(D50[0], D65[1]/max(D65[1]), '-.', label="D65 illuminant", color=pal[4]) 96 | ax.plot(wl_cell, cmf[:,0]/3, linestyle='dotted', alpha=0.8, label=r"$\bar{x}$/3") 97 | ax.plot(wl_cell, cmf[:,1]/3, linestyle='dotted', alpha=0.8, label=r"$\bar{y}$/3") 98 | ax.plot(wl_cell, cmf[:,2]/3, linestyle='dotted', alpha=0.8, label=r"$\bar{z}$/3") 99 | ax.set_xlabel("Wavelength (nm)") 100 | ax.set_ylabel("Normalised spectral power distribution") 101 | ax.grid(axis="both", color="0.9") 102 | ax.legend(loc="lower right") 103 | plt.tight_layout() 104 | ax.set_xlim(min(wl_cell), max(wl_cell)) 105 | ax.set_ylim(0, 1.02) 106 | plt.show() 107 | 108 | 109 | fig, ax = plt.subplots(1,1, figsize=(5, 2)) 110 | ax.plot(wl_cell, SPD/max(SPD), '-', label="AM1.5G", color=pal[2]) 111 | ax.plot(wl_cell, SPD_bb/max(SPD_bb), '--', label="5778K black body", color="k") 112 | ax.set_xlabel("Wavelength (nm)") 113 | ax.set_ylabel("Normalised SPD") 114 | ax.grid(axis="both", color="0.9") 115 | ax.legend(loc="upper right") 116 | plt.tight_layout() 117 | ax.set_xlim(min(wl_cell), max(wl_cell)) 118 | ax.axvline(1240/1.13, linestyle='--', color=pal[0]) 119 | ax.axvline(1240/1.34, linestyle='--', color=pal[0]) 120 | ax.text(1240/1.13, 0.7, "1.13 eV", rotation=90, va='center', ha='right') 121 | ax.text(1240/1.34, 0.7, "1.34 eV", rotation=90, va='center', ha='right') 122 | ax.set_ylim(0, 1.02) 123 | plt.show() 124 | 125 | 126 | Macbeth = np.loadtxt(path.join(path.dirname(current_path), "ecopv", "data", 127 | "Macbeth_ColorChecker_R.csv"), delimiter=',', skiprows=1).T 128 | 129 | MacBeth_2 = np.loadtxt(path.join(path.dirname(current_path), "ecopv", "data", 130 | "ColorChecker_R_BabelColor.csv"), delimiter=',', encoding='utf-8-sig') 131 | 132 | wl_M = Macbeth[0] 133 | R_M = Macbeth[1:] 134 | 135 | wl_M_2 = MacBeth_2[0] 136 | R_M_2 = MacBeth_2[1:] 137 | 138 | cmf_M = load_cmf(wl_M_2) 139 | 140 | AM15M = np.array( 141 | LightSource( 142 | source_type="standard", 143 | version="AM1.5g", 144 | x=wl_cell, 145 | output_units="power_density_per_nm", 146 | ).spectrum(wl_M_2)[1]) 147 | 148 | XYZ_R = np.zeros((len(R_M_2), 3)) 149 | 150 | color_names, _ = load_colorchecker() 151 | 152 | fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2,2, figsize=(8, 7)) 153 | 154 | for i1, R in enumerate(R_M_2): 155 | XYZ_R[i1] = spec_to_XYZ(R, AM15M, cmf_M, 5) 156 | 157 | color_xyz_f = XYZColor(*XYZ_R[i1]) 158 | color_srgb_f = convert_color(color_xyz_f, sRGBColor, 159 | target_illuminant="d65") 160 | # d65 is native illuminant of sRGB (but actually specifying illuminant doesn't do 161 | # anything...) 162 | print(color_srgb_f) 163 | 164 | color_srgb_f = [ 165 | color_srgb_f.clamped_rgb_r, 166 | color_srgb_f.clamped_rgb_g, 167 | color_srgb_f.clamped_rgb_b, 168 | ] 169 | 170 | if i1 < 6: 171 | # ax1.plot(wl_M, R, color=color_srgb_f, label=color_names[i1]) 172 | ax1.plot(wl_M_2, R_M_2[i1], color=color_srgb_f, label=color_names[i1]) 173 | 174 | if i1 >= 6 and i1 < 12: 175 | # ax2.plot(wl_M, R, color=color_srgb_f, label=color_names[i1]) 176 | ax2.plot(wl_M_2, R_M_2[i1], color=color_srgb_f, label=color_names[i1]) 177 | 178 | if i1 >= 12 and i1 < 18: 179 | # ax3.plot(wl_M, R, color=color_srgb_f, label=color_names[i1]) 180 | ax3.plot(wl_M_2, R_M_2[i1], color=color_srgb_f, label=color_names[i1]) 181 | 182 | if i1 >= 18 and i1 < 24: 183 | # ax4.plot(wl_M, R, color=color_srgb_f, label=color_names[i1]) 184 | ax4.plot(wl_M_2, R_M_2[i1], color=color_srgb_f, label=color_names[i1]) 185 | 186 | for ax in [ax1, ax2, ax3, ax4]: 187 | ax.set_xlim(380, 730) 188 | ax.set_ylim(0, 1) 189 | ax.grid("both") 190 | ax.legend(fontsize=8) 191 | 192 | for ax in [ax1, ax3]: 193 | ax.set_ylabel("Reflectance") 194 | 195 | for ax in [ax3, ax4]: 196 | ax.set_xlabel("Wavelength (nm)") 197 | 198 | plt.show() 199 | 200 | np.savetxt(path.join(path.dirname(current_path), "ecopv", "data", 201 | "Macbeth_XYZ_from_R.txt"), XYZ_R) 202 | -------------------------------------------------------------------------------- /ecopv/plot_utilities.py: -------------------------------------------------------------------------------- 1 | import matplotlib.ticker as tck 2 | from ecopv.main_optimization import load_colorchecker 3 | from matplotlib.patches import Rectangle 4 | from matplotlib import rc 5 | import numpy as np 6 | from typing import Sequence 7 | import matplotlib.pyplot as plt 8 | 9 | from colormath.color_objects import sRGBColor, XYZColor 10 | from colormath.color_conversions import convert_color 11 | import xarray as xr 12 | 13 | rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"]}) 14 | 15 | patch_width = 0.7 16 | 17 | color_names, color_XYZ = load_colorchecker() 18 | 19 | Y = np.hstack((color_XYZ[:, 1], [0])) 20 | default_Y_cols = Y[:18] 21 | 22 | col_names_default = xr.DataArray( 23 | data=color_names[:18], dims=["Y"], coords={"Y": default_Y_cols} 24 | ) 25 | col_names_default = col_names_default.sortby("Y", ascending=False) 26 | 27 | color_XYZ_xr = xr.DataArray( 28 | color_XYZ[:18], 29 | dims=["color", "XYZ"], 30 | coords={"color": color_XYZ[:18, 1], "XYZ": ["X", "Y", "Z"]}, 31 | ) 32 | 33 | color_XYZ_xr = color_XYZ_xr.sortby("color", ascending=False) 34 | color_XYZ_bw = xr.DataArray( 35 | color_XYZ[18:], 36 | dims=["color", "XYZ"], 37 | coords={"color": color_XYZ[18:, 1], "XYZ": ["X", "Y", "Z"]}, 38 | ) 39 | color_XYZ_xr = xr.concat([color_XYZ_xr, color_XYZ_bw], dim="color") 40 | 41 | 42 | def wavelength_to_rgb(wavelengths, gamma=0.8): 43 | """ 44 | Taken from http://www.noah.org/wiki/Wavelength_to_RGB_in_Python 45 | This converts a given wavelength of light to an 46 | approximate RGB color value. The wavelength must be given 47 | in nanometers in the range from 380 nm through 750 nm 48 | (789 THz through 400 THz). 49 | 50 | Based on code by Dan Bruton 51 | http://www.physics.sfasu.edu/astro/color/spectra.html 52 | Additionally alpha value set to 0.5 outside range 53 | 54 | :param wavelengths: list or array of wavelengths in nm 55 | :param gamma: gamma value for gamma correction 56 | """ 57 | 58 | RGBA = np.zeros((len(wavelengths), 4)) 59 | 60 | for wavelength in wavelengths: 61 | wavelength = float(wavelength) 62 | if wavelength >= 380 and wavelength <= 750: 63 | A = 1.0 64 | else: 65 | A = 0.5 66 | if wavelength < 380: 67 | wavelength = 380.0 68 | if wavelength > 750: 69 | wavelength = 750.0 70 | if 380 <= wavelength <= 440: 71 | attenuation = 0.3 + 0.7 * (wavelength - 380) / (440 - 380) 72 | R = ((-(wavelength - 440) / (440 - 380)) * attenuation) ** gamma 73 | G = 0.0 74 | B = (1.0 * attenuation) ** gamma 75 | elif 440 <= wavelength <= 490: 76 | R = 0.0 77 | G = ((wavelength - 440) / (490 - 440)) ** gamma 78 | B = 1.0 79 | elif 490 <= wavelength <= 510: 80 | R = 0.0 81 | G = 1.0 82 | B = (-(wavelength - 510) / (510 - 490)) ** gamma 83 | elif 510 <= wavelength <= 580: 84 | R = ((wavelength - 510) / (580 - 510)) ** gamma 85 | G = 1.0 86 | B = 0.0 87 | elif 580 <= wavelength <= 645: 88 | R = 1.0 89 | G = (-(wavelength - 645) / (645 - 580)) ** gamma 90 | B = 0.0 91 | elif 645 <= wavelength <= 750: 92 | attenuation = 0.3 + 0.7 * (750 - wavelength) / (750 - 645) 93 | R = (1.0 * attenuation) ** gamma 94 | G = 0.0 95 | B = 0.0 96 | else: 97 | R = 0.0 98 | G = 0.0 99 | B = 0.0 100 | 101 | RGBA[wavelengths == wavelength] = (R, G, B, A) 102 | 103 | return RGBA 104 | 105 | 106 | def add_colour_patches(ax, width, labels, color_XYZ=color_XYZ_xr, 107 | color_coords="XYZ"): 108 | # width is with an axis spacing of "1" for the x-axis colour labels 109 | 110 | ymin = ax.get_ylim()[0] 111 | ymax = ax.get_ylim()[1] 112 | h = ax.get_window_extent().height 113 | w = ax.get_window_extent().width 114 | 115 | h_p = width * w / len(labels) 116 | h_ax = h_p * (ymax - ymin) / h 117 | print("h", h_ax) 118 | 119 | for l1, lab in enumerate(labels): 120 | 121 | if lab != "Black": 122 | 123 | target = color_XYZ[l1] 124 | 125 | else: 126 | target = [0, 0, 0] 127 | 128 | if color_coords == "XYZ": 129 | 130 | color_xyz_t = XYZColor(*target) 131 | color_srgb_t = convert_color(color_xyz_t, sRGBColor, 132 | target_illuminant="d65")#.get_value_tuple() 133 | color_srgb_t = [ 134 | color_srgb_t.clamped_rgb_r, 135 | color_srgb_t.clamped_rgb_g, 136 | color_srgb_t.clamped_rgb_b, 137 | ] 138 | 139 | else: 140 | color_srgb_t = color_XYZ[l1] 141 | 142 | ax.add_patch( 143 | Rectangle( 144 | xy=(l1 - (width / 2), ymin), 145 | width=width, 146 | height=h_ax, 147 | facecolor=color_srgb_t, 148 | ) 149 | ) 150 | 151 | 152 | def apply_formatting(ax, color_labels=None, grid="both", n_colors=None): 153 | 154 | if n_colors is None: 155 | n_colors = len(color_labels) 156 | 157 | ax.yaxis.set_minor_locator(tck.AutoMinorLocator()) 158 | ax.grid(axis=grid, color="0.9") 159 | 160 | ax.xaxis.set_ticks(np.arange(0, n_colors)) 161 | ax.tick_params(direction="in", which="both", top=True, right=True) 162 | print(color_labels) 163 | 164 | if color_labels is not None: 165 | ax.set_xticklabels( 166 | color_labels, rotation=45, ha="right", rotation_mode="anchor" 167 | ) 168 | ax.tick_params(direction="inout", axis="x", top=True, length=7) 169 | 170 | else: 171 | ax.set_xticklabels([]) 172 | 173 | ax.set_xlim(-0.6, len(ax.get_xticks()) - 0.4) 174 | 175 | ax.set_axisbelow(True) 176 | 177 | 178 | def make_sorted_xr( 179 | arr, 180 | color_names, 181 | append_black=None, 182 | Y_cols=default_Y_cols, 183 | col_names_sorted=col_names_default, 184 | ): 185 | if arr.ndim == 1: 186 | dims = ["color"] 187 | 188 | else: 189 | dims = ["color", "n"] 190 | 191 | eff_xr_col = xr.DataArray(data=arr[:18], dims=dims, coords={"color": Y_cols}) 192 | 193 | eff_xr_col = eff_xr_col.sortby("color", ascending=False) 194 | eff_xr_col = eff_xr_col.assign_coords(color=col_names_sorted.data) 195 | 196 | if append_black is not None: 197 | eff_xr_bw = xr.DataArray( 198 | data=np.append(arr[18:], [append_black], axis=0), 199 | dims=dims, 200 | coords={"color": np.append(color_names[18:], "Black")}, 201 | ) 202 | 203 | else: 204 | eff_xr_bw = xr.DataArray( 205 | data=arr[18:], dims=dims, coords={"color": color_names[18:]} 206 | ) 207 | 208 | eff_xr = xr.concat([eff_xr_col, eff_xr_bw], dim="color") 209 | 210 | return eff_xr 211 | 212 | 213 | def sRGB_color_list(order="sorted"): 214 | 215 | _, XYZ_list = load_colorchecker(source="BabelColor", 216 | illuminant="AM1.5g", 217 | output_coords="XYZ") 218 | 219 | rgb = np.zeros((len(XYZ_list), 3)) 220 | 221 | for i1, XYZ in enumerate(XYZ_list): 222 | color_xyz_t = XYZColor(*XYZ) 223 | color_srgb_t = convert_color(color_xyz_t, sRGBColor, 224 | target_illuminant="d65")#.get_value_tuple() 225 | rgb[i1] = [ 226 | color_srgb_t.clamped_rgb_r, 227 | color_srgb_t.clamped_rgb_g, 228 | color_srgb_t.clamped_rgb_b, 229 | ] 230 | 231 | if order == "sorted": 232 | return rgb[np.argsort(color_XYZ[:, 1])] 233 | 234 | else: 235 | return rgb -------------------------------------------------------------------------------- /ecopv/data/CIE_std_illum_D65.csv: -------------------------------------------------------------------------------- 1 | 300,0.0341 2 | 301,0.36014 3 | 302,0.68618 4 | 303,1.01222 5 | 304,1.33826 6 | 305,1.6643 7 | 306,1.99034 8 | 307,2.31638 9 | 308,2.64242 10 | 309,2.96846 11 | 310,3.2945 12 | 311,4.98865 13 | 312,6.6828 14 | 313,8.37695 15 | 314,10.0711 16 | 315,11.7652 17 | 316,13.4594 18 | 317,15.1535 19 | 318,16.8477 20 | 319,18.5418 21 | 320,20.236 22 | 321,21.9177 23 | 322,23.5995 24 | 323,25.2812 25 | 324,26.963 26 | 325,28.6447 27 | 326,30.3265 28 | 327,32.0082 29 | 328,33.69 30 | 329,35.3717 31 | 330,37.0535 32 | 331,37.343 33 | 332,37.6326 34 | 333,37.9221 35 | 334,38.2116 36 | 335,38.5011 37 | 336,38.7907 38 | 337,39.0802 39 | 338,39.3697 40 | 339,39.6593 41 | 340,39.9488 42 | 341,40.4451 43 | 342,40.9414 44 | 343,41.4377 45 | 344,41.934 46 | 345,42.4302 47 | 346,42.9265 48 | 347,43.4228 49 | 348,43.9191 50 | 349,44.4154 51 | 350,44.9117 52 | 351,45.0844 53 | 352,45.257 54 | 353,45.4297 55 | 354,45.6023 56 | 355,45.775 57 | 356,45.9477 58 | 357,46.1203 59 | 358,46.293 60 | 359,46.4656 61 | 360,46.6383 62 | 361,47.1834 63 | 362,47.7285 64 | 363,48.2735 65 | 364,48.8186 66 | 365,49.3637 67 | 366,49.9088 68 | 367,50.4539 69 | 368,50.9989 70 | 369,51.544 71 | 370,52.0891 72 | 371,51.8777 73 | 372,51.6664 74 | 373,51.455 75 | 374,51.2437 76 | 375,51.0323 77 | 376,50.8209 78 | 377,50.6096 79 | 378,50.3982 80 | 379,50.1869 81 | 380,49.9755 82 | 381,50.4428 83 | 382,50.91 84 | 383,51.3773 85 | 384,51.8446 86 | 385,52.3118 87 | 386,52.7791 88 | 387,53.2464 89 | 388,53.7137 90 | 389,54.1809 91 | 390,54.6482 92 | 391,57.4589 93 | 392,60.2695 94 | 393,63.0802 95 | 394,65.8909 96 | 395,68.7015 97 | 396,71.5122 98 | 397,74.3229 99 | 398,77.1336 100 | 399,79.9442 101 | 400,82.7549 102 | 401,83.628 103 | 402,84.5011 104 | 403,85.3742 105 | 404,86.2473 106 | 405,87.1204 107 | 406,87.9936 108 | 407,88.8667 109 | 408,89.7398 110 | 409,90.6129 111 | 410,91.486 112 | 411,91.6806 113 | 412,91.8752 114 | 413,92.0697 115 | 414,92.2643 116 | 415,92.4589 117 | 416,92.6535 118 | 417,92.8481 119 | 418,93.0426 120 | 419,93.2372 121 | 420,93.4318 122 | 421,92.7568 123 | 422,92.0819 124 | 423,91.4069 125 | 424,90.732 126 | 425,90.057 127 | 426,89.3821 128 | 427,88.7071 129 | 428,88.0322 130 | 429,87.3572 131 | 430,86.6823 132 | 431,88.5006 133 | 432,90.3188 134 | 433,92.1371 135 | 434,93.9554 136 | 435,95.7736 137 | 436,97.5919 138 | 437,99.4102 139 | 438,101.228 140 | 439,103.047 141 | 440,104.865 142 | 441,106.079 143 | 442,107.294 144 | 443,108.508 145 | 444,109.722 146 | 445,110.936 147 | 446,112.151 148 | 447,113.365 149 | 448,114.579 150 | 449,115.794 151 | 450,117.008 152 | 451,117.088 153 | 452,117.169 154 | 453,117.249 155 | 454,117.33 156 | 455,117.41 157 | 456,117.49 158 | 457,117.571 159 | 458,117.651 160 | 459,117.732 161 | 460,117.812 162 | 461,117.517 163 | 462,117.222 164 | 463,116.927 165 | 464,116.632 166 | 465,116.336 167 | 466,116.041 168 | 467,115.746 169 | 468,115.451 170 | 469,115.156 171 | 470,114.861 172 | 471,114.967 173 | 472,115.073 174 | 473,115.18 175 | 474,115.286 176 | 475,115.392 177 | 476,115.498 178 | 477,115.604 179 | 478,115.711 180 | 479,115.817 181 | 480,115.923 182 | 481,115.212 183 | 482,114.501 184 | 483,113.789 185 | 484,113.078 186 | 485,112.367 187 | 486,111.656 188 | 487,110.945 189 | 488,110.233 190 | 489,109.522 191 | 490,108.811 192 | 491,108.865 193 | 492,108.92 194 | 493,108.974 195 | 494,109.028 196 | 495,109.082 197 | 496,109.137 198 | 497,109.191 199 | 498,109.245 200 | 499,109.3 201 | 500,109.354 202 | 501,109.199 203 | 502,109.044 204 | 503,108.888 205 | 504,108.733 206 | 505,108.578 207 | 506,108.423 208 | 507,108.268 209 | 508,108.112 210 | 509,107.957 211 | 510,107.802 212 | 511,107.501 213 | 512,107.2 214 | 513,106.898 215 | 514,106.597 216 | 515,106.296 217 | 516,105.995 218 | 517,105.694 219 | 518,105.392 220 | 519,105.091 221 | 520,104.79 222 | 521,105.08 223 | 522,105.37 224 | 523,105.66 225 | 524,105.95 226 | 525,106.239 227 | 526,106.529 228 | 527,106.819 229 | 528,107.109 230 | 529,107.399 231 | 530,107.689 232 | 531,107.361 233 | 532,107.032 234 | 533,106.704 235 | 534,106.375 236 | 535,106.047 237 | 536,105.719 238 | 537,105.39 239 | 538,105.062 240 | 539,104.733 241 | 540,104.405 242 | 541,104.369 243 | 542,104.333 244 | 543,104.297 245 | 544,104.261 246 | 545,104.225 247 | 546,104.19 248 | 547,104.154 249 | 548,104.118 250 | 549,104.082 251 | 550,104.046 252 | 551,103.641 253 | 552,103.237 254 | 553,102.832 255 | 554,102.428 256 | 555,102.023 257 | 556,101.618 258 | 557,101.214 259 | 558,100.809 260 | 559,100.405 261 | 560,100 262 | 561,99.6334 263 | 562,99.2668 264 | 563,98.9003 265 | 564,98.5337 266 | 565,98.1671 267 | 566,97.8005 268 | 567,97.4339 269 | 568,97.0674 270 | 569,96.7008 271 | 570,96.3342 272 | 571,96.2796 273 | 572,96.225 274 | 573,96.1703 275 | 574,96.1157 276 | 575,96.0611 277 | 576,96.0065 278 | 577,95.9519 279 | 578,95.8972 280 | 579,95.8426 281 | 580,95.788 282 | 581,95.0778 283 | 582,94.3675 284 | 583,93.6573 285 | 584,92.947 286 | 585,92.2368 287 | 586,91.5266 288 | 587,90.8163 289 | 588,90.1061 290 | 589,89.3958 291 | 590,88.6856 292 | 591,88.8177 293 | 592,88.9497 294 | 593,89.0818 295 | 594,89.2138 296 | 595,89.3459 297 | 596,89.478 298 | 597,89.61 299 | 598,89.7421 300 | 599,89.8741 301 | 600,90.0062 302 | 601,89.9655 303 | 602,89.9248 304 | 603,89.8841 305 | 604,89.8434 306 | 605,89.8026 307 | 606,89.7619 308 | 607,89.7212 309 | 608,89.6805 310 | 609,89.6398 311 | 610,89.5991 312 | 611,89.4091 313 | 612,89.219 314 | 613,89.029 315 | 614,88.8389 316 | 615,88.6489 317 | 616,88.4589 318 | 617,88.2688 319 | 618,88.0788 320 | 619,87.8887 321 | 620,87.6987 322 | 621,87.2577 323 | 622,86.8167 324 | 623,86.3757 325 | 624,85.9347 326 | 625,85.4936 327 | 626,85.0526 328 | 627,84.6116 329 | 628,84.1706 330 | 629,83.7296 331 | 630,83.2886 332 | 631,83.3297 333 | 632,83.3707 334 | 633,83.4118 335 | 634,83.4528 336 | 635,83.4939 337 | 636,83.535 338 | 637,83.576 339 | 638,83.6171 340 | 639,83.6581 341 | 640,83.6992 342 | 641,83.332 343 | 642,82.9647 344 | 643,82.5975 345 | 644,82.2302 346 | 645,81.863 347 | 646,81.4958 348 | 647,81.1285 349 | 648,80.7613 350 | 649,80.394 351 | 650,80.0268 352 | 651,80.0456 353 | 652,80.0644 354 | 653,80.0831 355 | 654,80.1019 356 | 655,80.1207 357 | 656,80.1395 358 | 657,80.1583 359 | 658,80.177 360 | 659,80.1958 361 | 660,80.2146 362 | 661,80.4209 363 | 662,80.6272 364 | 663,80.8336 365 | 664,81.0399 366 | 665,81.2462 367 | 666,81.4525 368 | 667,81.6588 369 | 668,81.8652 370 | 669,82.0715 371 | 670,82.2778 372 | 671,81.8784 373 | 672,81.4791 374 | 673,81.0797 375 | 674,80.6804 376 | 675,80.281 377 | 676,79.8816 378 | 677,79.4823 379 | 678,79.0829 380 | 679,78.6836 381 | 680,78.2842 382 | 681,77.4279 383 | 682,76.5716 384 | 683,75.7153 385 | 684,74.859 386 | 685,74.0027 387 | 686,73.1465 388 | 687,72.2902 389 | 688,71.4339 390 | 689,70.5776 391 | 690,69.7213 392 | 691,69.9101 393 | 692,70.0989 394 | 693,70.2876 395 | 694,70.4764 396 | 695,70.6652 397 | 696,70.854 398 | 697,71.0428 399 | 698,71.2315 400 | 699,71.4203 401 | 700,71.6091 402 | 701,71.8831 403 | 702,72.1571 404 | 703,72.4311 405 | 704,72.7051 406 | 705,72.979 407 | 706,73.253 408 | 707,73.527 409 | 708,73.801 410 | 709,74.075 411 | 710,74.349 412 | 711,73.0745 413 | 712,71.8 414 | 713,70.5255 415 | 714,69.251 416 | 715,67.9765 417 | 716,66.702 418 | 717,65.4275 419 | 718,64.153 420 | 719,62.8785 421 | 720,61.604 422 | 721,62.4322 423 | 722,63.2603 424 | 723,64.0885 425 | 724,64.9166 426 | 725,65.7448 427 | 726,66.573 428 | 727,67.4011 429 | 728,68.2293 430 | 729,69.0574 431 | 730,69.8856 432 | 731,70.4057 433 | 732,70.9259 434 | 733,71.446 435 | 734,71.9662 436 | 735,72.4863 437 | 736,73.0064 438 | 737,73.5266 439 | 738,74.0467 440 | 739,74.5669 441 | 740,75.087 442 | 741,73.9376 443 | 742,72.7881 444 | 743,71.6387 445 | 744,70.4893 446 | 745,69.3398 447 | 746,68.1904 448 | 747,67.041 449 | 748,65.8916 450 | 749,64.7421 451 | 750,63.5927 452 | 751,61.8752 453 | 752,60.1578 454 | 753,58.4403 455 | 754,56.7229 456 | 755,55.0054 457 | 756,53.288 458 | 757,51.5705 459 | 758,49.8531 460 | 759,48.1356 461 | 760,46.4182 462 | 761,48.4569 463 | 762,50.4956 464 | 763,52.5344 465 | 764,54.5731 466 | 765,56.6118 467 | 766,58.6505 468 | 767,60.6892 469 | 768,62.728 470 | 769,64.7667 471 | 770,66.8054 472 | 771,66.4631 473 | 772,66.1209 474 | 773,65.7786 475 | 774,65.4364 476 | 775,65.0941 477 | 776,64.7518 478 | 777,64.4096 479 | 778,64.0673 480 | 779,63.7251 481 | 780,63.3828 482 | 781,63.4749 483 | 782,63.567 484 | 783,63.6592 485 | 784,63.7513 486 | 785,63.8434 487 | 786,63.9355 488 | 787,64.0276 489 | 788,64.1198 490 | 789,64.2119 491 | 790,64.304 492 | 791,63.8188 493 | 792,63.3336 494 | 793,62.8484 495 | 794,62.3632 496 | 795,61.8779 497 | 796,61.3927 498 | 797,60.9075 499 | 798,60.4223 500 | 799,59.9371 501 | 800,59.4519 502 | 801,58.7026 503 | 802,57.9533 504 | 803,57.204 505 | 804,56.4547 506 | 805,55.7054 507 | 806,54.9562 508 | 807,54.2069 509 | 808,53.4576 510 | 809,52.7083 511 | 810,51.959 512 | 811,52.5072 513 | 812,53.0553 514 | 813,53.6035 515 | 814,54.1516 516 | 815,54.6998 517 | 816,55.248 518 | 817,55.7961 519 | 818,56.3443 520 | 819,56.8924 521 | 820,57.4406 522 | 821,57.7278 523 | 822,58.015 524 | 823,58.3022 525 | 824,58.5894 526 | 825,58.8765 527 | 826,59.1637 528 | 827,59.4509 529 | 828,59.7381 530 | 829,60.0253 531 | 830,60.3125 532 | -------------------------------------------------------------------------------- /ecopv/data/CIE_std_illum_D50.csv: -------------------------------------------------------------------------------- 1 | 300,0.01922 2 | 301,0.222348 3 | 302,0.425476 4 | 303,0.628604 5 | 304,0.831732 6 | 305,1.03486 7 | 306,1.23799 8 | 307,1.44112 9 | 308,1.64424 10 | 309,1.84737 11 | 310,2.0505 12 | 311,2.62329 13 | 312,3.19608 14 | 313,3.76887 15 | 314,4.34166 16 | 315,4.91445 17 | 316,5.48724 18 | 317,6.06003 19 | 318,6.63282 20 | 319,7.20561 21 | 320,7.7784 22 | 321,8.47531 23 | 322,9.17222 24 | 323,9.86913 25 | 324,10.566 26 | 325,11.263 27 | 326,11.9599 28 | 327,12.6568 29 | 328,13.3537 30 | 329,14.0506 31 | 330,14.7475 32 | 331,15.0676 33 | 332,15.3876 34 | 333,15.7076 35 | 334,16.0277 36 | 335,16.3478 37 | 336,16.6678 38 | 337,16.9878 39 | 338,17.3079 40 | 339,17.628 41 | 340,17.948 42 | 341,18.2542 43 | 342,18.5603 44 | 343,18.8665 45 | 344,19.1727 46 | 345,19.4788 47 | 346,19.785 48 | 347,20.0912 49 | 348,20.3974 50 | 349,20.7035 51 | 350,21.0097 52 | 351,21.3029 53 | 352,21.5961 54 | 353,21.8894 55 | 354,22.1826 56 | 355,22.4758 57 | 356,22.769 58 | 357,23.0622 59 | 358,23.3555 60 | 359,23.6487 61 | 360,23.9419 62 | 361,24.2438 63 | 362,24.5457 64 | 363,24.8475 65 | 364,25.1494 66 | 365,25.4513 67 | 366,25.7532 68 | 367,26.0551 69 | 368,26.3569 70 | 369,26.6588 71 | 370,26.9607 72 | 371,26.7134 73 | 372,26.4661 74 | 373,26.2187 75 | 374,25.9714 76 | 375,25.7241 77 | 376,25.4768 78 | 377,25.2295 79 | 378,24.9821 80 | 379,24.7348 81 | 380,24.4875 82 | 381,25.0258 83 | 382,25.5641 84 | 383,26.1024 85 | 384,26.6407 86 | 385,27.179 87 | 386,27.7174 88 | 387,28.2557 89 | 388,28.794 90 | 389,29.3323 91 | 390,29.8706 92 | 391,31.8144 93 | 392,33.7581 94 | 393,35.7018 95 | 394,37.6456 96 | 395,39.5894 97 | 396,41.5331 98 | 397,43.4768 99 | 398,45.4206 100 | 399,47.3644 101 | 400,49.3081 102 | 401,50.0286 103 | 402,50.749 104 | 403,51.4695 105 | 404,52.19 106 | 405,52.9104 107 | 406,53.6309 108 | 407,54.3514 109 | 408,55.0719 110 | 409,55.7923 111 | 410,56.5128 112 | 411,56.8649 113 | 412,57.217 114 | 413,57.5691 115 | 414,57.9212 116 | 415,58.2733 117 | 416,58.6254 118 | 417,58.9775 119 | 418,59.3296 120 | 419,59.6817 121 | 420,60.0338 122 | 421,59.8122 123 | 422,59.5905 124 | 423,59.3689 125 | 424,59.1473 126 | 425,58.9256 127 | 426,58.704 128 | 427,58.4824 129 | 428,58.2608 130 | 429,58.0391 131 | 430,57.8175 132 | 431,59.5182 133 | 432,61.219 134 | 433,62.9197 135 | 434,64.6205 136 | 435,66.3212 137 | 436,68.0219 138 | 437,69.7227 139 | 438,71.4234 140 | 439,73.1242 141 | 440,74.8249 142 | 441,76.0671 143 | 442,77.3094 144 | 443,78.5516 145 | 444,79.7938 146 | 445,81.036 147 | 446,82.2783 148 | 447,83.5205 149 | 448,84.7627 150 | 449,86.005 151 | 450,87.2472 152 | 451,87.5837 153 | 452,87.9202 154 | 453,88.2567 155 | 454,88.5932 156 | 455,88.9297 157 | 456,89.2662 158 | 457,89.6027 159 | 458,89.9392 160 | 459,90.2757 161 | 460,90.6122 162 | 461,90.6878 163 | 462,90.7634 164 | 463,90.839 165 | 464,90.9146 166 | 465,90.9902 167 | 466,91.0657 168 | 467,91.1413 169 | 468,91.2169 170 | 469,91.2925 171 | 470,91.3681 172 | 471,91.7421 173 | 472,92.1162 174 | 473,92.4902 175 | 474,92.8643 176 | 475,93.2383 177 | 476,93.6123 178 | 477,93.9864 179 | 478,94.3604 180 | 479,94.7345 181 | 480,95.1085 182 | 481,94.7939 183 | 482,94.4793 184 | 483,94.1648 185 | 484,93.8502 186 | 485,93.5356 187 | 486,93.221 188 | 487,92.9064 189 | 488,92.5919 190 | 489,92.2773 191 | 490,91.9627 192 | 491,92.3388 193 | 492,92.7149 194 | 493,93.091 195 | 494,93.4671 196 | 495,93.8432 197 | 496,94.2193 198 | 497,94.5954 199 | 498,94.9715 200 | 499,95.3476 201 | 500,95.7237 202 | 501,95.8127 203 | 502,95.9016 204 | 503,95.9906 205 | 504,96.0795 206 | 505,96.1685 207 | 506,96.2575 208 | 507,96.3464 209 | 508,96.4354 210 | 509,96.5243 211 | 510,96.6133 212 | 511,96.6649 213 | 512,96.7164 214 | 513,96.768 215 | 514,96.8196 216 | 515,96.8712 217 | 516,96.9227 218 | 517,96.9743 219 | 518,97.0259 220 | 519,97.0774 221 | 520,97.129 222 | 521,97.626 223 | 522,98.123 224 | 523,98.62 225 | 524,99.117 226 | 525,99.614 227 | 526,100.111 228 | 527,100.608 229 | 528,101.105 230 | 529,101.602 231 | 530,102.099 232 | 531,101.965 233 | 532,101.83 234 | 533,101.696 235 | 534,101.561 236 | 535,101.427 237 | 536,101.292 238 | 537,101.158 239 | 538,101.024 240 | 539,100.889 241 | 540,100.755 242 | 541,100.911 243 | 542,101.067 244 | 543,101.223 245 | 544,101.38 246 | 545,101.536 247 | 546,101.692 248 | 547,101.848 249 | 548,102.005 250 | 549,102.161 251 | 550,102.317 252 | 551,102.085 253 | 552,101.854 254 | 553,101.622 255 | 554,101.39 256 | 555,101.158 257 | 556,100.927 258 | 557,100.695 259 | 558,100.463 260 | 559,100.232 261 | 560,100 262 | 561,99.7735 263 | 562,99.547 264 | 563,99.3205 265 | 564,99.094 266 | 565,98.8675 267 | 566,98.641 268 | 567,98.4145 269 | 568,98.188 270 | 569,97.9615 271 | 570,97.735 272 | 571,97.8533 273 | 572,97.9716 274 | 573,98.0899 275 | 574,98.2082 276 | 575,98.3265 277 | 576,98.4448 278 | 577,98.5631 279 | 578,98.6814 280 | 579,98.7997 281 | 580,98.918 282 | 581,98.3761 283 | 582,97.8342 284 | 583,97.2922 285 | 584,96.7503 286 | 585,96.2084 287 | 586,95.6665 288 | 587,95.1246 289 | 588,94.5826 290 | 589,94.0407 291 | 590,93.4988 292 | 591,93.9177 293 | 592,94.3366 294 | 593,94.7555 295 | 594,95.1744 296 | 595,95.5933 297 | 596,96.0122 298 | 597,96.4311 299 | 598,96.85 300 | 599,97.2689 301 | 600,97.6878 302 | 601,97.8459 303 | 602,98.0041 304 | 603,98.1622 305 | 604,98.3203 306 | 605,98.4784 307 | 606,98.6366 308 | 607,98.7947 309 | 608,98.9528 310 | 609,99.111 311 | 610,99.2691 312 | 611,99.2463 313 | 612,99.2236 314 | 613,99.2008 315 | 614,99.1781 316 | 615,99.1553 317 | 616,99.1325 318 | 617,99.1098 319 | 618,99.087 320 | 619,99.0643 321 | 620,99.0415 322 | 621,98.7095 323 | 622,98.3776 324 | 623,98.0456 325 | 624,97.7136 326 | 625,97.3816 327 | 626,97.0497 328 | 627,96.7177 329 | 628,96.3857 330 | 629,96.0538 331 | 630,95.7218 332 | 631,96.0353 333 | 632,96.3489 334 | 633,96.6624 335 | 634,96.976 336 | 635,97.2895 337 | 636,97.603 338 | 637,97.9166 339 | 638,98.2301 340 | 639,98.5437 341 | 640,98.8572 342 | 641,98.5382 343 | 642,98.2192 344 | 643,97.9002 345 | 644,97.5812 346 | 645,97.2622 347 | 646,96.9432 348 | 647,96.6242 349 | 648,96.3052 350 | 649,95.9862 351 | 650,95.6672 352 | 651,95.9195 353 | 652,96.1717 354 | 653,96.424 355 | 654,96.6762 356 | 655,96.9285 357 | 656,97.1808 358 | 657,97.433 359 | 658,97.6853 360 | 659,97.9375 361 | 660,98.1898 362 | 661,98.6712 363 | 662,99.1525 364 | 663,99.6339 365 | 664,100.115 366 | 665,100.597 367 | 666,101.078 368 | 667,101.559 369 | 668,102.041 370 | 669,102.522 371 | 670,103.003 372 | 671,102.616 373 | 672,102.229 374 | 673,101.842 375 | 674,101.455 376 | 675,101.068 377 | 676,100.681 378 | 677,100.294 379 | 678,99.9071 380 | 679,99.52 381 | 680,99.133 382 | 681,97.9578 383 | 682,96.7826 384 | 683,95.6074 385 | 684,94.4322 386 | 685,93.257 387 | 686,92.0817 388 | 687,90.9065 389 | 688,89.7313 390 | 689,88.5561 391 | 690,87.3809 392 | 691,87.8032 393 | 692,88.2254 394 | 693,88.6477 395 | 694,89.0699 396 | 695,89.4922 397 | 696,89.9145 398 | 697,90.3367 399 | 698,90.759 400 | 699,91.1812 401 | 700,91.6035 402 | 701,91.732 403 | 702,91.8605 404 | 703,91.989 405 | 704,92.1175 406 | 705,92.246 407 | 706,92.3746 408 | 707,92.5031 409 | 708,92.6316 410 | 709,92.7601 411 | 710,92.8886 412 | 711,91.2852 413 | 712,89.6818 414 | 713,88.0783 415 | 714,86.4749 416 | 715,84.8715 417 | 716,83.2681 418 | 717,81.6647 419 | 718,80.0612 420 | 719,78.4578 421 | 720,76.8544 422 | 721,77.8201 423 | 722,78.7858 424 | 723,79.7514 425 | 724,80.7171 426 | 725,81.6828 427 | 726,82.6485 428 | 727,83.6142 429 | 728,84.5798 430 | 729,85.5455 431 | 730,86.5112 432 | 731,87.1181 433 | 732,87.7249 434 | 733,88.3318 435 | 734,88.9386 436 | 735,89.5455 437 | 736,90.1524 438 | 737,90.7592 439 | 738,91.3661 440 | 739,91.9729 441 | 740,92.5798 442 | 741,91.1448 443 | 742,89.7098 444 | 743,88.2748 445 | 744,86.8398 446 | 745,85.4048 447 | 746,83.9699 448 | 747,82.5349 449 | 748,81.0999 450 | 749,79.6649 451 | 750,78.2299 452 | 751,76.1761 453 | 752,74.1223 454 | 753,72.0685 455 | 754,70.0147 456 | 755,67.9608 457 | 756,65.907 458 | 757,63.8532 459 | 758,61.7994 460 | 759,59.7456 461 | 760,57.6918 462 | 761,60.2149 463 | 762,62.738 464 | 763,65.2612 465 | 764,67.7843 466 | 765,70.3074 467 | 766,72.8305 468 | 767,75.3536 469 | 768,77.8768 470 | 769,80.3999 471 | 770,82.923 472 | 771,82.4581 473 | 772,81.9932 474 | 773,81.5283 475 | 774,81.0634 476 | 775,80.5985 477 | 776,80.1336 478 | 777,79.6687 479 | 778,79.2038 480 | 779,78.7389 481 | 780,78.274 482 | 781,78.402 483 | 782,78.5301 484 | 783,78.6581 485 | 784,78.7862 486 | 785,78.9142 487 | 786,79.0422 488 | 787,79.1703 489 | 788,79.2983 490 | 789,79.4264 491 | 790,79.5544 492 | 791,78.9391 493 | 792,78.3238 494 | 793,77.7085 495 | 794,77.0932 496 | 795,76.478 497 | 796,75.8627 498 | 797,75.2474 499 | 798,74.6321 500 | 799,74.0168 501 | 800,73.4015 502 | 801,72.4534 503 | 802,71.5052 504 | 803,70.5571 505 | 804,69.609 506 | 805,68.6608 507 | 806,67.7127 508 | 807,66.7646 509 | 808,65.8165 510 | 809,64.8683 511 | 810,63.9202 512 | 811,64.6059 513 | 812,65.2916 514 | 813,65.9772 515 | 814,66.6629 516 | 815,67.3486 517 | 816,68.0343 518 | 817,68.72 519 | 818,69.4056 520 | 819,70.0913 521 | 820,70.777 522 | 821,71.1435 523 | 822,71.5099 524 | 823,71.8764 525 | 824,72.2429 526 | 825,72.6094 527 | 826,72.9758 528 | 827,73.3423 529 | 828,73.7088 530 | 829,74.0752 531 | 830,74.4417 532 | -------------------------------------------------------------------------------- /examples/gamut_luminance_colors.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import color_optimization_only 2 | from colormath.color_objects import xyYColor 3 | from solcore.light_source import LightSource 4 | from os import path 5 | from ecopv.plot_utilities import * 6 | from colour import wavelength_to_XYZ 7 | import os 8 | 9 | force_rerun = False 10 | 11 | Ys = [0.25, 0.5, 0.75] 12 | 13 | wl_vis = np.linspace(360, 780, 500) 14 | 15 | XYZ = wavelength_to_XYZ(wl_vis) 16 | 17 | sumXYZ = np.sum(XYZ, axis=1) 18 | 19 | xg = XYZ[:, 0] / sumXYZ 20 | yg = XYZ[:, 1] / sumXYZ 21 | 22 | # xs = np.arange(np.min(xg), np.max(xg), 0.01) 23 | # ys = np.arange(np.min(yg), np.max(yg), 0.01) 24 | 25 | xs = np.arange(np.min(xg), np.max(xg), 0.01) 26 | ys = np.arange(np.min(yg), np.max(yg), 0.01) 27 | 28 | is_inside = np.full((len(xs), len(ys)), False) 29 | 30 | peak = np.argmax(yg) 31 | 32 | left_edge = [xg[:peak], yg[:peak]] 33 | right_edge = [xg[peak:], yg[peak:]] 34 | 35 | # now check if the points are inside the gamut defined by the spectral locus 36 | 37 | for j, yc in enumerate(ys): 38 | left_y = np.argmin(np.abs(left_edge[1] - yc)) 39 | right_y = np.argmin(np.abs(right_edge[1] - yc)) 40 | 41 | left_x = left_edge[0][left_y] 42 | right_x = right_edge[0][right_y] 43 | is_inside[np.all((xs > left_x, xs < right_x), axis=0), j] = True 44 | 45 | # eliminate everything below the line of purples: 46 | 47 | # equation for line of purples: 48 | 49 | slope = (yg[-1] - yg[0]) / (xg[-1] - xg[0]) 50 | c = yg[0] - slope * xg[0] 51 | 52 | for j, yc in enumerate(ys): 53 | above = yc > slope * xs + c 54 | is_inside[:, j] = np.all((above, is_inside[:, j]), axis=0) 55 | 56 | 57 | col_thresh = 0.004 # for a wavelength interval of 0.1, minimum achievable color error will be (very rough estimate!) ~ 0.001. 58 | # This is the maximum allowed fractional error in X, Y, or Z colour coordinates. 59 | 60 | acceptable_eff_change = 1e-3 # how much can the efficiency (in %) change between iteration sets? Stop when have reached 61 | # col_thresh and efficiency change is less than this. 62 | 63 | n_trials = 10 # number of islands which will run concurrently in parallel 64 | interval = 0.1 # wavelength interval (in nm) 65 | 66 | n_peaks = 2 67 | 68 | initial_iters = 100 # number of initial evolutions for the archipelago 69 | add_iters = 100 # additional evolutions added each time if color threshold/convergence condition not met 70 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 71 | 72 | max_trials_col = ( 73 | 2 * add_iters 74 | ) # how many population evolutions happen before giving up if there are no populations 75 | # which meet the color threshold 76 | 77 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 78 | fixed_height = True # fixed height peaks (will be at the value of max_height) if True, or peak height is an optimization 79 | # variable if False 80 | light_source_name = "AM1.5g" 81 | 82 | max_height = ( 83 | 1 # maximum height of reflection peaks; fixed at this value of fixed_height = True 84 | ) 85 | base = 0 # baseline fixed reflection (fixed at this value for both fixed_height = True and False). 86 | 87 | wl_col = np.arange(380, 730.001, interval) 88 | 89 | # Use AM1.5G spectrum: 90 | light_source = LightSource( 91 | source_type="standard", 92 | version=light_source_name, 93 | x=np.arange(380, 730.001, interval), 94 | output_units="power_density_per_nm", 95 | ) 96 | 97 | spectral_power_density = np.array(light_source.spectrum(wl_col)) 98 | 99 | shapes = ["+", "o", "^", ".", "*", "v", "s", "x"] 100 | 101 | loop_n = 0 102 | 103 | # precalculate optimal bandgaps for junctions: 104 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 105 | 106 | 107 | if __name__ == "__main__": 108 | # Need this __main__ construction because otherwise the parallel running of the different islands (n_trials) may throw an error 109 | 110 | fig, axs = plt.subplots(1, len(Ys), figsize=(len(Ys) * 3.5, 4)) 111 | if len(Ys) == 1: 112 | axs = [axs] 113 | 114 | for i1, Y in enumerate(Ys): 115 | 116 | best_population = np.zeros((len(xs), len(ys), 4)) 117 | 118 | counter = 0 119 | 120 | col_possible_str = save_path + "/possible_colours_Y_{}.txt".format(Y) 121 | 122 | if os.path.exists(save_path + "/possible_colours_Y_{}.txt".format(Y)) \ 123 | and not force_rerun: 124 | is_possible = np.loadtxt(col_possible_str) 125 | deltaE = np.loadtxt( 126 | save_path + "/possible_colours_Y_{}.txt".format(Y) 127 | ) 128 | print("Possible colours:", np.sum(is_possible)) 129 | 130 | else: 131 | 132 | if i1 == 0: 133 | is_possible = np.full((len(xs), len(ys)), True) 134 | is_possible[is_inside == False] = False 135 | 136 | else: 137 | print("Load lower luminance data") 138 | col_possible_str_pr = ( 139 | save_path + "/possible_colours_Y_{}.txt".format(Ys[i1 - 1]) 140 | ) 141 | is_possible = np.loadtxt(col_possible_str_pr) 142 | 143 | # print(is_possible) 144 | deltaE = np.ones_like(is_possible, dtype=float) 145 | 146 | # for i1, n_junctions in enumerate(n_junc_loop): 147 | for j1, x in enumerate(xs): 148 | for k1, y in enumerate(ys): 149 | if is_inside[j1, k1] and is_possible[j1, k1]: 150 | XYZ = np.array( 151 | convert_color(xyYColor(x, y, Y), XYZColor).get_value_tuple() 152 | ) 153 | print(XYZ) 154 | 155 | prob = color_optimization_only() 156 | result = prob.run(XYZ, spectral_power_density, n_peaks, 40, 157 | 1000) 158 | 159 | if result[0] > col_thresh: 160 | print(counter, j1, k1, "Cannot make colour", result) 161 | is_possible[j1, k1] = False 162 | deltaE[j1, k1] = result[0] 163 | 164 | else: 165 | print(counter, j1, k1, "Can make colour", result) 166 | deltaE[j1, k1] = result[0] 167 | best_population[j1, k1] = result[1] 168 | 169 | counter += 1 170 | 171 | np.savetxt(col_possible_str, is_possible) 172 | np.savetxt( 173 | save_path + "/possible_colours_Y_{}_deltaE.txt".format(Y), deltaE 174 | ) 175 | np.save(save_path + "/possible_colours_Y_{}_populations.npy".format( 176 | Y), best_population 177 | ) 178 | 179 | print("sum:", np.sum(is_possible)) 180 | 181 | width = np.diff(xs)[0] 182 | height = np.diff(ys)[0] 183 | 184 | standard_illuminant = [0.3128, 0.3290, Y] 185 | XYZ = convert_color(xyYColor(*standard_illuminant), XYZColor) 186 | s_i_RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 187 | s_i_RGB[s_i_RGB > 1] = 1 188 | 189 | label_wls = np.arange(440, 660, 20) 190 | 191 | XYZlab = wavelength_to_XYZ(label_wls) 192 | 193 | sumXYZlab = np.sum(XYZlab, axis=1) 194 | 195 | xgl = XYZlab[:, 0] / sumXYZlab 196 | ygl = XYZlab[:, 1] / sumXYZlab 197 | 198 | tick_orig = np.zeros((len(label_wls), 2)) 199 | tick_dir = np.zeros((len(label_wls), 2)) 200 | # create ticks 201 | for m1, lwl in enumerate(label_wls): 202 | p0 = wavelength_to_XYZ(lwl) 203 | p1 = wavelength_to_XYZ(lwl - 1) 204 | p2 = wavelength_to_XYZ(lwl + 1) 205 | 206 | p0 = p0 / np.sum(p0) 207 | p1 = p1 / np.sum(p1) 208 | p2 = p2 / np.sum(p2) 209 | 210 | m = np.array([p2[0] - p1[0], p2[1] - p1[1]]) 211 | mp = np.array([-m[1], m[0]]) 212 | mp = mp / np.linalg.norm(mp) 213 | 214 | tick_orig[m1] = p0[:2] 215 | tick_dir[m1] = p0[:2] + 0.02 * mp 216 | 217 | ax = axs[i1] 218 | ax.set_aspect("equal") 219 | ax.set_facecolor(s_i_RGB) 220 | ax.plot(xg, yg, "k") 221 | ax.plot([xg[0], xg[-1]], [yg[0], yg[-1]], "k") 222 | 223 | for m1, lwl in enumerate(label_wls): 224 | ax.plot( 225 | [tick_orig[m1, 0], tick_dir[m1, 0]], 226 | [tick_orig[m1, 1], tick_dir[m1, 1]], 227 | "-k", 228 | ) 229 | 230 | if lwl > 520: 231 | ax.text(*tick_dir[m1], str(lwl)) 232 | 233 | elif lwl == 520: 234 | ax.text(*tick_dir[m1], str(lwl), horizontalalignment="center") 235 | 236 | else: 237 | ax.text( 238 | *tick_dir[m1], 239 | str(lwl), 240 | horizontalalignment="right", 241 | verticalalignment="center" 242 | ) 243 | 244 | ax.set_xlim(-0.09, 0.8) 245 | ax.set_ylim(-0.07, 0.9) 246 | ax.set_xlabel("x") 247 | ax.set_ylabel("y") 248 | 249 | for j1, x in enumerate(xs): 250 | for k1, y in enumerate(ys): 251 | 252 | if is_possible[j1, k1]: 253 | XYZ = convert_color(xyYColor(x, y, Y), XYZColor) 254 | RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 255 | 256 | RGB[RGB > 1] = 1 257 | ax.add_patch( 258 | Rectangle( 259 | xy=(x - width / 2, y - height / 2), 260 | width=width, 261 | height=height, 262 | facecolor=RGB, 263 | ) 264 | ) 265 | 266 | ax.set_title("Y = " + str(Y)) 267 | ax.yaxis.set_minor_locator(tck.AutoMinorLocator()) 268 | ax.xaxis.set_minor_locator(tck.AutoMinorLocator()) 269 | ax.grid(axis="both", color="0.4", alpha=0.5) 270 | ax.tick_params(direction="in", which="both", top=True, right=True) 271 | # ax.set_axisbelow(True) 272 | 273 | plt.tight_layout() 274 | plt.show() 275 | -------------------------------------------------------------------------------- /examples/plot_pareto_front.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import load_colorchecker, single_color_cell, color_function_mobj 2 | from ecopv.spectrum_functions import make_spectrum_ndip, spec_to_XYZ, load_cmf 3 | import numpy as np 4 | from solcore.light_source import LightSource 5 | import matplotlib.pyplot as plt 6 | from time import time 7 | import pygmo as pg 8 | from os import path 9 | from pygmo.core import fast_non_dominated_sorting 10 | from colormath.color_objects import sRGBColor, XYZColor 11 | from colormath.color_conversions import convert_color 12 | from copy import deepcopy 13 | from matplotlib import rc 14 | from solcore.constants import h, c 15 | 16 | rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"]}) 17 | 18 | # Use smaller population size than actual results for clarity! 19 | 20 | def plot_non_dominated_fronts(points, marker='o', comp=[0, 1], axes=None, 21 | color=None, linecolor='k', mfc='none', 22 | markersize=4, **kwargs): 23 | """ 24 | Plots the nondominated fronts of a set of points. Makes use of :class:`~pygmo.fast_non_dominated_sorting` to 25 | compute the non dominated fronts. 26 | """ 27 | 28 | 29 | if color is None: 30 | color = ['k']*len(points) 31 | 32 | if mfc == 'none': 33 | mfc = ['none']*len(points) 34 | 35 | 36 | fronts, _, _, _ = fast_non_dominated_sorting(points) 37 | 38 | # We define the colors of the fronts (grayscale from black to white) 39 | alpha = np.linspace(1, 0.1, len(fronts)) 40 | 41 | if axes is None: 42 | axes = plt.axes() 43 | 44 | for ndr, front in enumerate(fronts): 45 | 46 | # We plot the fronts 47 | # First compute the points coordinates 48 | x = [points[idx][comp[0]] for idx in front] 49 | y = [-100*points[idx][comp[1]] for idx in front] 50 | # Then sort them by the first objective 51 | tmp = [(a, b) for a, b in zip(x, y)] 52 | tmp = sorted(tmp, key=lambda k: k[0]) 53 | # Now plot using step 54 | axes.step([c[0] for c in tmp], [c[1] 55 | for c in tmp], 56 | color=linecolor, 57 | where='post', 58 | # alpha=alpha[ndr], 59 | alpha=0.5, 60 | linestyle='--') 61 | 62 | # We plot the points 63 | for idx in front: 64 | axes.plot(points[idx][comp[0]], -100*points[idx][ 65 | comp[1]], marker=marker, 66 | # alpha=alpha[ndr], 67 | color=color[idx], 68 | markersize=markersize, 69 | mfc=mfc[idx], **kwargs) 70 | 71 | return axes 72 | 73 | 74 | col_thresh = 0.004 # for a wavelength interval of 0.1, minimum achievable color error deltaXYZ will be ~ 0.001. 75 | # (deltaXYZ = maximum fractional error in X, Y, Z colour coordinates) 76 | acceptable_eff_change = ( 77 | 1e-4 # how much can the efficiency (in %) change between iteration sets? 78 | ) 79 | n_trials = 1 # number of islands which will run concurrently 80 | interval = 0.1 # wavelength interval (in nm) 81 | wl_cell = np.arange(280, 4000, interval) # wavelengths 82 | 83 | initial_iters = 100 # number of initial evolutions for the archipelago 84 | add_iters = 400 # additional evolutions added each time if color 85 | # threshold/convergence condition not met 86 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 87 | 88 | max_trials_col = ( 89 | 5 * add_iters 90 | ) # how many population evolutions happen before giving up if there are no populations 91 | # which meet the color threshold 92 | 93 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 94 | fixed_height = True # fixed height peaks (will be at the value of max_height) or not 95 | 96 | max_height = 1 # maximum height of reflection peaks 97 | base = 0 # baseline fixed reflection 98 | 99 | n_junctions = 1 # number of junctions in the cell 100 | 101 | n_peaks = 2 # number of reflection peaks 102 | 103 | color_names, color_XYZ = load_colorchecker() # load the 24 default ColorChecker colors 104 | color_names = [color_names[-6]]#, color_names[14]] 105 | color_XYZ = [color_XYZ[-6]]#, color_XYZ[14]] 106 | 107 | # Define the incident photon flux. This should be a 2D array with the first row being the wavelengths and the second row 108 | # being the photon flux at each wavelength. The wavelengths should be in nm and the photon flux in photons/m^2/s/nm. 109 | light_source = LightSource( 110 | source_type="standard", 111 | version="AM1.5g", 112 | x=wl_cell, 113 | output_units="photon_flux_per_nm", 114 | ) 115 | 116 | light_source_name = "AM1.5g" 117 | 118 | photon_flux_cell = np.array( 119 | light_source.spectrum(wl_cell) 120 | ) 121 | 122 | # Use only the visible range of wavelengths (380-780 nm) for color calculations: 123 | photon_flux_color = photon_flux_cell[ 124 | :, np.all((photon_flux_cell[0] >= 380, photon_flux_cell[0] <= 730), axis=0) 125 | ] 126 | 127 | wl_col = np.arange(380, 730, interval) 128 | 129 | cmf = load_cmf(wl_col) 130 | 131 | solar_spec_color = LightSource( 132 | source_type="standard", 133 | version="AM1.5g", 134 | x=wl_col, 135 | output_units="power_density_per_nm", 136 | ).spectrum(wl_col)[1] 137 | 138 | shapes = ["x", "o", "^", ".", "*", "v", "s", "+"] 139 | 140 | loop_n = 0 141 | 142 | illuminant = h*c*photon_flux_color[1]/ (wl_col * 1e-9) 143 | 144 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 145 | 146 | if __name__ == "__main__": 147 | 148 | placeholder_obj = make_spectrum_ndip( 149 | n_peaks=n_peaks, R_type=R_type, fixed_height=fixed_height 150 | ) 151 | n_params = placeholder_obj.n_spectrum_params + n_junctions 152 | pop_size = n_params * 10 153 | 154 | start = time() 155 | # Need this because otherwise the parallel running of the different islands (n_trials) may throw an error 156 | 157 | fig, ax = plt.subplots(1, figsize=(4,3.5)) 158 | 159 | # Run for the selected peak shape, with both fixed and non-fixed height 160 | Eg_black = np.loadtxt( 161 | save_path 162 | + "/champion_pop_{}juncs_{}spec.txt".format( 163 | n_junctions, light_source_name 164 | ), 165 | ndmin=1, 166 | ) 167 | 168 | x_vals_start = np.loadtxt("results/pareto_plot_pop.txt") 169 | 170 | RGB_initpop = np.zeros((len(x_vals_start), 3)) 171 | 172 | for i1, xs in enumerate(x_vals_start): 173 | spec = placeholder_obj.spectrum_function(xs, n_peaks=n_peaks, wl=wl_col) 174 | XYZ_finalpop = spec_to_XYZ(spec, solar_spec_color, cmf, interval) 175 | color_xyz_t = XYZColor(*XYZ_finalpop) 176 | RGB_initpop[i1, :] = convert_color(color_xyz_t, sRGBColor).get_value_tuple() 177 | 178 | RGB_initpop[RGB_initpop > 1] = 1 179 | 180 | RGB_finalpop = np.zeros((len(x_vals_start), 3)) 181 | 182 | for j1, target_col in enumerate(color_XYZ): 183 | 184 | spectrum_obj = make_spectrum_ndip( 185 | n_peaks=n_peaks, 186 | target=target_col, 187 | R_type=R_type, 188 | fixed_height=fixed_height, 189 | ) 190 | 191 | internal_run = single_color_cell( 192 | spectrum_function=spectrum_obj.spectrum_function 193 | ) 194 | 195 | p_init = color_function_mobj( 196 | n_peaks, 197 | n_junctions, 198 | target_col, 199 | photon_flux_cell, 200 | illuminant, 201 | spectrum_obj.spectrum_function, 202 | light_source.power_density, 203 | spectrum_obj.get_bounds(), 204 | Eg_black, 205 | ) 206 | 207 | udp = pg.problem(p_init) 208 | 209 | algo = pg.algorithm(pg.moead(gen=add_iters, CR=1, F=1, preserve_diversity=True)) 210 | 211 | archi = pg.archipelago(n=n_trials, algo=algo, prob=udp, pop_size=pop_size) 212 | 213 | for i1 in range(pop_size): 214 | archi[0].get_population().set_x(i1, x_vals_start[i1]) 215 | 216 | if j1 == 0: 217 | f_vals_start = archi[0].get_population().get_f() 218 | plot_non_dominated_fronts(f_vals_start, axes=ax, color=RGB_initpop, 219 | linecolor='k') 220 | 221 | archi = internal_run.run( 222 | target_col, 223 | photon_flux_cell, 224 | illuminant, 225 | n_peaks, 226 | n_junctions, 227 | pop_size, 228 | add_iters, 229 | n_trials=n_trials, 230 | power_in=light_source.power_density, 231 | spectrum_bounds=spectrum_obj.get_bounds(), 232 | Eg_black=Eg_black, 233 | archi=archi, 234 | base=base, 235 | max_height=max_height, 236 | ) 237 | 238 | f_vals = archi[0].get_population().get_f() 239 | x_vals = archi[0].get_population().get_x() 240 | 241 | # back-calculate colors: 242 | 243 | for i1, xs in enumerate(x_vals): 244 | spec = spectrum_obj.spectrum_function(xs, n_peaks=n_peaks, wl=wl_col) 245 | XYZ_finalpop = spec_to_XYZ(spec, solar_spec_color, cmf, interval) 246 | color_xyz_t = XYZColor(*XYZ_finalpop) 247 | RGB_finalpop[i1, :] = np.clip(convert_color(color_xyz_t, 248 | sRGBColor).get_value_tuple(), 249 | a_min=0, a_max=1) 250 | 251 | 252 | plot_non_dominated_fronts(f_vals, axes=ax, 253 | color=RGB_finalpop, 254 | mfc=RGB_finalpop, 255 | # linecolor=RGB_finalpop[-1], 256 | # linecolor=[0.8, 0.5, 0.5], 257 | markeredgewidth=0.5, 258 | markersize=6, 259 | ) 260 | 261 | 262 | ax.set_xlabel(r"Colour deviation, max(|$\Delta XYZ$|)") 263 | ax.set_ylabel("Cell efficiency (%)") 264 | 265 | left, bottom, width, height = [0.275, 0.8, 0.23, 0.15] 266 | ax2 = fig.add_axes([left, bottom, width, height]) 267 | 268 | ax2.set_facecolor((0.7, 0.7, 0.7)) 269 | 270 | f_vals_thresh = f_vals[f_vals[:, 0] < 0.051] 271 | RGB_finalpop_thresh = RGB_finalpop[f_vals[:, 0] < 0.051] 272 | 273 | plot_non_dominated_fronts(f_vals_thresh, axes=ax2, color=RGB_finalpop_thresh, 274 | mfc=RGB_finalpop_thresh, 275 | linecolor=RGB_finalpop_thresh[-1]) 276 | 277 | for label in (ax2.get_xticklabels() + ax2.get_yticklabels()): 278 | label.set_fontsize(8) 279 | 280 | ax.grid(axis="both") 281 | ax2.grid(axis="both") 282 | # ax.set_xlim(0, np.max(f_vals_start[:, 0] + 0.01)) 283 | ax.set_xlim(0, 1.02) 284 | # ax.set_ylim(-100*(np.max(f_vals_start[:, 1]) + 0.01), 34.3) 285 | ax.set_ylim(16, 34.3) 286 | ax2.set_xlim(-0.001, 0.051) 287 | ax2.set_ylim(23.5, 24.8) 288 | ax2.axvline(0.004, linestyle='--', color='k', alpha=0.6) 289 | ax.set_facecolor((0.98, 0.97, 0.95)) 290 | 291 | plt.tight_layout() 292 | plt.show() -------------------------------------------------------------------------------- /ecopv/data/Macbeth_ColorChecker_R.csv: -------------------------------------------------------------------------------- 1 | wavelength,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24 2 | 380,0.048,0.103,0.113,0.048,0.123,0.11,0.053,0.099,0.096,0.101,0.056,0.06,0.069,0.055,0.052,0.054,0.118,0.093,0.153,0.15,0.138,0.113,0.074,0.032 3 | 385,0.051,0.12,0.138,0.049,0.152,0.133,0.054,0.12,0.108,0.115,0.058,0.061,0.081,0.056,0.052,0.053,0.142,0.11,0.189,0.184,0.167,0.131,0.079,0.033 4 | 390,0.055,0.141,0.174,0.049,0.197,0.167,0.054,0.15,0.123,0.135,0.059,0.063,0.096,0.057,0.052,0.054,0.179,0.134,0.245,0.235,0.206,0.15,0.084,0.033 5 | 395,0.06,0.163,0.219,0.049,0.258,0.208,0.054,0.189,0.135,0.157,0.059,0.064,0.114,0.058,0.052,0.053,0.228,0.164,0.319,0.299,0.249,0.169,0.088,0.034 6 | 400,0.065,0.182,0.266,0.05,0.328,0.252,0.054,0.231,0.144,0.177,0.06,0.065,0.136,0.058,0.051,0.053,0.283,0.195,0.409,0.372,0.289,0.183,0.091,0.035 7 | 405,0.068,0.192,0.3,0.049,0.385,0.284,0.054,0.268,0.145,0.191,0.061,0.065,0.156,0.058,0.051,0.053,0.322,0.22,0.536,0.459,0.324,0.193,0.093,0.035 8 | 410,0.068,0.197,0.32,0.049,0.418,0.303,0.053,0.293,0.144,0.199,0.061,0.064,0.175,0.059,0.05,0.053,0.343,0.238,0.671,0.529,0.346,0.199,0.094,0.036 9 | 415,0.067,0.199,0.33,0.05,0.437,0.314,0.053,0.311,0.141,0.203,0.061,0.064,0.193,0.059,0.05,0.052,0.354,0.249,0.772,0.564,0.354,0.201,0.094,0.036 10 | 420,0.064,0.201,0.336,0.05,0.446,0.322,0.052,0.324,0.138,0.206,0.062,0.064,0.208,0.059,0.049,0.052,0.359,0.258,0.84,0.58,0.357,0.202,0.094,0.036 11 | 425,0.062,0.203,0.337,0.051,0.448,0.329,0.052,0.335,0.134,0.198,0.063,0.064,0.224,0.06,0.049,0.052,0.357,0.27,0.868,0.584,0.358,0.203,0.094,0.036 12 | 430,0.059,0.205,0.337,0.052,0.448,0.336,0.052,0.348,0.132,0.19,0.064,0.064,0.244,0.062,0.049,0.053,0.35,0.281,0.878,0.585,0.359,0.203,0.094,0.036 13 | 435,0.057,0.208,0.337,0.053,0.447,0.344,0.052,0.361,0.132,0.179,0.066,0.065,0.265,0.063,0.049,0.053,0.339,0.296,0.882,0.587,0.36,0.204,0.095,0.036 14 | 440,0.055,0.212,0.335,0.054,0.444,0.353,0.052,0.373,0.131,0.168,0.068,0.065,0.29,0.065,0.049,0.053,0.327,0.315,0.883,0.587,0.361,0.205,0.095,0.035 15 | 445,0.054,0.217,0.334,0.056,0.44,0.363,0.052,0.383,0.131,0.156,0.071,0.066,0.316,0.067,0.049,0.054,0.313,0.334,0.885,0.588,0.362,0.205,0.095,0.035 16 | 450,0.053,0.224,0.331,0.058,0.434,0.375,0.052,0.387,0.129,0.144,0.075,0.067,0.335,0.07,0.049,0.055,0.298,0.352,0.886,0.588,0.362,0.205,0.095,0.035 17 | 455,0.053,0.231,0.327,0.06,0.428,0.39,0.052,0.383,0.128,0.132,0.079,0.068,0.342,0.074,0.048,0.056,0.282,0.37,0.886,0.587,0.361,0.205,0.094,0.035 18 | 460,0.052,0.24,0.322,0.061,0.421,0.408,0.052,0.374,0.126,0.12,0.085,0.069,0.338,0.078,0.048,0.059,0.267,0.391,0.887,0.586,0.361,0.204,0.094,0.035 19 | 465,0.052,0.251,0.316,0.063,0.413,0.433,0.052,0.361,0.126,0.11,0.093,0.073,0.324,0.084,0.047,0.065,0.253,0.414,0.888,0.585,0.359,0.204,0.094,0.035 20 | 470,0.052,0.262,0.31,0.064,0.405,0.46,0.053,0.345,0.125,0.101,0.104,0.077,0.302,0.091,0.047,0.075,0.239,0.434,0.888,0.583,0.358,0.203,0.094,0.035 21 | 475,0.053,0.273,0.302,0.065,0.394,0.492,0.054,0.325,0.123,0.093,0.118,0.084,0.273,0.101,0.046,0.093,0.225,0.449,0.888,0.582,0.358,0.203,0.093,0.035 22 | 480,0.054,0.282,0.293,0.067,0.381,0.523,0.055,0.301,0.119,0.086,0.135,0.092,0.239,0.113,0.045,0.121,0.209,0.458,0.888,0.581,0.357,0.202,0.093,0.034 23 | 485,0.055,0.289,0.285,0.068,0.372,0.548,0.056,0.275,0.114,0.08,0.157,0.1,0.205,0.125,0.045,0.157,0.195,0.461,0.888,0.58,0.356,0.202,0.093,0.034 24 | 490,0.057,0.293,0.276,0.07,0.362,0.566,0.057,0.247,0.109,0.075,0.185,0.107,0.172,0.14,0.044,0.202,0.182,0.457,0.888,0.58,0.356,0.202,0.093,0.034 25 | 495,0.059,0.296,0.268,0.072,0.352,0.577,0.059,0.223,0.105,0.07,0.221,0.115,0.144,0.157,0.044,0.252,0.172,0.447,0.888,0.58,0.356,0.202,0.092,0.034 26 | 500,0.061,0.301,0.26,0.078,0.342,0.582,0.061,0.202,0.103,0.067,0.269,0.123,0.12,0.18,0.044,0.303,0.163,0.433,0.887,0.58,0.356,0.202,0.092,0.034 27 | 505,0.062,0.31,0.251,0.088,0.33,0.583,0.064,0.184,0.102,0.063,0.326,0.133,0.101,0.208,0.044,0.351,0.155,0.414,0.887,0.58,0.356,0.202,0.093,0.034 28 | 510,0.065,0.321,0.243,0.106,0.314,0.58,0.068,0.167,0.1,0.061,0.384,0.146,0.086,0.244,0.044,0.394,0.146,0.392,0.887,0.58,0.356,0.202,0.093,0.034 29 | 515,0.067,0.326,0.234,0.13,0.294,0.576,0.076,0.152,0.097,0.059,0.44,0.166,0.074,0.286,0.044,0.436,0.135,0.366,0.887,0.581,0.356,0.202,0.093,0.034 30 | 520,0.07,0.322,0.225,0.155,0.271,0.569,0.086,0.137,0.094,0.058,0.484,0.193,0.066,0.324,0.044,0.475,0.124,0.339,0.887,0.581,0.357,0.202,0.093,0.034 31 | 525,0.072,0.31,0.215,0.173,0.249,0.56,0.101,0.125,0.091,0.056,0.516,0.229,0.059,0.351,0.044,0.512,0.113,0.31,0.887,0.582,0.357,0.202,0.093,0.034 32 | 530,0.074,0.298,0.208,0.181,0.231,0.549,0.12,0.116,0.089,0.054,0.534,0.273,0.054,0.363,0.044,0.544,0.106,0.282,0.887,0.582,0.357,0.203,0.093,0.034 33 | 535,0.075,0.291,0.203,0.182,0.219,0.535,0.143,0.11,0.09,0.053,0.542,0.323,0.051,0.363,0.044,0.572,0.102,0.255,0.887,0.582,0.358,0.203,0.093,0.034 34 | 540,0.076,0.292,0.198,0.177,0.211,0.519,0.17,0.106,0.092,0.052,0.545,0.374,0.048,0.355,0.045,0.597,0.102,0.228,0.887,0.583,0.358,0.203,0.093,0.034 35 | 545,0.078,0.297,0.195,0.168,0.209,0.501,0.198,0.103,0.096,0.052,0.541,0.418,0.046,0.342,0.046,0.615,0.105,0.204,0.886,0.583,0.358,0.203,0.093,0.034 36 | 550,0.079,0.3,0.191,0.157,0.209,0.48,0.228,0.099,0.102,0.053,0.533,0.456,0.045,0.323,0.047,0.63,0.107,0.18,0.886,0.583,0.358,0.203,0.093,0.034 37 | 555,0.082,0.298,0.188,0.147,0.207,0.458,0.26,0.094,0.106,0.054,0.524,0.487,0.044,0.303,0.048,0.645,0.107,0.159,0.887,0.584,0.358,0.203,0.092,0.034 38 | 560,0.087,0.295,0.183,0.137,0.201,0.436,0.297,0.09,0.108,0.055,0.513,0.512,0.043,0.281,0.05,0.66,0.106,0.141,0.887,0.584,0.359,0.203,0.093,0.033 39 | 565,0.092,0.295,0.177,0.129,0.196,0.414,0.338,0.086,0.109,0.055,0.501,0.534,0.042,0.26,0.053,0.673,0.107,0.126,0.887,0.585,0.359,0.203,0.093,0.033 40 | 570,0.1,0.305,0.172,0.126,0.196,0.392,0.38,0.083,0.112,0.054,0.487,0.554,0.041,0.238,0.057,0.686,0.112,0.114,0.888,0.586,0.36,0.204,0.093,0.033 41 | 575,0.107,0.326,0.167,0.125,0.199,0.369,0.418,0.083,0.126,0.053,0.472,0.57,0.041,0.217,0.063,0.698,0.123,0.104,0.888,0.587,0.361,0.204,0.093,0.033 42 | 580,0.115,0.358,0.163,0.122,0.206,0.346,0.452,0.083,0.157,0.052,0.454,0.584,0.04,0.196,0.072,0.708,0.141,0.097,0.887,0.588,0.361,0.205,0.093,0.033 43 | 585,0.122,0.397,0.16,0.119,0.215,0.324,0.481,0.085,0.208,0.052,0.436,0.598,0.04,0.177,0.086,0.718,0.166,0.092,0.886,0.588,0.361,0.205,0.093,0.033 44 | 590,0.129,0.435,0.157,0.115,0.223,0.302,0.503,0.086,0.274,0.053,0.416,0.609,0.04,0.158,0.109,0.726,0.198,0.088,0.886,0.588,0.361,0.205,0.093,0.033 45 | 595,0.134,0.468,0.153,0.109,0.229,0.279,0.52,0.087,0.346,0.055,0.394,0.617,0.04,0.14,0.143,0.732,0.235,0.083,0.886,0.588,0.361,0.205,0.092,0.033 46 | 600,0.138,0.494,0.15,0.104,0.235,0.26,0.532,0.087,0.415,0.059,0.374,0.624,0.039,0.124,0.192,0.737,0.279,0.08,0.887,0.588,0.36,0.204,0.092,0.033 47 | 605,0.142,0.514,0.147,0.1,0.241,0.245,0.543,0.086,0.473,0.065,0.358,0.63,0.039,0.111,0.256,0.742,0.333,0.077,0.888,0.587,0.36,0.204,0.092,0.033 48 | 610,0.146,0.53,0.144,0.098,0.245,0.234,0.552,0.085,0.517,0.074,0.346,0.635,0.04,0.101,0.332,0.746,0.394,0.075,0.889,0.586,0.359,0.204,0.092,0.033 49 | 615,0.15,0.541,0.141,0.097,0.245,0.226,0.56,0.084,0.547,0.086,0.337,0.64,0.04,0.094,0.413,0.749,0.46,0.074,0.89,0.586,0.358,0.203,0.091,0.033 50 | 620,0.154,0.55,0.137,0.098,0.243,0.221,0.566,0.084,0.567,0.099,0.331,0.645,0.04,0.089,0.486,0.753,0.522,0.073,0.891,0.585,0.357,0.203,0.091,0.033 51 | 625,0.158,0.557,0.133,0.1,0.243,0.217,0.572,0.085,0.582,0.113,0.328,0.65,0.04,0.086,0.55,0.757,0.58,0.073,0.891,0.584,0.356,0.202,0.091,0.033 52 | 630,0.163,0.564,0.13,0.1,0.247,0.215,0.578,0.088,0.591,0.126,0.325,0.654,0.041,0.084,0.598,0.761,0.628,0.073,0.891,0.583,0.355,0.201,0.09,0.033 53 | 635,0.167,0.569,0.126,0.099,0.254,0.212,0.583,0.092,0.597,0.138,0.322,0.658,0.041,0.082,0.631,0.765,0.666,0.073,0.891,0.581,0.354,0.201,0.09,0.033 54 | 640,0.173,0.574,0.123,0.097,0.269,0.21,0.587,0.098,0.601,0.149,0.32,0.662,0.042,0.08,0.654,0.768,0.696,0.073,0.89,0.58,0.353,0.2,0.09,0.033 55 | 645,0.18,0.582,0.12,0.096,0.291,0.209,0.593,0.105,0.604,0.161,0.319,0.667,0.042,0.078,0.672,0.772,0.722,0.073,0.889,0.579,0.352,0.199,0.09,0.033 56 | 650,0.188,0.59,0.118,0.095,0.318,0.208,0.599,0.111,0.607,0.172,0.319,0.672,0.042,0.077,0.686,0.777,0.742,0.074,0.889,0.578,0.351,0.198,0.089,0.033 57 | 655,0.196,0.597,0.115,0.095,0.351,0.209,0.602,0.118,0.608,0.182,0.32,0.675,0.043,0.076,0.694,0.779,0.756,0.075,0.889,0.577,0.35,0.198,0.089,0.033 58 | 660,0.204,0.605,0.112,0.095,0.384,0.211,0.604,0.123,0.607,0.193,0.324,0.676,0.043,0.075,0.7,0.78,0.766,0.076,0.889,0.576,0.349,0.197,0.089,0.033 59 | 665,0.213,0.614,0.11,0.097,0.417,0.215,0.606,0.126,0.606,0.205,0.33,0.677,0.043,0.075,0.704,0.78,0.774,0.076,0.889,0.575,0.348,0.197,0.088,0.033 60 | 670,0.222,0.624,0.108,0.101,0.446,0.22,0.608,0.126,0.605,0.217,0.337,0.678,0.044,0.075,0.707,0.781,0.78,0.077,0.888,0.574,0.346,0.196,0.088,0.033 61 | 675,0.231,0.637,0.106,0.11,0.47,0.227,0.611,0.124,0.605,0.232,0.345,0.681,0.044,0.077,0.712,0.782,0.785,0.076,0.888,0.573,0.346,0.195,0.088,0.033 62 | 680,0.242,0.652,0.105,0.125,0.49,0.233,0.615,0.12,0.605,0.248,0.354,0.685,0.044,0.078,0.718,0.785,0.791,0.075,0.888,0.572,0.345,0.195,0.087,0.033 63 | 685,0.251,0.668,0.104,0.147,0.504,0.239,0.619,0.117,0.604,0.266,0.362,0.688,0.044,0.08,0.721,0.785,0.794,0.074,0.888,0.571,0.344,0.194,0.087,0.033 64 | 690,0.261,0.682,0.104,0.174,0.511,0.244,0.622,0.115,0.605,0.282,0.368,0.69,0.045,0.082,0.724,0.787,0.798,0.074,0.888,0.57,0.343,0.194,0.087,0.032 65 | 695,0.271,0.697,0.103,0.21,0.517,0.249,0.625,0.115,0.606,0.301,0.375,0.693,0.046,0.085,0.727,0.789,0.801,0.073,0.888,0.569,0.342,0.193,0.087,0.032 66 | 700,0.282,0.713,0.103,0.247,0.52,0.252,0.628,0.116,0.606,0.319,0.379,0.696,0.048,0.088,0.729,0.792,0.804,0.072,0.888,0.568,0.341,0.192,0.086,0.032 67 | 705,0.294,0.728,0.102,0.283,0.522,0.252,0.63,0.118,0.604,0.338,0.381,0.698,0.05,0.089,0.73,0.792,0.806,0.072,0.887,0.567,0.34,0.192,0.086,0.032 68 | 710,0.305,0.745,0.102,0.311,0.523,0.25,0.633,0.12,0.602,0.355,0.379,0.698,0.051,0.089,0.73,0.793,0.807,0.071,0.886,0.566,0.339,0.191,0.086,0.032 69 | 715,0.318,0.753,0.102,0.329,0.522,0.248,0.633,0.124,0.601,0.371,0.376,0.698,0.053,0.09,0.729,0.792,0.807,0.073,0.886,0.565,0.338,0.191,0.086,0.032 70 | 720,0.334,0.762,0.102,0.343,0.521,0.244,0.633,0.128,0.599,0.388,0.373,0.698,0.056,0.09,0.727,0.79,0.807,0.075,0.886,0.564,0.337,0.19,0.085,0.032 71 | 725,0.354,0.774,0.102,0.353,0.521,0.245,0.636,0.133,0.598,0.406,0.372,0.7,0.06,0.09,0.728,0.792,0.81,0.078,0.885,0.562,0.336,0.189,0.085,0.032 72 | 730,0.372,0.783,0.102,0.358,0.522,0.245,0.637,0.139,0.596,0.422,0.375,0.701,0.064,0.089,0.729,0.792,0.813,0.082,0.885,0.562,0.335,0.189,0.085,0.032 73 | 735,0.392,0.788,0.104,0.362,0.521,0.251,0.639,0.149,0.595,0.436,0.382,0.701,0.07,0.092,0.729,0.79,0.814,0.09,0.885,0.56,0.334,0.188,0.085,0.032 74 | 740,0.409,0.791,0.104,0.364,0.521,0.26,0.638,0.162,0.593,0.451,0.392,0.701,0.079,0.094,0.727,0.787,0.813,0.1,0.884,0.56,0.333,0.188,0.085,0.032 75 | 745,0.42,0.787,0.104,0.36,0.516,0.269,0.633,0.178,0.587,0.46,0.401,0.695,0.091,0.097,0.723,0.782,0.81,0.116,0.884,0.558,0.332,0.187,0.084,0.032 76 | 750,0.436,0.789,0.104,0.362,0.514,0.278,0.633,0.197,0.584,0.471,0.412,0.694,0.104,0.102,0.721,0.778,0.808,0.133,0.883,0.557,0.331,0.187,0.084,0.032 77 | 755,0.45,0.794,0.106,0.364,0.514,0.288,0.636,0.219,0.584,0.481,0.422,0.696,0.12,0.106,0.724,0.78,0.811,0.154,0.882,0.556,0.33,0.186,0.084,0.032 78 | 760,0.462,0.801,0.106,0.368,0.517,0.297,0.641,0.242,0.586,0.492,0.433,0.7,0.138,0.11,0.728,0.782,0.814,0.176,0.882,0.555,0.329,0.185,0.084,0.032 79 | 765,0.465,0.799,0.107,0.368,0.515,0.301,0.639,0.259,0.584,0.495,0.436,0.698,0.154,0.111,0.727,0.781,0.813,0.191,0.881,0.554,0.328,0.185,0.084,0.032 80 | 770,0.448,0.771,0.11,0.355,0.5,0.297,0.616,0.275,0.566,0.482,0.426,0.673,0.168,0.112,0.702,0.752,0.785,0.2,0.88,0.553,0.327,0.184,0.083,0.032 81 | 775,0.432,0.747,0.115,0.346,0.491,0.296,0.598,0.294,0.551,0.471,0.413,0.653,0.186,0.112,0.68,0.728,0.765,0.208,0.88,0.551,0.326,0.184,0.083,0.032 82 | 780,0.421,0.734,0.12,0.341,0.487,0.296,0.582,0.316,0.54,0.467,0.404,0.639,0.204,0.112,0.664,0.71,0.752,0.214,0.879,0.55,0.325,0.183,0.083,0.032 -------------------------------------------------------------------------------- /examples/run_cell_color_optim_loop_bb.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import ( 2 | load_colorchecker, 3 | multiple_color_cells, 4 | cell_optimization, 5 | ) 6 | import numpy as np 7 | from solcore.light_source import LightSource 8 | import matplotlib.pyplot as plt 9 | import pygmo as pg 10 | from os import path 11 | import pandas as pd 12 | 13 | force_rerun = True 14 | force_rerun_ideal = False 15 | include_minimum_effs = False 16 | include_seed_population = False 17 | 18 | col_thresh = 0.004 # for a wavelength interval of 0.1, minimum achievable color error will be (very rough estimate!) ~ 0.001. 19 | # This is the maximum allowed fractional error in X, Y, or Z colour coordinates. 20 | 21 | acceptable_eff_change = 1e-4 # how much can the efficiency (in %) change between iteration sets? Stop when have reached 22 | # col_thresh and efficiency change is less than this. 23 | 24 | n_trials = 10 # number of islands which will run concurrently 25 | interval = 0.1 # wavelength interval (in nm) 26 | wl_cell = np.arange( 27 | 150, 5000, interval 28 | ) # wavelengths used for cell calculations (range of wavelengths in AM1.5G solar 29 | # spectrum. For calculations relating to colour perception, only the visible range (380-780 nm) will be used. 30 | 31 | single_J_result = pd.read_csv("../ecopv/data/paper_colors.csv") 32 | 33 | initial_iters = 100 # number of initial evolutions for the archipelago 34 | add_iters = 100 # additional evolutions added each time if color threshold/convergence condition not met 35 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 36 | 37 | max_trials_col = 3 * add_iters 38 | # how many population evolutions happen before giving up if there are no populations 39 | # which meet the color threshold 40 | 41 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 42 | fixed_height = True # fixed height peaks (will be at the value of max_height) if True, or peak height is an optimization 43 | # variable if False 44 | light_source_name = "BB" 45 | j01_method = "perfect_R" 46 | 47 | max_height = 1 48 | # maximum height of reflection peaks; fixed at this value of if fixed_height = True 49 | 50 | base = 0 51 | # baseline fixed reflection (fixed at this value for both fixed_height = True and False). 52 | 53 | n_junc_loop = [1, 2, 3, 4, 5, 6] # loop through these numbers of junctions 54 | n_peak_loop = [2] # loop through these numbers of reflection peaks 55 | 56 | color_names, color_XYZ = load_colorchecker(illuminant="AM1.5g", output_coords="XYZ") 57 | # load the names and XYZ coordinates of the 24 default Babel colors 58 | start_ind = 0 59 | end_ind = len(color_names) 60 | color_names = color_names[start_ind:end_ind] 61 | color_XYZ = color_XYZ[start_ind:end_ind] 62 | 63 | # Use AM1.5G spectrum for cell calculations: 64 | light_source = LightSource( 65 | source_type="black body", 66 | x=wl_cell, 67 | output_units="photon_flux_per_nm", 68 | entendue="Sun", 69 | T=5778, 70 | ) 71 | 72 | photon_flux_cell = np.array(light_source.spectrum(wl_cell)) 73 | 74 | shapes = ["+", "o", "^", ".", "*", "v", "s", "x"] 75 | 76 | loop_n = 0 77 | 78 | # precalculate optimal bandgaps for junctions: 79 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 80 | 81 | for n_junctions in n_junc_loop: 82 | 83 | save_loc = save_path + "/champion_pop_{}juncs_{}spec.txt".format( 84 | n_junctions, light_source_name 85 | ) 86 | 87 | if not path.exists(save_loc) or force_rerun_ideal: 88 | 89 | p_init = cell_optimization( 90 | n_junctions, 91 | photon_flux_cell, 92 | power_in=light_source.power_density, 93 | eta_ext=1, 94 | Eg_limits=[0.4, 2.35] 95 | ) 96 | 97 | prob = pg.problem(p_init) 98 | algo = pg.algorithm( 99 | pg.de( 100 | gen=500*n_junctions, 101 | F=1, 102 | CR=1, 103 | ftol=0, 104 | xtol=0, 105 | ) 106 | ) 107 | 108 | pop = pg.population(prob, 30 * n_junctions) 109 | pop = algo.evolve(pop) 110 | 111 | champion_pop = np.sort(pop.champion_x) 112 | 113 | print(n_junctions, pop.problem.get_fevals()/(30*n_junctions), 114 | -pop.champion_f*100) 115 | 116 | np.savetxt( 117 | save_path 118 | + "/champion_pop_{}juncs_{}spec.txt".format( 119 | n_junctions, light_source_name 120 | ), 121 | champion_pop, 122 | ) 123 | 124 | 125 | if __name__ == "__main__": 126 | # Need this __main__ construction because otherwise the parallel running of the different islands (n_trials) may throw an error 127 | 128 | for n_peaks in n_peak_loop: 129 | for n_junctions in n_junc_loop: 130 | champion_bandgaps = np.zeros((len(color_names), n_junctions)) 131 | 132 | Eg_guess = np.loadtxt( 133 | save_path 134 | + "/champion_pop_{}juncs_{}spec.txt".format( 135 | n_junctions, light_source_name 136 | ), 137 | ndmin=1, 138 | ) 139 | 140 | save_loc = ( 141 | "results/champion_eff_" 142 | + R_type 143 | + str(n_peaks) 144 | + "_" 145 | + str(n_junctions) 146 | + "_" 147 | + str(fixed_height) 148 | + str(max_height) 149 | + "_" 150 | + str(base) 151 | + "_" 152 | + j01_method + light_source_name + ".txt" 153 | ) 154 | 155 | if not path.exists(save_loc) or force_rerun: 156 | 157 | print( 158 | n_peaks, 159 | "peaks,", 160 | n_junctions, 161 | "junctions,", 162 | "fixed height:", 163 | fixed_height, 164 | ) 165 | 166 | minimum_effs_file = "results/champion_eff_" + R_type + str(n_peaks) +\ 167 | "_" + str(n_junctions - 1) + "_" + \ 168 | str(fixed_height) + str(max_height) + "_" + \ 169 | str(base) + "_" + j01_method + light_source_name + ".txt" 170 | 171 | seed_pop_file = "results/champion_pop_" + R_type + str(n_peaks) +\ 172 | "_" + str(n_junctions - 1) + "_" + \ 173 | str(fixed_height) + str(max_height) + "_" + \ 174 | str(base) + "_" + j01_method + light_source_name + ".txt" 175 | 176 | 177 | if path.exists(minimum_effs_file) and include_minimum_effs: 178 | minimum_effs = np.loadtxt(minimum_effs_file)#[start_ind:end_ind] 179 | 180 | else: 181 | minimum_effs = np.zeros(len(color_names)) 182 | 183 | if path.exists(seed_pop_file) and include_seed_population: 184 | seed_pop = np.loadtxt(seed_pop_file)#[start_ind:end_ind] 185 | print("seeding population") 186 | # seed_pop = seed_pop[:, :(seed_pop.shape[1] - n_junctions + 1)] 187 | 188 | else: 189 | seed_pop = None 190 | 191 | print("minimum efficiencies:", minimum_effs) 192 | 193 | result = multiple_color_cells( 194 | color_XYZ, 195 | color_names, 196 | photon_flux_cell, 197 | n_peaks=n_peaks, 198 | n_junctions=n_junctions, 199 | R_type=R_type, 200 | fixed_height=fixed_height, 201 | n_trials=n_trials, 202 | initial_iters=initial_iters, 203 | add_iters=add_iters, 204 | col_thresh=col_thresh, 205 | acceptable_eff_change=acceptable_eff_change, 206 | max_trials_col=max_trials_col, 207 | base=base, 208 | max_height=max_height, 209 | Eg_black=Eg_guess, 210 | plot=False, 211 | power_in=light_source.power_density, 212 | return_archipelagos=True, 213 | j01_method=j01_method, 214 | minimum_eff=minimum_effs, 215 | seed_population=seed_pop, 216 | illuminant=light_source_name, 217 | ) 218 | 219 | champion_effs = result["champion_eff"] 220 | champion_pops = result["champion_pop"] 221 | champion_bandgaps = champion_pops[:, -n_junctions:] 222 | 223 | final_populations = result["archipelagos"] 224 | 225 | np.savetxt( 226 | "results/champion_eff_" 227 | + R_type 228 | + str(n_peaks) 229 | + "_" 230 | + str(n_junctions) 231 | + "_" 232 | + str(fixed_height) 233 | + str(max_height) 234 | + "_" 235 | + str(base) 236 | + "_" + j01_method + light_source_name + ".txt", 237 | champion_effs, 238 | ) 239 | np.savetxt( 240 | "results/champion_pop_" 241 | + R_type 242 | + str(n_peaks) 243 | + "_" 244 | + str(n_junctions) 245 | + "_" 246 | + str(fixed_height) 247 | + str(max_height) 248 | + "_" 249 | + str(base) 250 | + "_" + j01_method + light_source_name + ".txt", 251 | champion_pops, 252 | ) 253 | np.save( 254 | "results/final_pop_" 255 | + R_type 256 | + str(n_peaks) 257 | + "_" 258 | + str(n_junctions) 259 | + "_" 260 | + str(fixed_height) 261 | + str(max_height) 262 | + "_" 263 | + str(base) 264 | + "_" + j01_method + light_source_name + ".npy", 265 | final_populations, 266 | ) 267 | 268 | else: 269 | 270 | champion_effs = np.loadtxt( 271 | "results/champion_eff_" 272 | + R_type 273 | + str(n_peaks) 274 | + "_" 275 | + str(n_junctions) 276 | + "_" 277 | + str(fixed_height) 278 | + str(max_height) 279 | + "_" 280 | + str(base) 281 | + "_" + j01_method + light_source_name + ".txt", 282 | ) 283 | champion_pops = np.loadtxt( 284 | "results/champion_pop_" 285 | + R_type 286 | + str(n_peaks) 287 | + "_" 288 | + str(n_junctions) 289 | + "_" 290 | + str(fixed_height) 291 | + str(max_height) 292 | + "_" 293 | + str(base) 294 | + "_" + j01_method + light_source_name + ".txt", 295 | ) 296 | champion_bandgaps = champion_pops[:, -n_junctions:] 297 | 298 | 299 | plt.figure() 300 | plt.plot( 301 | color_names, 302 | champion_effs, 303 | marker=shapes[0], 304 | mfc="none", 305 | linestyle="none", 306 | label="Power density", 307 | ) 308 | 309 | plt.plot( 310 | color_names, single_J_result["eta"], "o", mfc="none", label="1J result" 311 | ) 312 | 313 | plt.xticks(rotation=45) 314 | plt.legend() 315 | plt.ylabel("Efficiency (%)") 316 | # plt.title("Pop:" + str(pop_size) + "Iters:" + str(n_iters) + "Time:" + str(time_taken)) 317 | plt.title("Peaks:" + str(n_peaks) + "Junctions:" + str(n_junctions)) 318 | plt.tight_layout() 319 | plt.show() 320 | 321 | plt.figure() 322 | plt.plot( 323 | color_names, 324 | champion_bandgaps, 325 | marker=shapes[0], 326 | mfc="none", 327 | linestyle="none", 328 | label="Power density", 329 | ) 330 | 331 | plt.plot( 332 | color_names, single_J_result["Eg"], "o", mfc="none", label="1J result" 333 | ) 334 | plt.xticks(rotation=45) 335 | plt.legend() 336 | plt.ylabel("Bandgap (eV)") 337 | plt.title("Peaks:" + str(n_peaks) + "Junctions:" + str(n_junctions)) 338 | plt.tight_layout() 339 | plt.show() 340 | -------------------------------------------------------------------------------- /examples/plot_pareto_front_animation.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import load_colorchecker, single_color_cell, color_function_mobj 2 | from ecopv.spectrum_functions import make_spectrum_ndip, spec_to_XYZ, load_cmf 3 | import numpy as np 4 | from solcore.light_source import LightSource 5 | import matplotlib.pyplot as plt 6 | from matplotlib.animation import FuncAnimation 7 | from time import time 8 | import pygmo as pg 9 | from os import path 10 | from pygmo.core import fast_non_dominated_sorting 11 | from colormath.color_objects import sRGBColor, XYZColor 12 | from colormath.color_conversions import convert_color 13 | from copy import deepcopy 14 | from matplotlib import rc 15 | from solcore.constants import h, c 16 | from imageio.v3 import imread 17 | from imageio import get_writer 18 | 19 | 20 | 21 | rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"], "size": 14}) 22 | 23 | # Use smaller population size than actual results for clarity! 24 | 25 | def plot_non_dominated_fronts(points, marker='o', comp=[0, 1], axes=None, 26 | color=None, linecolor='k', mfc='none', 27 | markersize=4, **kwargs): 28 | """ 29 | Plots the nondominated fronts of a set of points. Makes use of :class:`~pygmo.fast_non_dominated_sorting` to 30 | compute the non dominated fronts. 31 | """ 32 | 33 | 34 | if color is None: 35 | color = ['k']*len(points) 36 | 37 | if mfc == 'none': 38 | mfc = ['none']*len(points) 39 | 40 | 41 | fronts, _, _, _ = fast_non_dominated_sorting(points) 42 | 43 | # We define the colors of the fronts (grayscale from black to white) 44 | alpha = np.linspace(1, 0.1, len(fronts)) 45 | 46 | if axes is None: 47 | axes = plt.axes() 48 | 49 | for ndr, front in enumerate(fronts): 50 | 51 | # We plot the fronts 52 | # First compute the points coordinates 53 | x = [points[idx][comp[0]] for idx in front] 54 | y = [-100*points[idx][comp[1]] for idx in front] 55 | # Then sort them by the first objective 56 | tmp = [(a, b) for a, b in zip(x, y)] 57 | tmp = sorted(tmp, key=lambda k: k[0]) 58 | # Now plot using step 59 | axes.step([c[0] for c in tmp], [c[1] 60 | for c in tmp], 61 | color=linecolor, 62 | where='post', 63 | # alpha=alpha[ndr], 64 | alpha=0.5, 65 | linestyle='--') 66 | 67 | # We plot the points 68 | for idx in front: 69 | axes.plot(points[idx][comp[0]], -100*points[idx][ 70 | comp[1]], marker=marker, 71 | # alpha=alpha[ndr], 72 | color=color[idx], 73 | markersize=markersize, 74 | mfc=mfc[idx], **kwargs) 75 | 76 | return axes 77 | 78 | 79 | # function takes frame as an input 80 | 81 | col_thresh = 0.01 # for a wavelength interval of 0.1, minimum achievable color error 82 | # deltaXYZ will be ~ 0.001. 83 | # (deltaXYZ = maximum fractional error in X, Y, Z colour coordinates) 84 | acceptable_eff_change = ( 85 | 1e-4 # how much can the efficiency (in %) change between iteration sets? 86 | ) 87 | n_trials = 1 # number of islands which will run concurrently 88 | interval = 0.1 # wavelength interval (in nm) 89 | wl_cell = np.arange(280, 4000, interval) # wavelengths 90 | 91 | initial_iters = 100 # number of initial evolutions for the archipelago 92 | add_iters = 200 # additional evolutions added each time if color 93 | # threshold/convergence condition not met 94 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 95 | 96 | max_trials_col = ( 97 | 5 * add_iters 98 | ) # how many population evolutions happen before giving up if there are no populations 99 | # which meet the color threshold 100 | 101 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 102 | fixed_height = True # fixed height peaks (will be at the value of max_height) or not 103 | 104 | max_height = 1 # maximum height of reflection peaks 105 | base = 0 # baseline fixed reflection 106 | 107 | n_junctions = 1 # number of junctions in the cell 108 | 109 | n_peaks = 2 # number of reflection peaks 110 | 111 | color_names, color_XYZ = load_colorchecker() # load the 24 default ColorChecker colors 112 | # color_names = [color_names[5]]#, color_names[14]] 113 | # color_XYZ = [color_XYZ[5]]#, color_XYZ[14]] 114 | 115 | color_names = [color_names[-6]]#, color_names[14]] 116 | color_XYZ = [color_XYZ[-6]]#, color_XYZ[14]] 117 | 118 | # Define the incident photon flux. This should be a 2D array with the first row being the wavelengths and the second row 119 | # being the photon flux at each wavelength. The wavelengths should be in nm and the photon flux in photons/m^2/s/nm. 120 | light_source = LightSource( 121 | source_type="standard", 122 | version="AM1.5g", 123 | x=wl_cell, 124 | output_units="photon_flux_per_nm", 125 | ) 126 | 127 | light_source_name = "AM1.5g" 128 | 129 | photon_flux_cell = np.array( 130 | light_source.spectrum(wl_cell) 131 | ) 132 | 133 | # Use only the visible range of wavelengths (380-780 nm) for color calculations: 134 | photon_flux_color = photon_flux_cell[ 135 | :, np.all((photon_flux_cell[0] >= 380, photon_flux_cell[0] <= 730), axis=0) 136 | ] 137 | 138 | wl_col = np.arange(380, 730, interval) 139 | 140 | cmf = load_cmf(wl_col) 141 | 142 | solar_spec_color = LightSource( 143 | source_type="standard", 144 | version="AM1.5g", 145 | x=wl_col, 146 | output_units="power_density_per_nm", 147 | ).spectrum(wl_col)[1] 148 | 149 | shapes = ["x", "o", "^", ".", "*", "v", "s", "+"] 150 | 151 | loop_n = 0 152 | 153 | illuminant = h*c*photon_flux_color[1]/ (wl_col * 1e-9) 154 | 155 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 156 | 157 | 158 | 159 | placeholder_obj = make_spectrum_ndip( 160 | n_peaks=n_peaks, R_type=R_type, fixed_height=fixed_height 161 | ) 162 | n_params = placeholder_obj.n_spectrum_params + n_junctions 163 | pop_size = n_params * 10 164 | 165 | # Need this because otherwise the parallel running of the different islands (n_trials) may throw an error 166 | 167 | 168 | # Run for the selected peak shape, with both fixed and non-fixed height 169 | Eg_black = np.loadtxt( 170 | save_path 171 | + "/champion_pop_{}juncs_{}spec.txt".format( 172 | n_junctions, light_source_name 173 | ), 174 | ndmin=1, 175 | ) 176 | 177 | # x_vals_start = np.loadtxt("results/pareto_plot_pop.txt") 178 | 179 | # RGB_initpop = np.zeros((len(x_vals_start), 3)) 180 | 181 | # for i1, xs in enumerate(x_vals_start): 182 | # spec = placeholder_obj.spectrum_function(xs, n_peaks=n_peaks, wl=wl_col) 183 | # XYZ_finalpop = spec_to_XYZ(spec, solar_spec_color, cmf, interval) 184 | # color_xyz_t = XYZColor(*XYZ_finalpop) 185 | # RGB_initpop[i1, :] = convert_color(color_xyz_t, sRGBColor).get_value_tuple() 186 | 187 | # RGB_initpop[RGB_initpop > 1] = 1 188 | # 189 | # RGB_finalpop = np.zeros((len(x_vals_start), 3)) 190 | 191 | best_eff = [] 192 | overall_best_eff = [] 193 | 194 | j1 = 0 195 | target_col = color_XYZ[j1] 196 | 197 | if __name__ == "__main__": 198 | 199 | spectrum_obj = make_spectrum_ndip( 200 | n_peaks=n_peaks, 201 | target=target_col, 202 | R_type=R_type, 203 | fixed_height=fixed_height, 204 | ) 205 | 206 | internal_run = single_color_cell( 207 | spectrum_function=spectrum_obj.spectrum_function 208 | ) 209 | 210 | p_init = color_function_mobj( 211 | n_peaks, 212 | n_junctions, 213 | target_col, 214 | photon_flux_cell, 215 | illuminant, 216 | spectrum_obj.spectrum_function, 217 | light_source.power_density, 218 | spectrum_obj.get_bounds(), 219 | Eg_black, 220 | ) 221 | 222 | udp = pg.problem(p_init) 223 | 224 | algo = pg.algorithm(pg.moead(gen=1, CR=1, F=1, preserve_diversity=True)) 225 | 226 | archi = pg.archipelago(n=n_trials, algo=algo, prob=udp, pop_size=pop_size) 227 | 228 | # this doesn't work 229 | # for i1 in range(pop_size): 230 | # print("setting population") 231 | # print('i1', archi[0].get_population().get_x()[i1]) 232 | # archi[0].get_population().set_x(i1, x_vals_start[i1]) 233 | # print(archi[0].get_population().get_x()[i1]) 234 | 235 | 236 | # if j1 == 0: 237 | # f_vals_start = archi[0].get_population().get_f() 238 | # plot_non_dominated_fronts(f_vals_start, axes=ax, color=RGB_initpop, 239 | # linecolor='k') 240 | 241 | for k1 in range(add_iters): 242 | print("iteration", k1) 243 | f_vals = archi[0].get_population().get_f() 244 | x_vals = archi[0].get_population().get_x() 245 | 246 | RGB_finalpop = np.zeros((len(x_vals), 3)) 247 | # back-calculate colors: 248 | 249 | for i1, xs in enumerate(x_vals): 250 | spec = spectrum_obj.spectrum_function(xs, n_peaks=n_peaks, wl=wl_col) 251 | XYZ_finalpop = spec_to_XYZ(spec, solar_spec_color, cmf, interval) 252 | color_xyz_t = XYZColor(*XYZ_finalpop) 253 | RGB_finalpop[i1, :] = np.clip(convert_color(color_xyz_t, 254 | sRGBColor).get_value_tuple(), 255 | a_min=0, a_max=1) 256 | 257 | archi = internal_run.run( 258 | target_col, 259 | photon_flux_cell, 260 | illuminant, 261 | n_peaks, 262 | n_junctions, 263 | pop_size, 264 | gen=1, 265 | n_trials=n_trials, 266 | power_in=light_source.power_density, 267 | spectrum_bounds=spectrum_obj.get_bounds(), 268 | Eg_black=Eg_black, 269 | archi=archi, 270 | base=base, 271 | max_height=max_height, 272 | ) 273 | 274 | 275 | 276 | fig, (ax, ax2) = plt.subplots(1,2, figsize=(9, 4)) 277 | plot_non_dominated_fronts(f_vals, axes=ax, 278 | color=RGB_finalpop, 279 | mfc=RGB_finalpop, 280 | # linecolor=RGB_finalpop[-1], 281 | # linecolor=[0.8, 0.5, 0.5], 282 | markeredgewidth=0.5, 283 | markersize=6, 284 | ) 285 | 286 | 287 | ax.set_xlabel(r"Colour deviation, max(|$\Delta XYZ$|)") 288 | ax.set_ylabel("Cell efficiency (%)") 289 | 290 | # left, bottom, width, height = [0.275, 0.8, 0.23, 0.15] 291 | # ax2 = fig.add_axes([left, bottom, width, height]) 292 | # 293 | # ax2.set_facecolor((0.7, 0.7, 0.7)) 294 | # 295 | # f_vals_thresh = f_vals[f_vals[:, 0] < 0.051] 296 | # RGB_finalpop_thresh = RGB_finalpop[f_vals[:, 0] < 0.051] 297 | # 298 | # plot_non_dominated_fronts(f_vals_thresh, axes=ax2, color=RGB_finalpop_thresh, 299 | # mfc=RGB_finalpop_thresh, 300 | # linecolor=RGB_finalpop_thresh[-1]) 301 | # 302 | # for label in (ax2.get_xticklabels() + ax2.get_yticklabels()): 303 | # label.set_fontsize(8) 304 | 305 | ax.grid(axis="both") 306 | # ax2.grid(axis="both") 307 | # ax.set_xlim(0, np.max(f_vals_start[:, 0] + 0.01)) 308 | ax.set_xlim(0, 3.02) 309 | # ax.set_ylim(-100*(np.max(f_vals_start[:, 1]) + 0.01), 34.3) 310 | ax.set_ylim(16, 34.3) 311 | # ax2.set_xlim(-0.001, 0.051) 312 | # ax2.set_ylim(23.5, 24.8) 313 | # ax2.axvline(0.004, linestyle='--', color='k', alpha=0.6) 314 | ax.set_facecolor((0.98, 0.97, 0.95)) 315 | 316 | sln = f_vals[:, 0] < col_thresh 317 | 318 | if np.sum(sln) > 0: 319 | 320 | best_acc_ind = np.argmin(f_vals[sln, 1]) 321 | new_eff = -100*f_vals[sln, 1][best_acc_ind] 322 | if new_eff > best_eff[-1]: 323 | best_eff.append(new_eff) 324 | 325 | else: 326 | best_eff.append(best_eff[-1]) 327 | 328 | else: 329 | if k1 == 0: 330 | best_eff.append(0) 331 | else: 332 | best_eff.append(best_eff[-1]) 333 | 334 | overall_best_eff.append(-100*np.min(f_vals[:, 1])) 335 | 336 | # ax2.plot(best_eff, color='k', linewidth=1.5, label="Best white cell") 337 | ax2.plot(best_eff, color='k', linewidth=1.5, label="Best coloured cell") 338 | ax2.plot(overall_best_eff, 'k--', alpha=0.5, linewidth=1.5, label="Best black cell") 339 | ax2.plot(k1, best_eff[-1], 'ro') 340 | ax2.plot(k1, overall_best_eff[-1], 'ko', alpha=0.5) 341 | ax2.set_xlim(0, add_iters) 342 | ax2.set_ylim(16, 34.3) 343 | # ax2.legend(loc=[0.1, 0.6]) 344 | ax2.legend(loc="center right") 345 | 346 | ax2.set_xlabel("Iteration") 347 | ax2.set_ylabel("Efficiency (%)") 348 | 349 | plt.tight_layout() 350 | plt.savefig('results/' + str(k1) +'.png') 351 | 352 | n_frames = np.ones(add_iters) 353 | n_frames[0] = 20 354 | n_frames[-1] = 20 355 | 356 | with get_writer('mygif_bluishgreen.gif', mode='I') as writer: 357 | for k1 in range(add_iters): 358 | 359 | for _ in range(int(n_frames[k1])): 360 | 361 | image = imread('results/' + str(k1) +'.png') 362 | writer.append_data(image) -------------------------------------------------------------------------------- /ecopv/spectrum_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Sequence, Tuple 3 | import pathlib 4 | from scipy.interpolate import interp1d 5 | from os.path import join 6 | from solcore.constants import h, c 7 | 8 | from colormath.color_objects import ( 9 | LabColor, 10 | XYZColor, 11 | xyYColor, 12 | ) 13 | from colormath.color_conversions import convert_color 14 | 15 | current_path = pathlib.Path(__file__).parent.resolve() 16 | hc = h * c 17 | 18 | def load_cmf(wl: np.ndarray) -> np.ndarray: 19 | """Load the CIE 1931 2 degree standard observer color matching functions and interpolate them to the given wavelengths""" 20 | 21 | cmf = np.loadtxt(join(current_path, "data", "cmf.txt")) 22 | cmf_new = np.zeros((len(wl), 3)) 23 | 24 | intfunc = interp1d(cmf[:, 0], cmf[:, 1], fill_value=(0, 0), bounds_error=False) 25 | cmf_new[:, 0] = intfunc(wl) 26 | intfunc = interp1d(cmf[:, 0], cmf[:, 2], fill_value=(0, 0), bounds_error=False) 27 | cmf_new[:, 1] = intfunc(wl) 28 | intfunc = interp1d(cmf[:, 0], cmf[:, 3], fill_value=(0, 0), bounds_error=False) 29 | cmf_new[:, 2] = intfunc(wl) 30 | 31 | return cmf_new 32 | 33 | 34 | def convert_xyY_to_Lab(xyY_list: Sequence[float]) -> Tuple[float, float, float]: 35 | 36 | xyY = xyYColor(*xyY_list) 37 | lab = convert_color(xyY, LabColor) 38 | return lab.get_value_tuple() 39 | 40 | 41 | def convert_xyY_to_XYZ(xyY_list: Sequence[float]) -> Tuple[float, float, float]: 42 | 43 | xyY = xyYColor(*xyY_list) 44 | lab = convert_color(xyY, XYZColor) 45 | return lab.get_value_tuple() 46 | 47 | 48 | def convert_XYZ_to_Lab(XYZ_list: Sequence[float]) -> Tuple[float, float, float]: 49 | XYZ = XYZColor(*XYZ_list) 50 | lab = convert_color(XYZ, LabColor) 51 | return lab.get_value_tuple() 52 | 53 | 54 | def gen_spectrum_ndip( 55 | pop: np.ndarray, 56 | n_peaks: int, 57 | wl: np.ndarray, 58 | max_height: float = 1, 59 | base: float = 0, 60 | ) -> np.ndarray: # center and width in nm 61 | """Generate a reflection spectrum with n_peaks rectangular peaks of fixed height. 62 | 63 | :param pop: The population vector, which contains the peak centres and widths. 64 | The first n_peaks elements are the centres, the next n_peaks elements are the 65 | widths. 66 | :param n_peaks: The number of peaks in the reflection spectrum 67 | :param wl: vector of wavelengths 68 | :param max_height: height of the reflection peaks (0-1) 69 | :param base: fixed reflection baseline (0-1) 70 | """ 71 | 72 | centres = pop[:n_peaks] 73 | widths = pop[n_peaks : 2 * n_peaks] 74 | 75 | # centres and widths should be np arrays 76 | spectrum = np.ones_like(wl) * base 77 | 78 | lower = centres - widths / 2 79 | upper = centres + widths / 2 80 | 81 | for i in range(len(centres)): 82 | spectrum[np.all((wl >= lower[i], wl <= upper[i]), axis=0)] += max_height 83 | 84 | # possible peaks are overlapping; R can't be more than peak value 85 | 86 | spectrum[spectrum > max_height] = max_height 87 | 88 | return spectrum 89 | 90 | 91 | def gen_spectrum_ndip_varyheight( 92 | pop: np.ndarray, 93 | n_peaks: int, 94 | wl: np.ndarray, 95 | max_height: float = 1, 96 | base: float = 00, 97 | ) -> np.ndarray: 98 | """Generate a reflection spectrum with n_peaks rectangular peaks of variable height. 99 | 100 | :param pop: The population vector, which contains the peak centres and widths. The first n_peaks elements are the centres, 101 | the next n_peaks elements are the widths, the last n_peaks elements are the height 102 | :param n_peaks: The number of peaks in the reflection spectrum 103 | :param wl: vector of wavelengths 104 | :param max_height: height of the reflection peaks (0-1) 105 | :param base: fixed reflection baseline (0-1) 106 | """ 107 | 108 | # centres and widths should be np arrays 109 | spectrum = np.ones_like(wl) * base 110 | centres = pop[:n_peaks] 111 | widths = pop[n_peaks : 2 * n_peaks] 112 | heights = pop[2 * n_peaks : 3 * n_peaks] 113 | 114 | lower = centres - widths / 2 115 | upper = centres + widths / 2 116 | 117 | for i in range(len(centres)): 118 | spectrum[np.all((wl >= lower[i], wl <= upper[i]), axis=0)] += heights[i] 119 | 120 | # possible peaks are overlapping; R can't be more than peak value 121 | 122 | spectrum[spectrum > max_height] = max_height 123 | 124 | return spectrum 125 | 126 | 127 | def gen_spectrum_ngauss( 128 | pop: np.ndarray, 129 | n_peaks: int, 130 | wl: np.ndarray, 131 | max_height: float = 1, 132 | base: float = 0, 133 | ): 134 | """Generate a reflection spectrum with n_peaks Gaussian peaks of fixed height. 135 | 136 | :param pop: The population vector, which contains the peak centres and widths. The first n_peaks elements are the centres, 137 | the next n_peaks elements are the widths. 138 | :param n_peaks: The number of peaks in the reflection spectrum 139 | :param wl: vector of wavelengths 140 | :param max_height: height of the reflection peaks (0-1) 141 | :param base: fixed reflection baseline (0-1) 142 | """ 143 | centres = pop[:n_peaks] 144 | widths = pop[n_peaks : 2 * n_peaks] 145 | 146 | spectrum = np.zeros_like(wl) 147 | 148 | for i in range(len(centres)): 149 | spectrum += np.exp(-((wl - centres[i]) ** 2) / (2 * widths[i] ** 2)) 150 | 151 | return base + (max_height - base) * spectrum / max(spectrum) 152 | 153 | 154 | def gen_spectrum_ngauss_varyheight( 155 | pop: np.ndarray, 156 | n_peaks: int, 157 | wl: np.ndarray, 158 | max_height: float = 1, 159 | base: float = 0, 160 | ): # center and width in nm 161 | """Generate a reflection spectrum with n_peaks Gaussian peaks of variable height. 162 | 163 | :param pop: The population vector, which contains the peak centres and widths. The first n_peaks elements are the centres, 164 | the next n_peaks elements are the widths. 165 | :param n_peaks: The number of peaks in the reflection spectrum 166 | :param wl: vector of wavelengths 167 | :param max_height: height of the reflection peaks (0-1) 168 | :param base: fixed reflection baseline (0-1) 169 | """ 170 | 171 | centres = pop[:n_peaks] 172 | widths = pop[n_peaks : 2 * n_peaks] 173 | heights = pop[2 * n_peaks : 3 * n_peaks] 174 | 175 | spectrum = np.zeros_like(wl) 176 | 177 | for i in range(len(centres)): 178 | spectrum += heights[i] * np.exp( 179 | -((wl - centres[i]) ** 2) / (2 * widths[i] ** 2) 180 | ) 181 | 182 | return base + (max_height - base) * spectrum / max(spectrum) 183 | 184 | 185 | class make_spectrum_ndip: 186 | """Class which creates an object containing necessary information about the type of reflection spectrum to be used 187 | in the optimization: number of peaks, bounds on peak centres and widths, and the function to generate the spectrum.""" 188 | 189 | def __init__( 190 | self, 191 | n_peaks: int = 2, 192 | target: np.ndarray = np.array([0, 0, 0]), 193 | R_type: str = "sharp", 194 | fixed_height: bool = True, 195 | w_bounds: Sequence[float] = None, 196 | h_bounds: Sequence[float] = [0.01, 1], 197 | ): 198 | 199 | """Initialise the spectrum object. 200 | 201 | :param n_peaks: The number of peaks in the reflection spectrum 202 | :param target: The XYZ coordinates of the target colour, used to set the bounds on the peak widths 203 | :param R_type: The type of spectrum to be generated. Current options are "sharp" and "gauss" 204 | :param fixed_height: Whether the peaks should have a fixed height 205 | :param w_bounds: The bounds on the peak widths. If None, the bounds will be set automatically 206 | :param h_bounds: The bounds on the peak heights. Only used if fixed_height is False 207 | """ 208 | 209 | self.c_bounds = [380, 730] 210 | self.fixed_height = fixed_height 211 | self.n_peaks = n_peaks 212 | 213 | if w_bounds is None: 214 | if fixed_height: 215 | self.w_bounds = [ 216 | 0, # lower width bound 217 | np.max([160 / n_peaks, (350 / n_peaks) * target[1]]), # upper 218 | # width bound 219 | ] 220 | # width bounds are generated based on the Y (luminance) of the target colour and the number of peaks. 221 | # This was done by looking at the approximate number of photos needed to create a colour of a certain Y 222 | # (and obviously allowing extra tolerance around this). The minimum width is always 0, the maximum with is 223 | # either 120/n_peaks (so 60 nm for two peaks), or (Y*350/n_peaks), whichever is larger. 224 | 225 | else: 226 | self.w_bounds = [0, 400] 227 | # if not doing fixed height peaks, argument above obviously doesn't work, so just set max width to 400 nm 228 | 229 | else: 230 | self.w_bounds = w_bounds 231 | 232 | if R_type == "sharp": 233 | if fixed_height: 234 | self.n_spectrum_params = 2 * n_peaks 235 | self.spectrum_function = gen_spectrum_ndip 236 | 237 | else: 238 | self.h_bounds = h_bounds 239 | self.n_spectrum_params = 3 * n_peaks 240 | self.spectrum_function = gen_spectrum_ndip_varyheight 241 | 242 | elif R_type == "gauss": 243 | if fixed_height: 244 | self.n_spectrum_params = 2 * n_peaks 245 | self.spectrum_function = gen_spectrum_ngauss 246 | 247 | else: 248 | self.h_bounds = h_bounds 249 | self.n_spectrum_params = 3 * n_peaks 250 | self.spectrum_function = gen_spectrum_ngauss_varyheight 251 | 252 | def get_bounds(self): 253 | 254 | """Return the bounds on the population vector which contains the peak centres and widths (and heights, if relevant). 255 | This is in the format [list of lower bounds, list of upper bounds]. For each list, the first n_peaks elements 256 | are the centres, the next n_peaks elements are the widths. If fixed_height is False, the next n_peaks elements are the 257 | heights.""" 258 | 259 | if self.fixed_height: 260 | return ( 261 | [self.c_bounds[0]] * self.n_peaks + [self.w_bounds[0]] * self.n_peaks, 262 | [self.c_bounds[1]] * self.n_peaks + [self.w_bounds[1]] * self.n_peaks, 263 | ) 264 | # ([lower bound of peak 1 centre, lower bound of peak 2 centre, ...., lower bound of peak 1 width, lower bound of peak 2 width, ...], 265 | # [upper bound of peak 1 centre, upper bound of peak 2 centre, ..., upper bound of peak 1 width, upper bound of peak 2 width, ...]) 266 | 267 | else: 268 | return ( 269 | [self.c_bounds[0]] * self.n_peaks 270 | + [self.w_bounds[0]] * self.n_peaks 271 | + [self.h_bounds[0]] * self.n_peaks, 272 | [self.c_bounds[1]] * self.n_peaks 273 | + [self.w_bounds[1]] * self.n_peaks 274 | + [self.h_bounds[1]] * self.n_peaks, 275 | ) 276 | 277 | 278 | def spec_to_XYZ( 279 | spec: np.ndarray, illuminant: np.ndarray, cmf: np.ndarray, interval: float 280 | ) -> Tuple[float, float, float]: 281 | """Convert an incident spectrum (spectral power distribution, W m-2 nm-1) to XYZ colour coordinates. 282 | 283 | :param spec: The reflectance spectrum 284 | :param illuminant: The illuminant (W m-2 nm-1) 285 | :param cmf: The CIE colour matching functions at the same wavelengths as spec and 286 | the illuminant 287 | :param interval: The wavelength interval between each element of spec, solar_spec 288 | and cmf 289 | """ 290 | 291 | Ymax = np.trapz(cmf[:, 1] * illuminant, dx=interval) 292 | X = np.trapz(cmf[:, 0] * illuminant * spec, dx=interval) 293 | Y = np.trapz(cmf[:, 1] * illuminant * spec, dx=interval) 294 | Z = np.trapz(cmf[:, 2] * illuminant * spec, dx=interval) 295 | 296 | if Ymax == 0: 297 | return (X, Y, Z) 298 | 299 | else: 300 | X = X / Ymax 301 | Y = Y / Ymax 302 | Z = Z / Ymax 303 | XYZ = (X, Y, Z) 304 | 305 | return XYZ 306 | 307 | 308 | def delta_XYZ(target: np.ndarray, col: np.ndarray): 309 | """Calculate the maximum fractional difference in the X, Y, Z colours between two colours in XYZ space, relative 310 | to the target argument.""" 311 | dXYZ = np.abs(target - col) / target 312 | 313 | return max(dXYZ) 314 | 315 | 316 | def XYZ_from_pop_dips(pop, n_peaks, photon_flux, interval): 317 | cs = pop[:n_peaks] 318 | ws = pop[n_peaks : 2 * n_peaks] 319 | 320 | cmf = load_cmf(photon_flux[0]) 321 | # T = 298 322 | 323 | R_spec = gen_spectrum_ndip(cs, ws, wl=photon_flux[0]) 324 | XYZ = np.array( 325 | spec_to_XYZ( 326 | R_spec, hc * photon_flux[1] / (photon_flux[0] * 1e-9), cmf, interval 327 | ) 328 | ) 329 | 330 | return XYZ 331 | 332 | def load_D50(wl): 333 | 334 | data = np.loadtxt(join(current_path, "data", "CIE_std_illum_D50.csv"), 335 | delimiter=",") 336 | 337 | return np.interp(wl, data[:,0], data[:,1]) 338 | 339 | def load_D65(wl): 340 | 341 | data = np.loadtxt(join(current_path, "data", "CIE_std_illum_D65.csv"), 342 | delimiter=",") 343 | 344 | return np.interp(wl, data[:,0], data[:,1]) 345 | -------------------------------------------------------------------------------- /examples/run_cell_color_optim_loop_spd.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import ( 2 | load_colorchecker, 3 | multiple_color_cells, 4 | cell_optimization, 5 | ) 6 | import numpy as np 7 | from solcore.light_source import LightSource 8 | import matplotlib.pyplot as plt 9 | import pygmo as pg 10 | from os import path 11 | import pandas as pd 12 | 13 | force_rerun = False 14 | force_rerun_ideal = False 15 | include_minimum_effs = False 16 | include_seed_population = False 17 | 18 | col_thresh = 0.004 # for a wavelength interval of 0.1, minimum achievable color error will be (very rough estimate!) ~ 0.001. 19 | # This is the maximum allowed fractional error in X, Y, or Z colour coordinates. 20 | 21 | acceptable_eff_change = 1e-4 # how much can the efficiency (in %) change between iteration sets? Stop when have reached 22 | # col_thresh and efficiency change is less than this. 23 | 24 | n_trials = 10 # number of islands which will run concurrently 25 | interval = 0.1 # wavelength interval (in nm) 26 | wl_cell = np.arange( 27 | 300, 4000, interval 28 | ) # wavelengths used for cell calculations (range of wavelengths in AM1.5G solar 29 | # spectrum. For calculations relating to colour perception, only the visible range (380-780 nm) will be used. 30 | 31 | single_J_result = pd.read_csv("../ecopv/data/paper_colors.csv") 32 | 33 | initial_iters = 100 # number of initial evolutions for the archipelago 34 | add_iters = 100 # additional evolutions added each time if color threshold/convergence condition not met 35 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 36 | 37 | max_trials_col = 3 * add_iters 38 | # how many population evolutions happen before giving up if there are no populations 39 | # which meet the color threshold 40 | 41 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 42 | fixed_height = True # fixed height peaks (will be at the value of max_height) if True, or peak height is an optimization 43 | # variable if False 44 | light_source_name = "AM1.5g" 45 | j01_method = "perfect_R" 46 | 47 | max_height = 1 48 | # maximum height of reflection peaks; fixed at this value of if fixed_height = True 49 | 50 | base = 0 51 | # baseline fixed reflection (fixed at this value for both fixed_height = True and False). 52 | 53 | n_junc_loop = [1, 2, 3, 4, 5, 6] # loop through these numbers of junctions 54 | n_peak_loop = [2, 3, 4] # loop through these numbers of reflection peaks 55 | 56 | color_names, color_XYZ = load_colorchecker(illuminant="AM1.5g", output_coords="XYZ") 57 | # load the names and XYZ coordinates of the 24 default Babel colors 58 | start_ind = 0 59 | end_ind = len(color_names) 60 | color_names = color_names[start_ind:end_ind] 61 | color_XYZ = color_XYZ[start_ind:end_ind] 62 | 63 | # Use AM1.5G spectrum for cell calculations: 64 | light_source = LightSource( 65 | source_type="standard", 66 | version=light_source_name, 67 | x=wl_cell, 68 | output_units="photon_flux_per_nm", 69 | ) 70 | 71 | photon_flux_cell = np.array(light_source.spectrum(wl_cell)) 72 | 73 | shapes = ["+", "o", "^", ".", "*", "v", "s", "x"] 74 | 75 | loop_n = 0 76 | 77 | # precalculate optimal bandgaps for junctions: 78 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 79 | 80 | for n_junctions in n_junc_loop: 81 | 82 | save_loc = save_path + "/champion_pop_{}juncs_{}spec.txt".format( 83 | n_junctions, light_source_name 84 | ) 85 | 86 | if not path.exists(save_loc) or force_rerun_ideal: 87 | 88 | p_init = cell_optimization( 89 | n_junctions, 90 | photon_flux_cell, 91 | power_in=light_source.power_density, 92 | eta_ext=1, 93 | Eg_limits=[0.4, 2.35] 94 | ) 95 | 96 | prob = pg.problem(p_init) 97 | algo = pg.algorithm( 98 | pg.de( 99 | gen=500*n_junctions, 100 | F=1, 101 | CR=1, 102 | ftol=0, 103 | xtol=0, 104 | ) 105 | ) 106 | 107 | pop = pg.population(prob, 30 * n_junctions) 108 | pop = algo.evolve(pop) 109 | 110 | champion_pop = np.sort(pop.champion_x) 111 | 112 | print(n_junctions, pop.problem.get_fevals()/(30*n_junctions), 113 | -pop.champion_f*100) 114 | 115 | np.savetxt( 116 | save_path 117 | + "/champion_pop_{}juncs_{}spec.txt".format( 118 | n_junctions, light_source_name 119 | ), 120 | champion_pop, 121 | ) 122 | 123 | 124 | if __name__ == "__main__": 125 | # Need this __main__ construction because otherwise the parallel running of the different islands (n_trials) may throw an error 126 | champion_effs_table = np.zeros((len(n_peak_loop), len(n_junc_loop), len(color_names))) 127 | 128 | for i1, n_peaks in enumerate(n_peak_loop): 129 | for j1, n_junctions in enumerate(n_junc_loop): 130 | champion_bandgaps = np.zeros((len(color_names), n_junctions)) 131 | 132 | Eg_guess = np.loadtxt( 133 | save_path 134 | + "/champion_pop_{}juncs_{}spec.txt".format( 135 | n_junctions, light_source_name 136 | ), 137 | ndmin=1, 138 | ) 139 | 140 | save_loc = ( 141 | "results/champion_eff_" 142 | + R_type 143 | + str(n_peaks) 144 | + "_" 145 | + str(n_junctions) 146 | + "_" 147 | + str(fixed_height) 148 | + str(max_height) 149 | + "_" 150 | + str(base) 151 | + "_" 152 | + j01_method + light_source_name + ".txt" 153 | ) 154 | 155 | if not path.exists(save_loc) or force_rerun: 156 | 157 | print( 158 | n_peaks, 159 | "peaks,", 160 | n_junctions, 161 | "junctions,", 162 | "fixed height:", 163 | fixed_height, 164 | ) 165 | 166 | minimum_effs_file = "results/champion_eff_" + R_type + str(n_peaks) +\ 167 | "_" + str(n_junctions - 1) + "_" + \ 168 | str(fixed_height) + str(max_height) + "_" + \ 169 | str(base) + "_" + j01_method + light_source_name + ".txt" 170 | 171 | seed_pop_file = "results/champion_pop_" + R_type + str(n_peaks) +\ 172 | "_" + str(n_junctions - 1) + "_" + \ 173 | str(fixed_height) + str(max_height) + "_" + \ 174 | str(base) + "_" + j01_method + light_source_name + ".txt" 175 | 176 | 177 | if path.exists(minimum_effs_file) and include_minimum_effs: 178 | minimum_effs = np.loadtxt(minimum_effs_file)#[start_ind:end_ind] 179 | 180 | else: 181 | minimum_effs = np.zeros(len(color_names)) 182 | 183 | if path.exists(seed_pop_file) and include_seed_population: 184 | seed_pop = np.loadtxt(seed_pop_file)#[start_ind:end_ind] 185 | print("seeding population") 186 | # seed_pop = seed_pop[:, :(seed_pop.shape[1] - n_junctions + 1)] 187 | 188 | else: 189 | seed_pop = None 190 | 191 | print("minimum efficiencies:", minimum_effs) 192 | 193 | result = multiple_color_cells( 194 | color_XYZ, 195 | color_names, 196 | photon_flux_cell, 197 | n_peaks=n_peaks, 198 | n_junctions=n_junctions, 199 | R_type=R_type, 200 | fixed_height=fixed_height, 201 | n_trials=n_trials, 202 | initial_iters=initial_iters, 203 | add_iters=add_iters, 204 | col_thresh=col_thresh, 205 | acceptable_eff_change=acceptable_eff_change, 206 | max_trials_col=max_trials_col, 207 | base=base, 208 | max_height=max_height, 209 | Eg_black=Eg_guess, 210 | plot=False, 211 | power_in=light_source.power_density, 212 | return_archipelagos=True, 213 | j01_method=j01_method, 214 | minimum_eff=minimum_effs, 215 | seed_population=seed_pop, 216 | illuminant=light_source_name, 217 | ) 218 | 219 | champion_effs = result["champion_eff"] 220 | champion_pops = result["champion_pop"] 221 | champion_bandgaps = champion_pops[:, -n_junctions:] 222 | 223 | final_populations = result["archipelagos"] 224 | 225 | np.savetxt( 226 | "results/champion_eff_" 227 | + R_type 228 | + str(n_peaks) 229 | + "_" 230 | + str(n_junctions) 231 | + "_" 232 | + str(fixed_height) 233 | + str(max_height) 234 | + "_" 235 | + str(base) 236 | + "_" + j01_method + light_source_name + ".txt", 237 | champion_effs, 238 | ) 239 | np.savetxt( 240 | "results/champion_pop_" 241 | + R_type 242 | + str(n_peaks) 243 | + "_" 244 | + str(n_junctions) 245 | + "_" 246 | + str(fixed_height) 247 | + str(max_height) 248 | + "_" 249 | + str(base) 250 | + "_" + j01_method + light_source_name + ".txt", 251 | champion_pops, 252 | ) 253 | np.save( 254 | "results/final_pop_" 255 | + R_type 256 | + str(n_peaks) 257 | + "_" 258 | + str(n_junctions) 259 | + "_" 260 | + str(fixed_height) 261 | + str(max_height) 262 | + "_" 263 | + str(base) 264 | + "_" + j01_method + light_source_name + ".npy", 265 | final_populations, 266 | ) 267 | 268 | else: 269 | 270 | champion_effs = np.loadtxt( 271 | "results/champion_eff_" 272 | + R_type 273 | + str(n_peaks) 274 | + "_" 275 | + str(n_junctions) 276 | + "_" 277 | + str(fixed_height) 278 | + str(max_height) 279 | + "_" 280 | + str(base) 281 | + "_" + j01_method + light_source_name + ".txt", 282 | ) 283 | champion_pops = np.loadtxt( 284 | "results/champion_pop_" 285 | + R_type 286 | + str(n_peaks) 287 | + "_" 288 | + str(n_junctions) 289 | + "_" 290 | + str(fixed_height) 291 | + str(max_height) 292 | + "_" 293 | + str(base) 294 | + "_" + j01_method + light_source_name + ".txt", 295 | ) 296 | champion_bandgaps = champion_pops[:, -n_junctions:] 297 | 298 | 299 | champion_effs_table[i1, j1] = champion_effs 300 | 301 | plt.figure() 302 | plt.plot( 303 | color_names, 304 | champion_effs, 305 | marker=shapes[0], 306 | mfc="none", 307 | linestyle="none", 308 | label="Power density", 309 | ) 310 | 311 | plt.plot( 312 | color_names, single_J_result["eta"], "o", mfc="none", label="1J result" 313 | ) 314 | 315 | plt.xticks(rotation=45) 316 | plt.legend() 317 | plt.ylabel("Efficiency (%)") 318 | # plt.title("Pop:" + str(pop_size) + "Iters:" + str(n_iters) + "Time:" + str(time_taken)) 319 | plt.title("Peaks:" + str(n_peaks) + "Junctions:" + str(n_junctions)) 320 | plt.tight_layout() 321 | plt.show() 322 | 323 | plt.figure() 324 | plt.plot( 325 | color_names, 326 | champion_bandgaps, 327 | marker=shapes[0], 328 | mfc="none", 329 | linestyle="none", 330 | label="Power density", 331 | ) 332 | 333 | plt.plot( 334 | color_names, single_J_result["Eg"], "o", mfc="none", label="1J result" 335 | ) 336 | plt.xticks(rotation=45) 337 | plt.legend() 338 | plt.ylabel("Bandgap (eV)") 339 | plt.title("Peaks:" + str(n_peaks) + "Junctions:" + str(n_junctions)) 340 | plt.tight_layout() 341 | plt.show() 342 | 343 | 344 | import pandas as pd 345 | 346 | color_names, color_xyY = load_colorchecker(illuminant="AM1.5g", output_coords="xyY") 347 | 348 | data = {'index': np.arange(1, len(color_names)+1), 'name': color_names} 349 | 350 | df = pd.DataFrame(data) 351 | 352 | df['x'] = color_xyY[:, 0] 353 | df['y'] = color_xyY[:, 1] 354 | df['Y'] = color_xyY[:, 2] 355 | 356 | df['1J'], df['2J'], df['3J'], df['4J'], df['5J'], df['6J'] = champion_effs_table[0] 357 | 358 | df.to_csv('AM15G_xyY_ColorChecker_optimized.csv', index=False) 359 | -------------------------------------------------------------------------------- /examples/gamut_luminance_spd.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import multiple_color_cells, cell_optimization 2 | from colormath.color_objects import xyYColor 3 | from solcore.light_source import LightSource 4 | import matplotlib.pyplot as plt 5 | import pygmo as pg 6 | from os import path 7 | from ecopv.plot_utilities import * 8 | from colour import wavelength_to_XYZ 9 | import os 10 | 11 | force_rerun = False 12 | 13 | Ys = [0.25, 0.5, 0.75] 14 | 15 | wl_vis = np.linspace(360, 780, 500) 16 | 17 | XYZ = wavelength_to_XYZ(wl_vis) 18 | 19 | sumXYZ = np.sum(XYZ, axis=1) 20 | 21 | xg = XYZ[:, 0] / sumXYZ 22 | yg = XYZ[:, 1] / sumXYZ 23 | 24 | xs = np.arange(np.min(xg), np.max(xg), 0.01) 25 | ys = np.arange(np.min(yg), np.max(yg), 0.01) 26 | 27 | is_inside = np.full((len(xs), len(ys)), False) 28 | 29 | peak = np.argmax(yg) 30 | 31 | left_edge = [xg[:peak], yg[:peak]] 32 | right_edge = [xg[peak:], yg[peak:]] 33 | 34 | # now check if the points are inside the gamut defined by the spectral locus 35 | 36 | for j, yc in enumerate(ys): 37 | left_y = np.argmin(np.abs(left_edge[1] - yc)) 38 | right_y = np.argmin(np.abs(right_edge[1] - yc)) 39 | 40 | left_x = left_edge[0][left_y] 41 | right_x = right_edge[0][right_y] 42 | is_inside[np.all((xs > left_x, xs < right_x), axis=0), j] = True 43 | 44 | # eliminate everything below the line of purples: 45 | 46 | # equation for line of purples: 47 | 48 | slope = (yg[-1] - yg[0]) / (xg[-1] - xg[0]) 49 | c = yg[0] - slope * xg[0] 50 | 51 | for j, yc in enumerate(ys): 52 | above = yc > slope * xs + c 53 | is_inside[:, j] = np.all((above, is_inside[:, j]), axis=0) 54 | 55 | # plt.figure() 56 | # plt.plot(xg, yg) 57 | # for j1, x in enumerate(xs): 58 | # for k1, y in enumerate(ys): 59 | # if is_inside[j1, k1]: 60 | # plt.plot(x, y, "o", color="black") 61 | # 62 | # plt.show() 63 | print("Inside gamut:", np.sum(is_inside)) 64 | 65 | col_thresh = 0.008 # for a wavelength interval of 0.1, minimum achievable color 66 | # error will be (very rough estimate!) ~ 0.001. 67 | # This is the maximum allowed fractional error in X, Y, or Z colour coordinates. 68 | 69 | acceptable_eff_change = 1e-3 # how much can the efficiency (in %) change between iteration sets? Stop when have reached 70 | # col_thresh and efficiency change is less than this. 71 | 72 | n_trials = 10 # number of islands which will run concurrently in parallel 73 | interval = 0.1 # wavelength interval (in nm) 74 | wl_cell = np.arange( 75 | 300, 4000, interval 76 | ) # wavelengths used for cell calculations (range of wavelengths in AM1.5G solar 77 | # spectrum. For calculations relating to colour perception, only the visible range (380-780 nm) will be used. 78 | 79 | n_peaks = 2 80 | 81 | initial_iters = 100 # number of initial evolutions for the archipelago 82 | add_iters = 100 # additional evolutions added each time if color threshold/convergence condition not met 83 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 84 | 85 | max_trials_col = ( 86 | 4 * add_iters 87 | ) # how many population evolutions happen before giving up if there are no populations 88 | # which meet the color threshold 89 | 90 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 91 | fixed_height = True # fixed height peaks (will be at the value of max_height) if True, or peak height is an optimization 92 | # variable if False 93 | light_source_name = "AM1.5g" 94 | 95 | max_height = ( 96 | 1 # maximum height of reflection peaks; fixed at this value of fixed_height = True 97 | ) 98 | base = 0 # baseline fixed reflection (fixed at this value for both fixed_height = True and False). 99 | 100 | n_junctions = 3 # loop through these numbers of junctions 101 | 102 | # Use AM1.5G spectrum: 103 | light_source = LightSource( 104 | source_type="standard", 105 | version=light_source_name, 106 | x=wl_cell, 107 | output_units="photon_flux_per_nm", 108 | ) 109 | 110 | photon_flux_cell = np.array(light_source.spectrum(wl_cell)) 111 | 112 | shapes = ["+", "o", "^", ".", "*", "v", "s", "x"] 113 | 114 | loop_n = 0 115 | 116 | # precalculate optimal bandgaps for junctions: 117 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 118 | 119 | # 120 | # save_loc = save_path + "/champion_pop_{}juncs_{}spec.txt".format( 121 | # n_junctions, light_source_name 122 | # ) 123 | # 124 | # level_limits = [[44, 48], [42, 46], [39, 42]] 125 | # 126 | # if not path.exists(save_loc) or force_rerun: 127 | # 128 | # p_init = cell_optimization( 129 | # n_junctions, photon_flux_cell, power_in=light_source.power_density, eta_ext=1 130 | # ) 131 | # prob = pg.problem(p_init) 132 | # algo = pg.algorithm( 133 | # pg.de( 134 | # gen=1000, 135 | # F=1, 136 | # CR=1, 137 | # ) 138 | # ) 139 | # 140 | # pop = pg.population(prob, 20 * n_junctions) 141 | # pop = algo.evolve(pop) 142 | # 143 | # champion_pop = np.sort(pop.champion_x) 144 | # 145 | # np.savetxt( 146 | # save_path 147 | # + "/champion_pop_{}juncs_{}spec.txt".format(n_junctions, light_source_name), 148 | # champion_pop, 149 | # ) 150 | 151 | 152 | if __name__ == "__main__": 153 | # Need this __main__ construction because otherwise the parallel running of the different islands (n_trials) may throw an error 154 | 155 | fig, axs = plt.subplots(1, len(Ys), figsize=(len(Ys) * 3.5, 4)) 156 | if len(Ys) == 1: 157 | axs = [axs] 158 | 159 | for i1, Y in enumerate(Ys): 160 | 161 | if os.path.exists( 162 | save_path + "/pop_gamut_Y_{}_{}_spd_2.npy".format(Y, n_junctions) 163 | ): 164 | print("Load existing result") 165 | 166 | pop_array = np.load( 167 | save_path + "/pop_gamut_Y_{}_{}_spd_2.npy".format(Y, n_junctions) 168 | ) 169 | eff_array = np.load( 170 | save_path + "/eff_gamut_Y_{}_{}_spd_2.npy".format(Y, n_junctions) 171 | ) 172 | 173 | else: 174 | col_possible_str = save_path + "/possible_colours_Y_{}_spd_2.txt".format(Y) 175 | 176 | print("Found existing file for possible colours") 177 | is_possible = np.loadtxt(col_possible_str) 178 | print("Possible colours:", np.sum(is_possible)) 179 | 180 | eff_array = np.zeros((len(xs), len(ys))) 181 | pop_array = np.zeros((len(xs), len(ys), 4 + n_junctions)) 182 | 183 | for j1, x in enumerate(xs): 184 | for k1, y in enumerate(ys): 185 | if is_inside[j1, k1] and is_possible[j1, k1]: 186 | XYZ = np.array( 187 | [ 188 | convert_color( 189 | xyYColor(x, y, Y), XYZColor 190 | ).get_value_tuple() 191 | ] 192 | ) 193 | Eg_guess = np.loadtxt( 194 | save_path 195 | + "/champion_pop_{}juncs_{}spec.txt".format( 196 | n_junctions, light_source_name 197 | ), 198 | ndmin=1, 199 | ) 200 | 201 | result = multiple_color_cells( 202 | XYZ, 203 | [str(j1) + "_" + str(k1)], 204 | photon_flux_cell, 205 | n_peaks=n_peaks, 206 | n_junctions=n_junctions, 207 | R_type=R_type, 208 | fixed_height=fixed_height, 209 | n_trials=n_trials, 210 | initial_iters=initial_iters, 211 | add_iters=add_iters, 212 | col_thresh=col_thresh, 213 | acceptable_eff_change=acceptable_eff_change, 214 | max_trials_col=max_trials_col, 215 | base=base, 216 | max_height=max_height, 217 | Eg_black=Eg_guess, 218 | plot=False, 219 | ) 220 | 221 | champion_effs = result["champion_eff"] 222 | champion_pops = result["champion_pop"] 223 | 224 | print("champion effs", champion_effs) 225 | 226 | eff_array[j1, k1] = champion_effs 227 | pop_array[j1, k1, :] = champion_pops 228 | if np.max(champion_effs) == 0: 229 | print("SETTING TO 0") 230 | is_possible[j1, k1] = False 231 | 232 | print("sum:", np.sum(is_possible)) 233 | 234 | np.save( 235 | save_path + "/pop_gamut_Y_{}_{}_spd_2.npy".format(Y, n_junctions), 236 | pop_array, 237 | ) 238 | np.save( 239 | save_path + "/eff_gamut_Y_{}_{}_spd_2.npy".format(Y, n_junctions), 240 | eff_array, 241 | ) 242 | 243 | width = np.diff(xs)[0] 244 | height = np.diff(ys)[0] 245 | 246 | eff_mask = eff_array > 0 247 | 248 | Egs = pop_array[:, :, -1] 249 | Egs = eff_mask * Egs 250 | 251 | standard_illuminant = [0.3128, 0.3290, Y] 252 | XYZ = convert_color(xyYColor(*standard_illuminant), XYZColor) 253 | s_i_RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 254 | s_i_RGB[s_i_RGB > 1] = 1 255 | 256 | label_wls = np.arange(440, 660, 20) 257 | 258 | XYZlab = wavelength_to_XYZ(label_wls) 259 | 260 | sumXYZlab = np.sum(XYZlab, axis=1) 261 | 262 | xgl = XYZlab[:, 0] / sumXYZlab 263 | ygl = XYZlab[:, 1] / sumXYZlab 264 | 265 | tick_orig = np.zeros((len(label_wls), 2)) 266 | tick_dir = np.zeros((len(label_wls), 2)) 267 | # create ticks 268 | for m1, lwl in enumerate(label_wls): 269 | p0 = wavelength_to_XYZ(lwl) 270 | p1 = wavelength_to_XYZ(lwl - 1) 271 | p2 = wavelength_to_XYZ(lwl + 1) 272 | 273 | p0 = p0 / np.sum(p0) 274 | p1 = p1 / np.sum(p1) 275 | p2 = p2 / np.sum(p2) 276 | 277 | m = np.array([p2[0] - p1[0], p2[1] - p1[1]]) 278 | mp = np.array([-m[1], m[0]]) 279 | mp = mp / np.linalg.norm(mp) 280 | # b = p1[1] + (1/m) * p1[0] 281 | 282 | tick_orig[m1] = p0[:2] 283 | tick_dir[m1] = p0[:2] + 0.02 * mp 284 | 285 | ax = axs[i1] 286 | ax.set_aspect("equal") 287 | ax.set_facecolor(s_i_RGB) 288 | ax.plot(xg, yg, "k") 289 | ax.plot([xg[0], xg[-1]], [yg[0], yg[-1]], "k") 290 | 291 | for m1, lwl in enumerate(label_wls): 292 | ax.plot( 293 | [tick_orig[m1, 0], tick_dir[m1, 0]], 294 | [tick_orig[m1, 1], tick_dir[m1, 1]], 295 | "-k", 296 | ) 297 | 298 | if lwl > 520: 299 | ax.text(*tick_dir[m1], str(lwl)) 300 | 301 | elif lwl == 520: 302 | ax.text(*tick_dir[m1], str(lwl), horizontalalignment="center") 303 | 304 | else: 305 | ax.text( 306 | *tick_dir[m1], 307 | str(lwl), 308 | horizontalalignment="right", 309 | verticalalignment="center" 310 | ) 311 | 312 | ax.set_xlim(-0.09, 0.8) 313 | ax.set_ylim(-0.07, 0.9) 314 | ax.set_xlabel("x") 315 | ax.set_ylabel("y") 316 | 317 | for j1, x in enumerate(xs): 318 | for k1, y in enumerate(ys): 319 | 320 | if ~np.isnan(eff_array[j1, k1]) and eff_array[j1, k1] > 0: 321 | XYZ = convert_color(xyYColor(x, y, Y), XYZColor) 322 | RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 323 | 324 | RGB[RGB > 1] = 1 325 | # plt.plot(x, y, '.') 326 | ax.add_patch( 327 | Rectangle( 328 | xy=(x - width / 2, y - height / 2), 329 | width=width, 330 | height=height, 331 | facecolor=RGB, 332 | ) 333 | ) 334 | # ax.text(x, y, str(round(eff_array[j1, k1], 2)), color='k', ha='center', va='center') 335 | # 336 | # plt.plot(x, y, 'k.') 337 | 338 | # levels = np.arange(np.ceil(np.min(eff_array[eff_array>0]))+2, 1.01*np.floor(np.nanmax(eff_array[eff_array>0])), 1) 339 | 340 | levels = np.round( 341 | np.arange(0.98 * np.max(eff_array) - 3, 0.98 * np.max(eff_array) + 0.01, 1), 342 | 1, 343 | ) 344 | 345 | within_95_p = 0.95 * np.max(eff_array) 346 | # print(np.min(eff_array[eff_array>0]), np.max(eff_array)) 347 | cs = ax.contour(xs, ys, eff_array.T, levels=levels, colors="k", alpha=0.7) 348 | 349 | cs2 = ax.contour( 350 | xs, ys, eff_array.T, levels=[within_95_p], colors="k", linestyles="dashed" 351 | ) 352 | print(within_95_p) 353 | 354 | # levels = np.linspace(np.min(Egs[Egs > 0]), 355 | # np.max(Egs[Egs > 0]), 3) 356 | # print(levels) 357 | # 358 | # cs = ax.contour(xs, ys, Egs.T, 359 | # levels=levels, 360 | # colors='k', alpha=0.5) 361 | 362 | label_loc = [] 363 | 364 | for i1, seg in enumerate(cs.allsegs): 365 | x_lower = seg[0][:, 0][seg[0][:, 1] < 0.6] 366 | y_lower = seg[0][:, 1][seg[0][:, 1] < 0.6] 367 | 368 | close_x = np.argmin(np.abs(x_lower - 0.35)) 369 | close_y = y_lower[close_x] 370 | label_loc.append([0.35, close_y]) 371 | 372 | ax.clabel(cs, inline=1, fontsize=8, manual=label_loc) 373 | ax.set_title("Y = " + str(Y)) 374 | ax.yaxis.set_minor_locator(tck.AutoMinorLocator()) 375 | ax.xaxis.set_minor_locator(tck.AutoMinorLocator()) 376 | ax.grid(axis="both", color="0.4", alpha=0.5) 377 | ax.tick_params(direction="in", which="both", top=True, right=True) 378 | # ax.set_axisbelow(True) 379 | 380 | plt.tight_layout() 381 | plt.show() 382 | -------------------------------------------------------------------------------- /ecopv/optimization_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.special import lambertw 3 | from solcore.constants import kb, q, h, c 4 | from typing import Sequence 5 | from ecopv.spectrum_functions import gen_spectrum_ndip 6 | 7 | k = kb / q 8 | h_eV = h / q 9 | e = np.exp(1) 10 | T = 298 11 | kbT = k * T 12 | pref = ((2 * np.pi * q) / (h_eV**3 * c**2)) * kbT 13 | 14 | pref_wl = 1e27*2*np.pi*q*c 15 | wl_exp_const = 1e9*h*c/(kb*T) 16 | 17 | 18 | def reorder_peaks(pop, n_peaks, n_junctions, fixed_height=True): 19 | peaks = pop[:n_peaks] 20 | bandgaps = pop[-n_junctions:] if n_junctions > 0 else np.array([]) 21 | sorted_widths = np.array( 22 | [x for _, x in sorted(zip(peaks, pop[n_peaks : 2 * n_peaks]))] 23 | ) 24 | 25 | if not fixed_height: 26 | sorted_heights = np.array( 27 | [x for _, x in sorted(zip(peaks, pop[2 * n_peaks : 3 * n_peaks]))] 28 | ) 29 | 30 | peaks.sort() 31 | bandgaps.sort() 32 | 33 | if fixed_height: 34 | return np.hstack((peaks, sorted_widths, bandgaps)) 35 | 36 | else: 37 | return np.hstack((peaks, sorted_widths, sorted_heights, bandgaps)) 38 | 39 | 40 | def db_cell_calculation_noR( 41 | egs: Sequence[float], 42 | flux: np.ndarray, 43 | wl: np.ndarray, 44 | interval: float, 45 | rad_eff: int = 1, 46 | upperE: float = 4.43, 47 | *args, # this is in case a different db_cell_calculation method has additional 48 | # arguments 49 | ) -> tuple: 50 | 51 | """Calculates recombination current density, current due to illumination, voltages 52 | at the maximum power point and currents at the maximum power point of a 53 | multi-junction solar cell in the detailed-balance limit. Ignores the effect of 54 | reflected photons on the recombination current (dark current). 55 | 56 | :param egs: Bandgaps of the subcells in eV, order from highest to lowest 57 | :param flux: Flux of the solar spectrum in W/m^2/nm 58 | :param wl: Wavelengths of the solar spectrum in nm 59 | :param interval: Wavelength interval of the solar spectrum in nm 60 | :param rad_eff: Radiative efficiency of the cell (0-1) 61 | :param upperE: upper limit (in eV) for integrating over photon flux 62 | """ 63 | 64 | # Since we need previous Eg info have to iterate the Jsc array 65 | jscs = np.empty_like(egs, dtype=float) # Quick way of defining jscs with same 66 | # dimensions as egs 67 | j01s = np.empty_like(egs, dtype=float) 68 | 69 | for i, eg in enumerate(egs): 70 | 71 | j01s[i] = ( 72 | (pref / rad_eff) 73 | * (eg**2 + 2 * eg * (kbT) + 2 * (kbT) ** 2) 74 | * np.exp(-(eg) / (kbT)) 75 | ) 76 | jscs[i] = ( 77 | q 78 | * np.sum(flux[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)]) 79 | * interval 80 | ) 81 | 82 | upperE = eg 83 | 84 | Vmaxs = (kbT * (lambertw(e * (jscs / j01s)) - 1)).real 85 | Imaxs = jscs - j01s * np.exp(Vmaxs / (kbT)) 86 | 87 | return j01s, jscs, Vmaxs, Imaxs 88 | 89 | def indefinite_integral_J0(eg1, eg2): 90 | # eg1 is lower Eg, eg2 is higher Eg 91 | p1 = -(eg1 ** 2 + 2 * eg1 * (kbT) + 2 * (kbT) ** 2)* np.exp(-(eg1) / (kbT)) 92 | p2 = -(eg2 ** 2 + 2 * eg2 * (kbT) + 2 * (kbT) ** 2)* np.exp(-(eg2) / (kbT)) 93 | 94 | return (p2 - p1) 95 | 96 | def db_cell_calculation_perfectR( 97 | egs: Sequence[float], 98 | flux: np.ndarray, 99 | wl: np.ndarray, 100 | interval: float, 101 | rad_eff: int = 1, 102 | upperE: float = 4.43, 103 | x: Sequence[float] = None, 104 | n_peaks: int = 2, 105 | *args, # this is in case a different db_cell_calculation method has additional args 106 | ) -> tuple: 107 | 108 | """Calculates recombination current density, current due to illumination, voltages 109 | at the maximum power point and currents at the maximum power point of a 110 | multi-junction solar cell in the detailed-balance limit. Takes into account the 111 | effect of reflected photons on the recombination current (dark current) 112 | for rectangular reflection peaks using the analytical form of the integral. 113 | 114 | :param egs: Bandgaps of the subcells in eV, order from highest to lowest 115 | :param flux: Flux of the solar spectrum in W/m^2/nm 116 | :param wl: Wavelengths of the solar spectrum in nm 117 | :param interval: Wavelength interval of the solar spectrum in nm 118 | :param rad_eff: Radiative efficiency of the cell (0-1) 119 | :param upperE: upper limit (in eV) for integrating over photon flux 120 | :param x: List of parameters for the reflection peaks 121 | :param n_peaks: Number of reflection peaks 122 | """ 123 | 124 | # limits are in terms of energy, but R bands are defined in terms of wavelength! 125 | 126 | # limits = np.array([ 127 | # [0, 1240/(x[1]+x[3]/2)], # infinite wavelength to upper end of upper band 128 | # [1240/(x[1]-x[3]/2), 1240/(x[0]+x[2]/2)], # between the R bands 129 | # [1240/(x[0]-x[2]/2), upperE] # below the lower band 130 | # ]) 131 | 132 | limits = [[0, 1240/(x[n_peaks-1]+x[2*n_peaks-1]/2)]] 133 | 134 | if n_peaks > 1: 135 | 136 | for i1 in np.arange(1, n_peaks): 137 | limits.append([1240/(x[n_peaks-i1]-x[2*n_peaks-i1]/2), 1240/(x[ 138 | n_peaks-(i1+1)]+x[2*n_peaks-(i1+1)]/2)]) 139 | 140 | limits.append([1240/(x[0]-x[n_peaks]/2), upperE]) 141 | 142 | limits = np.array(limits) 143 | 144 | # if we have more than 2 peaks, some peaks can overlap. Remove superfluous limits 145 | # which cause incorrect integration limits 146 | overlapping = np.where(limits[:, 0] > limits[:, 1])[0] 147 | limits[overlapping + 1, 0] = limits[overlapping, 1] 148 | limits = np.delete(limits, overlapping, axis=0) 149 | 150 | limits[limits < 0] = 0 # set negative values to 0 151 | limits[limits > upperE] = upperE # set values above upperE to upperE 152 | 153 | # Since we need previous Eg info have to iterate the Jsc array 154 | jscs = np.zeros_like(egs, dtype=float) # Quick way of defining jscs with same 155 | # dimensions as egs 156 | j01s = np.zeros_like(egs, dtype=float) 157 | 158 | for i, eg in enumerate(egs): 159 | 160 | # check where Eg is in the limits array. Only care about bands below Eg 161 | loop_limits = limits[limits[:,1] > eg] 162 | loop_limits[0, 0] = np.max([eg, loop_limits[0,0]]) 163 | 164 | for lims in loop_limits: 165 | j01s[i] += (pref/rad_eff)*indefinite_integral_J0(lims[0], lims[1]) 166 | 167 | jscs[i] = ( 168 | q 169 | * np.sum(flux[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)]) 170 | * interval 171 | ) 172 | 173 | upperE = eg 174 | 175 | Vmaxs = (kbT * (lambertw(e * (jscs / j01s)) - 1)).real 176 | Imaxs = jscs - j01s * np.exp(Vmaxs / (kbT)) 177 | 178 | return j01s, jscs, Vmaxs, Imaxs 179 | 180 | def db_cell_calculation_numericalR( 181 | egs: Sequence[float], 182 | flux: np.ndarray, 183 | wl: np.ndarray, 184 | interval: float, 185 | rad_eff: int = 1, 186 | upperE: float = 4.43, 187 | x: Sequence[float] = None, 188 | n_peaks: int = 2, 189 | *args, # this is in case a different db_cell_calculation method has additional args 190 | ) -> tuple: 191 | 192 | """Calculates recombination current density, current due to illumination, voltages 193 | at the maximum power point and currents at the maximum power point of a 194 | multi-junction solar cell in the detailed-balance limit. Takes into accoun the 195 | effect of reflected photons on the recombination current (dark current) by 196 | using numerical integration (this is much slower than the 197 | db_cell_calculation_perfectR method). 198 | 199 | :param egs: Bandgaps of the subcells in eV, order from highest to lowest 200 | :param flux: Flux of the solar spectrum in W/m^2/nm 201 | :param wl: Wavelengths of the solar spectrum in nm 202 | :param interval: Wavelength interval of the solar spectrum in nm 203 | :param rad_eff: Radiative efficiency of the cell (0-1) 204 | :param upperE: upper limit (in eV) for integrating over photon flux 205 | :param x: List of parameters for the reflection peaks 206 | :param n_peaks: Number of reflection peaks 207 | """ 208 | 209 | # Since we need previous Eg info have to iterate the Jsc array 210 | jscs = np.empty_like(egs, dtype=float) # Quick way of defining jscs with same dimensions as egs 211 | j01s = np.zeros_like(egs, dtype=float) 212 | 213 | R = gen_spectrum_ndip(x, n_peaks, wl, 1, 0) 214 | 215 | upperE_j01 = upperE 216 | 217 | for i, eg in enumerate(egs): 218 | 219 | wl_inds = np.all((wl < 1240 / eg, wl > 1240 / upperE_j01), axis=0) 220 | wl_slice = wl[wl_inds] 221 | # print("numerical", eg, np.sum(wl_inds)) 222 | j01s[i] = (pref_wl / rad_eff) * np.trapz( 223 | (1 - R[wl_inds]) * np.exp(-wl_exp_const / wl_slice) / (wl_slice) ** 4, 224 | wl_slice) 225 | 226 | jscs[i] = ( 227 | q 228 | * np.sum(flux[np.all((wl < 1240 / eg, wl > 1240 / upperE), axis=0)]) 229 | * interval 230 | ) 231 | 232 | upperE = eg 233 | 234 | Vmaxs = (kbT * (lambertw(e * (jscs / j01s)) - 1)).real 235 | Imaxs = jscs - j01s * np.exp(Vmaxs / (kbT)) 236 | 237 | return j01s, jscs, Vmaxs, Imaxs 238 | 239 | 240 | def getPmax( 241 | egs: Sequence[float], 242 | flux: np.ndarray, 243 | wl: np.ndarray, 244 | interval: float, 245 | x: Sequence[float] = None, 246 | rad_eff: int = 1, 247 | upperE: float = 4.43, 248 | method: str = "perfect_R", 249 | n_peaks: int = 2, 250 | ) -> float: 251 | """Calculates the maximum power (in W) of a multi-junction solar cell in the detailed-balance limit. 252 | 253 | :param egs: Bandgaps of the subcells in eV, order from highest to lowest 254 | :param flux: Flux of the solar spectrum in W/m^2/nm 255 | :param wl: Wavelengths of the solar spectrum in nm 256 | :param interval: Wavelength interval of the solar spectrum in nm 257 | :param x: population vector defining the reflection peaks 258 | :param rad_eff: Radiative efficiency of the cell (0-1) 259 | :param upperE: upper limit (in eV) for integrating over photon flux 260 | :param method: Method for calculating the maximum power point. Current options are 261 | "perfect_R" and "no_R": in the first case, integration by parts is used to 262 | calculate the recombination current (dark current) accurately for rectangular 263 | reflection peaks, in the second case the reflection peaks are ignored when 264 | calculating the recombination current. In any case, reflection peaks are taken 265 | into account when calculating the Jsc (light-generated current). 266 | :param n_peaks: Number of peaks in the reflection spectrum 267 | 268 | """ 269 | 270 | db_cell_calculation = { 271 | 'perfect_R': db_cell_calculation_perfectR, 272 | 'no_R': db_cell_calculation_noR, 273 | 'numerical_R': db_cell_calculation_numericalR, 274 | } 275 | 276 | j01s, jscs, Vmaxs, Imaxs = db_cell_calculation[method](egs, flux, wl, interval, 277 | rad_eff, upperE, x, n_peaks) 278 | 279 | # Find the minimum Imaxs 280 | minImax = np.amin(Imaxs) 281 | 282 | # Find tandem voltage 283 | 284 | if np.any(minImax > jscs) or np.any(j01s < 0): 285 | # vsubcell = kbT * np.log((jscs - minImax) / j01s) 286 | 287 | # vTandem = np.sum(vsubcell) 288 | # print("vTandem", vTandem, np.isnan(vTandem), vTandem*(~np.isnan(vTandem))) 289 | # print(vsubcell, jscs, minImax, j01s) 290 | # print(vTandem*(~np.isnan(vTandem))*np.all(vsubcell > 0)) 291 | return 0 292 | 293 | else: 294 | vsubcell = kbT * np.log((jscs - minImax) / j01s) 295 | 296 | vTandem = np.sum(vsubcell) 297 | 298 | return vTandem * minImax 299 | 300 | 301 | def getIVmax( 302 | egs: Sequence[float], 303 | flux: np.ndarray, 304 | wl: np.ndarray, 305 | interval: float, 306 | x: Sequence[float] = None, 307 | rad_eff: int = 1, 308 | upperE: float = 4.43, 309 | method: str = "perfect_R", 310 | n_peaks: int = 2, 311 | ) -> tuple: 312 | """Calculates the voltages and currents of each junction in a multi-junction cell at the maximum power point 313 | in the detailed-balance limit. 314 | 315 | :param egs: Bandgaps of the subcells in eV, order from highest to lowest 316 | :param flux: Flux of the solar spectrum in W/m^2/nm 317 | :param wl: Wavelengths of the solar spectrum in nm 318 | :param interval: Wavelength interval of the solar spectrum in nm 319 | :param x: population vector defining the reflection peaks 320 | :param rad_eff: Radiative efficiency of the cell (0-1) 321 | :param upperE: upper limit (in eV) for integrating over photon flux 322 | :param method: Method for calculating the maximum power point. Current options are 323 | "perfect_R" and "no_R": in the first case, integration by parts is used to 324 | calculate the recombination current (dark current) accurately for rectangular 325 | reflection peaks, in the second case the reflection peaks are ignored when 326 | calculating the recombination current. In any case, reflection peaks are taken 327 | into account when calculating the Jsc (light-generated current). 328 | :param n_peaks: Number of peaks in the reflection spectrum 329 | 330 | """ 331 | 332 | db_cell_calculation = { 333 | 'perfect_R': db_cell_calculation_perfectR, 334 | 'no_R': db_cell_calculation_noR 335 | } 336 | 337 | _, _, Vmaxs, Imaxs = db_cell_calculation[method](egs, flux, wl, interval, 338 | rad_eff, upperE, x, n_peaks) 339 | 340 | 341 | return Vmaxs, Imaxs 342 | 343 | 344 | def getIVtandem( 345 | egs: Sequence[float], 346 | flux: np.ndarray, 347 | wl: np.ndarray, 348 | interval: float, 349 | x: Sequence[float], 350 | rad_eff: int = 1, 351 | upperE: float = 4.43, 352 | method: str = "perfect_R", 353 | n_peaks: int = 2, 354 | ) -> tuple: 355 | 356 | """Calculates the overall voltage and current at the maximum power point of multi-junction cell 357 | in the detailed-balance limit. 358 | 359 | :param egs: Bandgaps of the subcells in eV, order from highest to lowest 360 | :param flux: Flux of the solar spectrum in W/m^2/nm 361 | :param wl: Wavelengths of the solar spectrum in nm 362 | :param interval: Wavelength interval of the solar spectrum in nm 363 | :param x: population vector defining the reflection peaks 364 | :param rad_eff: Radiative efficiency of the cell (0-1) 365 | :param upperE: upper limit (in eV) for integrating over photon flux 366 | :param method: Method for calculating the maximum power point. Current options are 367 | "perfect_R" and "no_R": in the first case, integration by parts is used to 368 | calculate the recombination current (dark current) accurately for rectangular 369 | reflection peaks, in the second case the reflection peaks are ignored when 370 | calculating the recombination current. In any case, reflection peaks are taken 371 | into account when calculating the Jsc (light-generated current). 372 | :param n_peaks: Number of peaks in the reflection spectrum 373 | 374 | """ 375 | 376 | db_cell_calculation = { 377 | 'perfect_R': db_cell_calculation_perfectR, 378 | 'no_R': db_cell_calculation_noR 379 | } 380 | 381 | j01s, jscs, Vmaxs, Imaxs = db_cell_calculation[method](egs, flux, wl, interval, 382 | rad_eff, upperE, x, n_peaks) 383 | 384 | # Find the minimum Imaxs 385 | minImax = np.amin(Imaxs) 386 | 387 | # Find tandem voltage 388 | 389 | vsubcell = kbT * np.log((jscs - minImax) / j01s) 390 | vTandem = np.sum(vsubcell) 391 | 392 | return vTandem, np.min(Imaxs) 393 | -------------------------------------------------------------------------------- /examples/gamut_luminance_fixedEg.py: -------------------------------------------------------------------------------- 1 | from ecopv.main_optimization import ( 2 | load_colorchecker, 3 | multiple_color_cells, 4 | cell_optimization) 5 | from ecopv.optimization_functions import getIVtandem 6 | import numpy as np 7 | from colormath.color_conversions import convert_color 8 | from colormath.color_objects import xyYColor, XYZColor 9 | from solcore.light_source import LightSource 10 | import matplotlib.pyplot as plt 11 | import pygmo as pg 12 | from os import path 13 | from cycler import cycler 14 | from ecopv.plot_utilities import * 15 | from colour import wavelength_to_XYZ 16 | import os 17 | from time import time 18 | 19 | from matplotlib import rc 20 | rc("font", **{"family": "sans-serif", 21 | "sans-serif": ["Helvetica"], 22 | "size": 13}) 23 | 24 | 25 | # in such a large number of runs, get one or two values which are not converged. Try 26 | # to avoid this by enforcing a minimum efficiency? A point should always be better than 27 | # the one diagonally below to the left of it (i.e. x-1, y-1 coordinates). 28 | 29 | 30 | def wl_gamut_plot(ax): 31 | label_wls = np.arange(460, 660, 20) 32 | 33 | # XYZlab = wavelength_to_XYZ(label_wls) 34 | 35 | # sumXYZlab = np.sum(XYZlab, axis=1) 36 | 37 | # xgl = XYZlab[:, 0] / sumXYZlab 38 | # ygl = XYZlab[:, 1] / sumXYZlab 39 | 40 | tick_orig = np.zeros((len(label_wls), 2)) 41 | tick_dir = np.zeros((len(label_wls), 2)) 42 | # create ticks 43 | for m1, lwl in enumerate(label_wls): 44 | p0 = wavelength_to_XYZ(lwl) 45 | p1 = wavelength_to_XYZ(lwl - 1) 46 | p2 = wavelength_to_XYZ(lwl + 1) 47 | 48 | p0 = p0 / np.sum(p0) 49 | p1 = p1 / np.sum(p1) 50 | p2 = p2 / np.sum(p2) 51 | 52 | m = np.array([p2[0] - p1[0], p2[1] - p1[1]]) 53 | mp = np.array([-m[1], m[0]]) 54 | mp = mp / np.linalg.norm(mp) 55 | # b = p1[1] + (1/m) * p1[0] 56 | 57 | tick_orig[m1] = p0[:2] 58 | tick_dir[m1] = p0[:2] + 0.02 * mp 59 | 60 | ax.set_aspect("equal") 61 | 62 | ax.plot(xg, yg, "k") 63 | ax.plot([xg[0], xg[-1]], [yg[0], yg[-1]], "k") 64 | 65 | for m1, lwl in enumerate(label_wls): 66 | ax.plot( 67 | [tick_orig[m1, 0], tick_dir[m1, 0]], 68 | [tick_orig[m1, 1], tick_dir[m1, 1]], 69 | "-k", 70 | ) 71 | 72 | if lwl > 520: 73 | ax.text(*tick_dir[m1], str(lwl), rotation=35) 74 | 75 | elif lwl == 520: 76 | ax.text(*tick_dir[m1], str(lwl), horizontalalignment="center") 77 | 78 | else: 79 | ax.text( 80 | *tick_dir[m1], 81 | str(lwl), 82 | horizontalalignment="right", 83 | verticalalignment="center" 84 | ) 85 | 86 | ax.set_xlim(-0.09, 0.85) 87 | ax.set_ylim(-0.07, 0.9) 88 | ax.set_xlabel("x") 89 | ax.set_ylabel("y") 90 | 91 | force_rerun = False 92 | Ys = [0.25, 0.50, 0.75] 93 | # Ys = [0.75] 94 | wl_vis = np.linspace(360, 780, 500) 95 | 96 | XYZ = wavelength_to_XYZ(wl_vis) 97 | 98 | sumXYZ = np.sum(XYZ, axis=1) 99 | 100 | xg = XYZ[:, 0] / sumXYZ 101 | yg = XYZ[:, 1] / sumXYZ 102 | 103 | # xs = np.arange(np.min(xg), np.max(xg), 0.01) 104 | # ys = np.arange(np.min(yg), np.max(yg), 0.01) 105 | 106 | xs = np.arange(np.min(xg), np.max(xg), 0.02) 107 | ys = np.arange(np.min(yg), np.max(yg), 0.02) 108 | 109 | is_inside = np.full((len(xs), len(ys)), False) 110 | 111 | peak = np.argmax(yg) 112 | 113 | left_edge = [xg[:peak], yg[:peak]] 114 | right_edge = [xg[peak:], yg[peak:]] 115 | 116 | 117 | # now check if the points are inside the gamut defined by the spectral locus 118 | 119 | for j, yc in enumerate(ys): 120 | left_y = np.argmin(np.abs(left_edge[1] - yc)) 121 | right_y = np.argmin(np.abs(right_edge[1] - yc)) 122 | 123 | left_x = left_edge[0][left_y] 124 | right_x = right_edge[0][right_y] 125 | is_inside[np.all((xs > left_x, xs < right_x), axis=0), j] = True 126 | 127 | # eliminate everything below the line of purples: 128 | 129 | # equation for line of purples: 130 | 131 | slope = (yg[-1] - yg[0]) / (xg[-1] - xg[0]) 132 | c = yg[0] - slope * xg[0] 133 | 134 | for j, yc in enumerate(ys): 135 | above = yc > slope * xs + c 136 | is_inside[:, j] = np.all((above, is_inside[:, j]), axis=0) 137 | 138 | # plt.figure() 139 | # plt.plot(xg, yg) 140 | # for j1, x in enumerate(xs): 141 | # for k1, y in enumerate(ys): 142 | # if is_inside[j1, k1]: 143 | # plt.plot(x, y, "o", color="black") 144 | # 145 | # plt.show() 146 | print("Inside gamut:", np.sum(is_inside)) 147 | 148 | col_thresh = 0.004 # for a wavelength interval of 0.1, minimum achievable color 149 | # error will be (very rough estimate!) ~ 0.001. 150 | # This is the maximum allowed fractional error in X, Y, or Z colour coordinates. 151 | 152 | acceptable_eff_change = 1e-4 # how much can the efficiency (in %) change between iteration sets? Stop when have reached 153 | # col_thresh and efficiency change is less than this. 154 | 155 | n_trials = 8 # number of islands which will run concurrently in parallel 156 | interval = 0.1 # wavelength interval (in nm) 157 | wl_cell = np.arange( 158 | 300, 4000, interval 159 | ) # wavelengths used for cell calculations (range of wavelengths in AM1.5G solar 160 | # spectrum. For calculations relating to colour perception, only the visible range ( 161 | # 380-730 nm) will be used. 162 | 163 | n_peaks = 2 164 | 165 | initial_iters = 100 # number of initial evolutions for the archipelago 166 | add_iters = 100 # additional evolutions added each time if color threshold/convergence condition not met 167 | # every color will run a minimum of initial_iters + add_iters evolutions before ending! 168 | 169 | max_trials_col = ( 170 | 50 * add_iters 171 | ) # how many population evolutions happen before giving up if there are no populations 172 | # which meet the color threshold 173 | 174 | R_type = "sharp" # "sharp" for rectangular dips or "gauss" for gaussians 175 | fixed_height = True # fixed height peaks (will be at the value of max_height) if True, or peak height is an optimization 176 | # variable if False 177 | light_source_name = "AM1.5g" 178 | j01_method = "perfect_R" 179 | 180 | max_height = ( 181 | 1 # maximum height of reflection peaks; fixed at this value of fixed_height = True 182 | ) 183 | base = 0 # baseline fixed reflection (fixed at this value for both fixed_height = True and False). 184 | 185 | n_junctions = 1 # loop through these numbers of junctions 186 | 187 | # Use AM1.5G spectrum: 188 | light_source = LightSource( 189 | source_type="standard", 190 | version=light_source_name, 191 | x=wl_cell, 192 | output_units="photon_flux_per_nm", 193 | ) 194 | 195 | photon_flux_cell = np.array(light_source.spectrum(wl_cell)) 196 | 197 | shapes = ["+", "o", "^", ".", "*", "v", "s", "x"] 198 | 199 | loop_n = 0 200 | 201 | # precalculate optimal bandgaps for junctions: 202 | save_path = path.join(path.dirname(path.abspath(__file__)), "results") 203 | 204 | 205 | # fixed_bandgaps = [1.90, 1.44, 0.67] 206 | fixed_bandgaps = [1.12] 207 | 208 | if __name__ == "__main__": 209 | start = time() 210 | # Need this __main__ construction because otherwise the parallel running of the different islands (n_trials) may throw an error 211 | 212 | fig, (axs, axs2, axs3) = plt.subplots(3, len(Ys), figsize=(len(Ys) * 4, 8), 213 | gridspec_kw={'height_ratios': [1, 1, 0.1]}) 214 | if len(Ys) == 1: 215 | axs = [axs] 216 | 217 | for i1, Y in enumerate(Ys): 218 | 219 | if os.path.exists( 220 | save_path + "/pop_gamut_Y_{}_{}_{}_rough.npy".format(Y, n_junctions, "Si") 221 | ) and not force_rerun: 222 | print("Load existing result") 223 | 224 | pop_array = np.load( 225 | save_path + "/pop_gamut_Y_{}_{}_{}_rough.npy".format(Y, n_junctions, "Si") 226 | ) 227 | eff_array = np.load( 228 | save_path + "/eff_gamut_Y_{}_{}_{}_rough.npy".format(Y, n_junctions, "Si") 229 | ) 230 | 231 | else: 232 | col_possible_str = save_path + "/possible_colours_Y_{}_rough.txt".format(Y) 233 | 234 | print("Found existing file for possible colours") 235 | is_possible = np.loadtxt(col_possible_str) 236 | print("Possible colours:", np.sum(is_possible)) 237 | 238 | best_population = np.load( 239 | save_path + "/possible_colours_Y_{}_populations_rough.npy".format( 240 | Y)) 241 | # print(is_possible) 242 | eff_array = np.zeros((len(xs), len(ys))) 243 | pop_array = np.zeros((len(xs), len(ys), 4 + n_junctions)) 244 | 245 | for j1, x in enumerate(xs): 246 | for k1, y in enumerate(ys): 247 | if is_inside[j1, k1] and is_possible[j1, k1]: 248 | XYZ = np.array( 249 | [ 250 | convert_color( 251 | xyYColor(x, y, Y), XYZColor 252 | ).get_value_tuple() 253 | ] 254 | ) 255 | print(XYZ) 256 | seed_pop = best_population[j1, k1] 257 | 258 | if j1 > 0 and k1 > 0: 259 | minimum_eff = [0.99*eff_array[j1-1, k1-1]] 260 | else: 261 | minimum_eff = [0] 262 | 263 | print("minimum eff:", minimum_eff) 264 | 265 | 266 | result = multiple_color_cells( 267 | XYZ, 268 | [str(j1) + "_" + str(k1)], 269 | photon_flux_cell, 270 | n_peaks=n_peaks, 271 | n_junctions=n_junctions, 272 | R_type=R_type, 273 | fixed_height=fixed_height, 274 | n_trials=n_trials, 275 | initial_iters=initial_iters, 276 | add_iters=add_iters, 277 | col_thresh=col_thresh, 278 | acceptable_eff_change=acceptable_eff_change, 279 | max_trials_col=max_trials_col, 280 | base=base, 281 | max_height=max_height, 282 | fixed_bandgaps=fixed_bandgaps, 283 | plot=False, 284 | j01_method = j01_method, 285 | minimum_eff = minimum_eff, 286 | illuminant=light_source_name, 287 | seed_population=[seed_pop], 288 | ) 289 | 290 | champion_effs = result["champion_eff"] 291 | champion_pops = result["champion_pop"] 292 | 293 | print("champion effs", champion_effs) 294 | 295 | eff_array[j1, k1] = champion_effs 296 | pop_array[j1, k1, :] = champion_pops 297 | if np.max(champion_effs) == 0: 298 | print("SETTING TO 0") 299 | is_possible[j1, k1] = False 300 | 301 | print("sum:", np.sum(is_possible)) 302 | 303 | np.save( 304 | save_path + "/pop_gamut_Y_{}_{}_{}_rough.npy".format(Y, n_junctions, "Si"), 305 | pop_array, 306 | ) 307 | np.save( 308 | save_path + "/eff_gamut_Y_{}_{}_{}_rough.npy".format(Y, n_junctions, "Si"), 309 | eff_array, 310 | ) 311 | 312 | width = np.diff(xs)[0] 313 | height = np.diff(ys)[0] 314 | 315 | eff_mask = eff_array > 0 316 | 317 | Egs = pop_array[:, :, -1] 318 | Egs[~eff_mask] = np.nan 319 | Egs[Egs < 1.12] = np.nan 320 | 321 | standard_illuminant = [0.3128, 0.3290, Y] 322 | XYZ = convert_color(xyYColor(*standard_illuminant), XYZColor) 323 | s_i_RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 324 | s_i_RGB[s_i_RGB > 1] = 1 325 | 326 | axs[i1].set_facecolor(s_i_RGB) 327 | wl_gamut_plot(axs[i1]) 328 | 329 | 330 | for j1, x in enumerate(xs): 331 | for k1, y in enumerate(ys): 332 | 333 | if ~np.isnan(eff_array[j1, k1]) and eff_array[j1, k1] > 0: 334 | XYZ = convert_color(xyYColor(x, y, Y), XYZColor) 335 | RGB = np.array(convert_color(XYZ, sRGBColor).get_value_tuple()) 336 | 337 | RGB[RGB > 1] = 1 338 | # plt.plot(x, y, '.') 339 | axs[i1].add_patch( 340 | Rectangle( 341 | xy=(x - width / 2, y - height / 2), 342 | width=width, 343 | height=height, 344 | facecolor=RGB, 345 | ) 346 | ) 347 | # ax.text(x, y, str(round(eff_array[j1, k1], 2)), color='k', ha='center', va='center') 348 | # 349 | # plt.plot(x, y, 'k.') 350 | 351 | # levels = np.arange(np.ceil(np.min(eff_array[eff_array>0]))+2, 1.01*np.floor(np.nanmax(eff_array[eff_array>0])), 1) 352 | 353 | lower_end = np.ceil(0.98 * np.max(eff_array) - 2) 354 | upper_end = np.ceil(np.max(eff_array) - 1) 355 | levels = np.arange(lower_end, upper_end, 0.5) 356 | 357 | within_99_p = 0.99 * np.max(eff_array) 358 | # print(np.min(eff_array[eff_array>0]), np.max(eff_array)) 359 | cs = axs[i1].contour(xs, ys, eff_array.T, levels=levels, colors="k", alpha=0.7) 360 | 361 | cs2 = axs[i1].contour( 362 | xs, ys, eff_array.T, levels=[within_99_p], colors="firebrick", 363 | linestyles="dashed" 364 | ) 365 | 366 | label_loc = [] 367 | 368 | for seg in cs.allsegs: 369 | x_lower = seg[0][:, 0][seg[0][:, 1] < 0.6] 370 | y_lower = seg[0][:, 1][seg[0][:, 1] < 0.6] 371 | 372 | close_x = np.argmin(np.abs(x_lower - 0.35)) 373 | close_y = y_lower[close_x] 374 | label_loc.append([0.35, close_y]) 375 | 376 | max_loc = np.unravel_index(np.argmax(eff_array), eff_array.shape) 377 | axs[i1].plot(xs[max_loc[0]], ys[max_loc[1]], "o", markersize=8, 378 | color="firebrick", 379 | markerfacecolor="none", markeredgewidth=2) 380 | 381 | if Y < 0.75: 382 | axs[i1].text(xs[max_loc[0]] + 0.015, ys[max_loc[1]] - 0.025, 383 | np.round(eff_array[max_loc], 1), 384 | ha="right", va="top", fontsize=14, color="firebrick", weight="bold") 385 | 386 | else: 387 | axs[i1].text(xs[max_loc[0]] + 0.035, ys[max_loc[1]] + 0.02, 388 | np.round(eff_array[max_loc], 1), 389 | ha="left", va="center", fontsize=14, color="firebrick", 390 | weight="bold") 391 | 392 | axs[i1].clabel(cs, inline=1, fontsize=12, manual=label_loc) 393 | axs[i1].set_title("Y = " + str(Y)) 394 | axs[i1].yaxis.set_minor_locator(tck.AutoMinorLocator()) 395 | axs[i1].xaxis.set_minor_locator(tck.AutoMinorLocator()) 396 | axs[i1].grid(axis="both", color="0.4", alpha=0.5) 397 | axs[i1].tick_params(direction="in", which="both", top=True, right=True) 398 | axs[i1].set_axisbelow(True) 399 | 400 | # plt.figure() 401 | # plt.imshow(eff_array.T, vmin=np.min(eff_array[eff_array > 0]), origin='lower') 402 | # plt.colorbar() 403 | # plt.show() 404 | 405 | wl_gamut_plot(axs2[i1]) 406 | c = axs2[i1].pcolor(xs, ys, Egs.T, vmin=1.55, vmax=1.7, cmap="inferno_r") 407 | axs3[i1].axis("off") 408 | 409 | if i1 == 1: 410 | fig.colorbar(c, ax=axs3[i1], orientation="horizontal", fraction=1, 411 | ticks=np.arange(1.55, 1.71, 0.05)) 412 | axs3[i1-1].text(1.1, 0.7, "Bandgap (eV)", ha="right", va="center") 413 | 414 | axs2[i1].grid(axis="both", color="0.4", alpha=0.5) 415 | 416 | 417 | fig.savefig("efficiency_colour_gamut.pdf", bbox_inches="tight") 418 | plt.tight_layout() 419 | plt.show() 420 | 421 | print("TIME: ", time() - start) 422 | 423 | --------------------------------------------------------------------------------