Example: Synthetic module EL IV

This example shows how to build synthetic module EL IVs using single cell SDM parameters

[1]:
import os
from pathlib import Path

project_root = Path.cwd().parents[1]
os.chdir(project_root)   # now cwd is .../pvcracks

from pvcracks.pvspice_lite.pvspice_tools import run_ngspice
from pvcracks.pvspice_lite.pvspice_tools import ngpsice_read_voltage_current_modules

from pvcracks.pvspice_lite.pvspice_tools import Create_Cell_NetCode
from pvcracks.pvspice_lite.pvspice_tools import MiniMod_Spice

from pvcracks.pvspice_lite.pvspice_helper import Read_IV
from pvcracks.pvspice_lite.pvspice_helper import Read_EL
from pvcracks.pvspice_lite.pvspice_helper import cells2Mod
from pvcracks.pvspice_lite.pvspice_helper import Extract_Params


import numpy as np
import pandas as pd
import cv2
import matplotlib.pyplot as plt
import pvlib
import scipy

Install ngspice

Install ngspice with conda: - conda install ngspice Find path (e.g. “/home//.conda/envs/pyhpc_spice/bin/ngspice”) Edit the spicepath below to be that path

[2]:
# spicepath = <your spice path>
# spicepath = '/home/bkbyfor/anaconda3/envs/Things/bin/ngspice'
spicepath = "/home/nrjost/.conda/envs/pyhpc_spice/bin/ngspice"

Read Data

[3]:
# Master DataFrame
Cell9Master = pd.read_csv('docs/data/ELdata_module_209.csv', index_col=0)
# 3x3 minimodule IVs
FM_Init = Read_IV('docs/data/Minimodule_IVs/209_s0_FM_1000.csv')
FM_Deg1 = Read_IV('docs/data/Minimodule_IVs/209_s1_FM_1000.csv')
FM_Deg2 = Read_IV('docs/data/Minimodule_IVs/209_s2_FM_1000.csv')
[4]:
Cell9Master.head()
[4]:
ELPath Module Deg Rs Rsh I Is N Pmp Vmp Imp
0 /docs/data/EL/209_A3/Init/209_A3_0005_2021_04_... 209_A3 Init 0.008625 525.0 8.242323 6.545686e-08 1.292650 3.564256 0.465455 7.657580
1 /docs/data/EL/209_C1/Init/209_C1_0005_2021_04_... 209_C1 Init 0.009957 1543.5 8.201203 3.488834e-08 1.267408 3.547602 0.465455 7.621802
2 /docs/data/EL/209_A2/Init/209_A2_0005_2021_04_... 209_A2 Init 0.008988 1380.5 8.223689 6.476966e-08 1.293979 3.543461 0.465455 7.612904
3 /docs/data/EL/209_A1/Init/209_A1_0005_2021_04_... 209_A1 Init 0.009377 1789.5 8.185086 3.607070e-08 1.254117 3.524153 0.465455 7.571422
4 /docs/data/EL/209_C2/Init/209_C2_0005_2021_04_... 209_C2 Init 0.009753 1723.0 8.122330 2.261230e-08 1.224717 3.494077 0.465455 7.506806

Cell9Master description

ElPath : Path to the EL image of the cell
Module : Name of the cell
Deg    : Degredation step of Mini Module

SDM params found through fitting of the measured cell IVs

Rs  : Series Resistance
Rsh : Shunt Resistance
I   : Current
Is  : Dark Current
N   : Diode Ideality Factor

Display Single Cell Fits

[5]:
# iterate over the dataframe to display each entry
for ind, row in Cell9Master.iterrows():
    # File Path for writing Spice files
    file_path = f"{os.getcwd()}/pvcracks/pvspice_lite/"
    # Read image
    img = Read_EL(f"{os.getcwd()}{row.ELPath}")
    # Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
    Params =  Extract_Params(Cell9Master.loc[ind:ind])
    # Voltage points for Spice simulation
    V = np.linspace(0, 0.64, 100)
    # Spice simulation for series connected SDM cells
    IV_Pred = MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

    #Calculate maximum power point values
    V_sim = IV_Pred[0]
    I_sim = IV_Pred[1]
    P = V_sim * I_sim
    idx_mpp = np.argmax(P)
    Pmp = P[idx_mpp]

    # Plot Findings
    fig, axs = plt.subplots(1, 2, figsize = (12,5))
    fig.suptitle(f'Cell {row.Module} Deg {row.Deg}')
    axs[0].imshow(img)
    axs[0].set_title('EL image')
    axs[1].plot(IV_Pred[0], IV_Pred[1], label = 'Spice IV')
    axs[1].text(
        0.75, 0.90, f"Pmp={Pmp:.3f}W",
        transform=axs[1].transAxes,
        fontsize=10,
        verticalalignment='top',
        bbox=dict(boxstyle='round', facecolor='white', alpha=0.7)
    )
    axs[1].set_xlabel('Voltage')
    axs[1].set_ylabel('Current')
    axs[1].set_title('IV Curves')
    axs[1].legend()

    plt.show()
../_images/Examples_example_minimodule_build_9_0.png
../_images/Examples_example_minimodule_build_9_1.png
../_images/Examples_example_minimodule_build_9_2.png
../_images/Examples_example_minimodule_build_9_3.png
../_images/Examples_example_minimodule_build_9_4.png
../_images/Examples_example_minimodule_build_9_5.png
../_images/Examples_example_minimodule_build_9_6.png
../_images/Examples_example_minimodule_build_9_7.png
../_images/Examples_example_minimodule_build_9_8.png
../_images/Examples_example_minimodule_build_9_9.png
../_images/Examples_example_minimodule_build_9_10.png
../_images/Examples_example_minimodule_build_9_11.png
../_images/Examples_example_minimodule_build_9_12.png
../_images/Examples_example_minimodule_build_9_13.png
../_images/Examples_example_minimodule_build_9_14.png
../_images/Examples_example_minimodule_build_9_15.png
../_images/Examples_example_minimodule_build_9_16.png
../_images/Examples_example_minimodule_build_9_17.png
../_images/Examples_example_minimodule_build_9_18.png
../_images/Examples_example_minimodule_build_9_19.png
../_images/Examples_example_minimodule_build_9_20.png
../_images/Examples_example_minimodule_build_9_21.png
../_images/Examples_example_minimodule_build_9_22.png
../_images/Examples_example_minimodule_build_9_23.png
../_images/Examples_example_minimodule_build_9_24.png
../_images/Examples_example_minimodule_build_9_25.png
../_images/Examples_example_minimodule_build_9_26.png

Simulate 3x3 MiniMod

Initial

[6]:
# query the slice of initial readings sorted by the Module name
Init_Cells = Cell9Master.loc[Cell9Master.Deg == 'Init'].sort_values(by=['Module'])
[7]:
# File Path for writing Spice files
file_path = f"{os.getcwd()}/pvcracks/pvspice_lite/"
# Construct Image for cells into module
MMEL = cells2Mod(Init_Cells,(400,400),3,3)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Init_Cells)
# Spice simulation for series connected SDM cells
IV_Pred =  MiniMod_Spice(Params, file_path, FM_Init[0], V_Step=0.001, file_name ='', spicepath=spicepath)
# unpack measured vs. predicted
V_meas, I_meas = FM_Init
V_sp, I_sp = IV_Pred
# compute Power curves
P_meas = V_meas * I_meas
P_sp = V_sp * I_sp
# locate maximum‐power indices
idx_meas = np.argmax(P_meas)
idx_sp = np.argmax(P_sp)
# extract MPPs
Pmp_meas = P_meas[idx_meas]
Pmp_sp = P_sp[idx_sp]
# Plot Findigs
fig, axs = plt.subplots(1, 3, figsize = (17, 5))
axs[0].imshow(MMEL)
axs[0].set_title('EL image')
axs[1].plot(V_meas, I_meas, label = f'Measured IV (Pmpp={Pmp_meas:.3f} W)')
axs[1].plot(V_sp, I_sp, label = f'Spice IV (Pmpp={Pmp_sp:.3f} W)')
axs[1].set_xlabel('Voltage')
axs[1].set_ylabel('Current')
axs[1].set_title('IV Curves')
axs[1].legend()
axs[2].set_title('Residual')
axs[2].plot(FM_Init[0],FM_Init[1] - IV_Pred[1])
axs[2].set_ylabel('Current')
axs[2].set_xlabel('Sample')
[7]:
Text(0.5, 0, 'Sample')
../_images/Examples_example_minimodule_build_13_1.png

Deg1

[8]:
# query the slice of Deg 1 readings sorted by the Module name
Deg1_Cells = Cell9Master.loc[Cell9Master.Deg == 'Deg1'].sort_values(by=['Module'])
[9]:
# File Path for writing Spice files
file_path = f"{os.getcwd()}/pvcracks/pvspice_lite/"
# Construct Image for cells into module
MMEL = cells2Mod(Deg1_Cells,(400,400),3,3)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Deg1_Cells)
# Spice simulation for series connected SDM cells
IV_Pred =  MiniMod_Spice(Params, file_path, FM_Deg1[0], V_Step=0.001, file_name ='', spicepath=spicepath)
# unpack measured vs. predicted
V_meas, I_meas = FM_Deg1
V_sp, I_sp = IV_Pred
# compute Power curves
P_meas = V_meas * I_meas
P_sp = V_sp * I_sp
# locate maximum‐power indices
idx_meas = np.argmax(P_meas)
idx_sp = np.argmax(P_sp)
# extract MPPs
Pmp_meas = P_meas[idx_meas]
Pmp_sp = P_sp[idx_sp]
# Plot Findigs
fig, axs = plt.subplots(1, 3, figsize = (17, 5))
axs[0].imshow(MMEL)
axs[0].set_title('EL image')
axs[1].plot(V_meas, I_meas, label = f'Measured IV (Pmpp={Pmp_meas:.3f} W)')
axs[1].plot(V_sp, I_sp, label = f'Spice IV    (Pmpp={Pmp_sp:.3f} W)')
axs[1].set_xlabel('Voltage')
axs[1].set_ylabel('Current')
axs[1].set_title('IV Curves')
axs[1].legend()
axs[2].set_title('Residual')
axs[2].plot(FM_Deg1[0],FM_Deg1[1] - IV_Pred[1])
axs[2].set_ylabel('Current')
axs[2].set_xlabel('Sample')
[9]:
Text(0.5, 0, 'Sample')
../_images/Examples_example_minimodule_build_16_1.png

Deg 2

[10]:
# query the slice of Deg 2 readings sorted by the Module name
Deg2_Cells = Cell9Master.loc[Cell9Master.Deg == 'Deg2'].sort_values(by=['Module'])
[11]:
# File Path for writing Spice files
file_path = f"{os.getcwd()}/pvcracks/pvspice_lite/"
# Construct Image for cells into module
MMEL = cells2Mod(Deg2_Cells,(400,400),3,3)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Deg2_Cells)
# Spice simulation for series connected SDM cells
IV_Pred =  MiniMod_Spice(Params, file_path, FM_Deg2[0], V_Step=0.001, file_name ='', spicepath=spicepath)
# unpack measured vs. predicted
V_meas, I_meas = FM_Deg2
V_sp, I_sp = IV_Pred
# compute Power curves
P_meas = V_meas * I_meas
P_sp = V_sp * I_sp
# locate maximum‐power indices
idx_meas = np.argmax(P_meas)
idx_sp = np.argmax(P_sp)
# extract MPPs
Pmp_meas = P_meas[idx_meas]
Pmp_sp = P_sp[idx_sp]
# Plot Findigs
fig, axs = plt.subplots(1, 3, figsize = (17, 5))
axs[0].imshow(MMEL)
axs[0].set_title('EL image')
axs[1].plot(V_meas, I_meas, label = f'Measured IV (Pmpp={Pmp_meas:.3f} W)')
axs[1].plot(V_sp, I_sp, label = f'Spice IV (Pmpp={Pmp_sp:.3f} W)')
axs[1].set_xlabel('Voltage')
axs[1].set_ylabel('Current')
axs[1].set_title('IV Curves')
axs[1].legend()
axs[2].set_title('Residual')
axs[2].plot(FM_Deg2[0],FM_Deg2[1] - IV_Pred[1])
axs[2].set_ylabel('Current')
axs[2].set_xlabel('Sample')
[11]:
Text(0.5, 0, 'Sample')
../_images/Examples_example_minimodule_build_19_1.png

Constructing Synthetic MiniModule Data

[12]:
# Randomly select 4 init cells and combine them into a single dataframe
Synth_MiniMod_Init = Init_Cells.sample(n=4)
# Select the damaged cell (top left corner) in Deg1 and repeat it 4 times
Synth_MiniMod_Deg1 = Deg1_Cells.loc[[12,12,12,12]]
# Select the damaged cell (top left corner) in Deg2 and repeat it 4 times
Synth_MiniMod_Deg2 = Deg2_Cells.loc[[21,21,21,21]]
# Mix 2 initial cells and the most damaged cell in Deg2
Synth_MiniMod_Mix =  pd.concat((Deg2_Cells.loc[[21,21]],Init_Cells.sample(n=2) ) )
[13]:
# File Path for writing Spice files
file_path = f"{os.getcwd()}/pvcracks/pvspice_lite/"
# Voltage over which to simulate
V = np.linspace(0, 0.64*4, 400)

# Construct Image for cells into module
MMEL_Mix = cells2Mod(Synth_MiniMod_Mix,(400, 400),2,2)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Mix)
# Spice simulation for series connected SDM cells
IV_Pred_Mix =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

# Construct Image for cells into module
MMEL_Init = cells2Mod(Synth_MiniMod_Init,(400, 400),2,2)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Init)
# Spice simulation for series connected SDM cells
IV_Pred_Init =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

# Construct Image for cells into module
MMEL_Deg1 = cells2Mod(Synth_MiniMod_Deg1,(400, 400),2,2)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Deg1)
# Spice simulation for series connected SDM cells
IV_Pred_Deg1 =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

# Construct Image for cells into module
MMEL_Deg2 = cells2Mod(Synth_MiniMod_Deg2,(400, 400),2,2)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Deg2)
# Spice simulation for series connected SDM cells
IV_Pred_Deg2 =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

def compute_Pmpp(iv_pred):
    Varr, Iarr = iv_pred
    Parr = Varr * Iarr
    return Parr.max()

# compute all four Pmpp values
Pmp_mix  = compute_Pmpp(IV_Pred_Mix)
Pmp_init = compute_Pmpp(IV_Pred_Init)
Pmp_deg1 = compute_Pmpp(IV_Pred_Deg1)
Pmp_deg2 = compute_Pmpp(IV_Pred_Deg2)

# 1×2 figures for each dataset
for MMEL, iv_pred, title, Pmp in [
    (MMEL_Mix,   IV_Pred_Mix,  "Mix Synthetic Data",  Pmp_mix),
    (MMEL_Init,  IV_Pred_Init, "Init Synthetic Data", Pmp_init),
    (MMEL_Deg1,  IV_Pred_Deg1, "Deg1 Synthetic Data", Pmp_deg1),
    (MMEL_Deg2,  IV_Pred_Deg2, "Deg2 Synthetic Data", Pmp_deg2),
]:
    fig, axs = plt.subplots(1, 2, figsize=(10,5))
    fig.suptitle(title)
    axs[0].imshow(MMEL)
    axs[0].axis('off')
    axs[0].set_title('EL image')

    axs[1].plot(iv_pred[0], iv_pred[1],
                label=f"{title.split()[0]} IV (Pmp={Pmp:.3f} W)")
    axs[1].set_xlabel('Voltage')
    axs[1].set_ylabel('Current')
    axs[1].set_title('IV Curves')
    axs[1].legend()
    plt.tight_layout(rect=[0,0,1,0.95])
    plt.show()

# overlay with ΔPmpp relative to Init
plt.figure(figsize=(8,6))
plt.title("Overlay of synthetic cell IVs")

# plot Init first, with its absolute Pmpp
plt.plot(IV_Pred_Init[0], IV_Pred_Init[1],
         label=f"Init (Pmp={Pmp_init:.3f} W)")

# plot the others, with ΔPmpp = Pmpp_x – Pmpp_init
for label, iv_pred, Pmp in [
    ('Mix',  IV_Pred_Mix,  Pmp_mix),
    ('Deg1', IV_Pred_Deg1, Pmp_deg1),
    ('Deg2', IV_Pred_Deg2, Pmp_deg2),
]:
    delta = (Pmp - Pmp_init)/Pmp_init*100
    plt.plot(iv_pred[0], iv_pred[1],
             label=f"{label} (ΔPmp={delta:+.2f}%)")

plt.xlabel('Voltage')
plt.ylabel('Current')
plt.legend()
plt.tight_layout()
plt.show()
../_images/Examples_example_minimodule_build_23_0.png
../_images/Examples_example_minimodule_build_23_1.png
../_images/Examples_example_minimodule_build_23_2.png
../_images/Examples_example_minimodule_build_23_3.png
../_images/Examples_example_minimodule_build_23_4.png
[14]:
# Randomly select 9 init cells and combine them into a single dataframe
Synth_MiniMod_Init = Init_Cells.sample(n=9)
# Select the damaged cell (top left corner) in Deg1 and repeat it 9 times
Synth_MiniMod_Deg1 = Deg1_Cells.loc[[12,12,12,12,12,12,12,12,12]]
# Select the damaged cell (top left corner) in Deg2 and repeat it 9 times
Synth_MiniMod_Deg2 = Deg2_Cells.loc[[21,21,21,21,21,21,21,21,21]]
# Mix 3 initial cells and the most damaged cell in Deg2
Synth_MiniMod_Mix =  pd.concat((Deg2_Cells.loc[[21,21,21,21,21,21]], Init_Cells.sample(n=3)))
[15]:
# File Path for writing Spice files
file_path = f"{os.getcwd()}/pvcracks/pvspice_lite/"
# Voltage over which to simulate
V = np.linspace(0, 0.64*9, 400)

# Construct Image for cells into module
MMEL_Mix = cells2Mod(Synth_MiniMod_Mix, (400, 400), 3, 3)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Mix)
# Spice simulation for series connected SDM cells
IV_Pred_Mix =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

# Construct Image for cells into module
MMEL_Init = cells2Mod(Synth_MiniMod_Init, (400, 400), 3, 3)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Init)
# Spice simulation for series connected SDM cells
IV_Pred_Init =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

# Construct Image for cells into module
MMEL_Deg1 = cells2Mod(Synth_MiniMod_Deg1, (400, 400), 3, 3)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Deg1)
# Spice simulation for series connected SDM cells
IV_Pred_Deg1 =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

# Construct Image for cells into module
MMEL_Deg2 = cells2Mod(Synth_MiniMod_Deg2, (400, 400), 3, 3)
# Order SDM params in the passed DataFrame into a list of dictionary for MiniMod_Spice
Params = Extract_Params(Synth_MiniMod_Deg2)
# Spice simulation for series connected SDM cells
IV_Pred_Deg2 =  MiniMod_Spice(Params, file_path, V, V_Step=0.001, file_name ='', spicepath=spicepath)

def compute_Pmpp(iv_pred):
    Varr, Iarr = iv_pred
    Parr = Varr * Iarr
    return Parr.max()

# compute all four Pmpp values
Pmp_mix  = compute_Pmpp(IV_Pred_Mix)
Pmp_init = compute_Pmpp(IV_Pred_Init)
Pmp_deg1 = compute_Pmpp(IV_Pred_Deg1)
Pmp_deg2 = compute_Pmpp(IV_Pred_Deg2)

# 1×2 figures for each dataset
for MMEL, iv_pred, title, Pmp in [
    (MMEL_Mix,   IV_Pred_Mix,  "Mix Synthetic Data",  Pmp_mix),
    (MMEL_Init,  IV_Pred_Init, "Init Synthetic Data", Pmp_init),
    (MMEL_Deg1,  IV_Pred_Deg1, "Deg1 Synthetic Data", Pmp_deg1),
    (MMEL_Deg2,  IV_Pred_Deg2, "Deg2 Synthetic Data", Pmp_deg2),
]:
    fig, axs = plt.subplots(1, 2, figsize=(10,5))
    fig.suptitle(title)
    axs[0].imshow(MMEL)
    axs[0].axis('off')
    axs[0].set_title('EL image')

    axs[1].plot(iv_pred[0], iv_pred[1],
                label=f"{title.split()[0]} IV (Pmp={Pmp:.3f} W)")
    axs[1].set_xlabel('Voltage')
    axs[1].set_ylabel('Current')
    axs[1].set_title('IV Curves')
    axs[1].legend()
    plt.tight_layout(rect=[0,0,1,0.95])
    plt.show()

# overlay with ΔPmpp relative to Init
plt.figure(figsize=(8,6))
plt.title("Overlay of synthetic cell IVs")

# plot Init first, with its absolute Pmpp
plt.plot(IV_Pred_Init[0], IV_Pred_Init[1],
         label=f"Init (Pmp={Pmp_init:.3f} W)")

# plot the others, with ΔPmpp = Pmpp_x – Pmpp_init
for label, iv_pred, Pmp in [
    ('Mix',  IV_Pred_Mix,  Pmp_mix),
    ('Deg1', IV_Pred_Deg1, Pmp_deg1),
    ('Deg2', IV_Pred_Deg2, Pmp_deg2),
]:
    delta = (Pmp - Pmp_init)/Pmp_init*100
    plt.plot(iv_pred[0], iv_pred[1],
             label=f"{label} (ΔPmp={delta:+.2f}%)")

plt.xlabel('Voltage')
plt.ylabel('Current')
plt.legend()
plt.tight_layout()
plt.show()
../_images/Examples_example_minimodule_build_26_0.png
../_images/Examples_example_minimodule_build_26_1.png
../_images/Examples_example_minimodule_build_26_2.png
../_images/Examples_example_minimodule_build_26_3.png
../_images/Examples_example_minimodule_build_26_4.png