From bbe626a2f9a919241557fe7c6e56c089df2d528c Mon Sep 17 00:00:00 2001 From: Connor Date: Wed, 24 May 2023 15:06:30 -0700 Subject: [PATCH 01/47] pan ond reader --- pvlib/iotools/panond.py | 80 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 pvlib/iotools/panond.py diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py new file mode 100644 index 0000000000..d7a8c66409 --- /dev/null +++ b/pvlib/iotools/panond.py @@ -0,0 +1,80 @@ +import io + +def num_type(value): + # Determine if a value is float, int or leave as string + if '.' in value: + try: # Detect float + value_out = float(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + else: + + try: # Detect int + value_out = int(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + +def element_type(element): + # Determine if an element is a list then pass to num_type() + if ',' in element: # Detect a list + values = element.split(',') + element_out = [] + for val in values: # Determine datatype of each value + element_out.append(num_type(val)) + + return element_out + + else: + return num_type(element) + + +def parse_panond(fbuf): + # Parse a .pan or .ond file provided a file-like object + comp = {} # Component + dict_levels = [comp] + + fbuf.seek(0) + lines = fbuf.getvalue().splitlines() + for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. + if lines[i] == '': # Skipping blank lines + continue + + indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 + indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + line_data = lines[i].split('=') + key = line_data[0].strip() + value = element_type(line_data[1].strip()) if len(line_data) > 1 else None + if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. + current_level = dict_levels[indent_lvl_1] + new_level = {} + current_level[key] = new_level + dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] + #TODO: add value of indentation as a key in that new level + + elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict + current_level = dict_levels[indent_lvl_1] + current_level[key] = value + + return comp + + +def read_panond(file): + # Read a .pan file or string into a nested dictionary. + + with open(file, "r", encoding='utf-8-sig') as file: + f_content = file.read() + fbuf = io.StringIO(f_content) + + content = parse_panond(fbuf) + + return content + + From 9489d2bfac3e50a4bca8ae6fbb2d2d2f03131d6d Mon Sep 17 00:00:00 2001 From: Connor Date: Wed, 24 May 2023 15:26:17 -0700 Subject: [PATCH 02/47] pan ond reader --- pvlib/iotools/__init__.py | 2 + pvlib/tests/iotools/test_panond.py | 240 +++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 pvlib/tests/iotools/test_panond.py diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index b02ce243ae..2022689309 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -21,3 +21,5 @@ from pvlib.iotools.sodapro import get_cams # noqa: F401 from pvlib.iotools.sodapro import read_cams # noqa: F401 from pvlib.iotools.sodapro import parse_cams # noqa: F401 +from pvlib.iotools.panond import read_panond +from pvlib.iotools.panond import parse_panond \ No newline at end of file diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py new file mode 100644 index 0000000000..8c8d785423 --- /dev/null +++ b/pvlib/tests/iotools/test_panond.py @@ -0,0 +1,240 @@ +from ... import iotools +import io +# Not sure if I am creating these test scenarios correctly + +fn_file = 'C:/Users/Contractor1/Downloads/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' +ond_file = iotools.read_panond(fn_file) + +fn_str = """PVObject_=pvGInverter + Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 + Version=6.81 + ParObj1=2020 + Flags=$00381562 + + PVObject_Commercial=pvCommercial + Comment=www.chintpower.com (China) + Flags=$0041 + Manufacturer=ChintPower + Model=CPS SCH275KTL-DO/US-800 + DataSource=Manufacturer 2020 + YearBeg=2020 + Width=0.680 + Height=0.337 + Depth=1.100 + Weight=95.000 + NPieces=0 + PriceDate=02/06/20 00:02 + Currency=EUR + Remarks, Count=2 + Str_1=Protection: -30 - +60, IP 66: outdoor installable + Str_2 + End of Remarks + End of PVObject pvCommercial + Transfo=Without + + Converter=TConverter + PNomConv=250.000 + PMaxOUT=250.000 + VOutConv=800.0 + VMppMin=500 + VMPPMax=1500 + VAbsMax=1500 + PSeuil=500.0 + EfficMax=99.01 + EfficEuro=98.49 + FResNorm=0.00 + ModeOper=MPPT + CompPMax=Lim + CompVMax=Lim + MonoTri=Tri + ModeAffEnum=Efficf_POut + UnitAffEnum=kW + PNomDC=253.000 + PMaxDC=375.000 + IDCMax=0.0 + IMaxDC=360.0 + INomAC=181.0 + IMaxAC=199.0 + TPNom=45.0 + TPMax=40.0 + TPLim1=50.0 + TPLimAbs=60.0 + PLim1=225.000 + PLimAbs=90.000 + PInEffMax =150000.000 + PThreshEff=3332.4 + HasdefaultPThresh=False + + ProfilPIO=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8085 + Mode=1 + Point_1=1250,0 + Point_2=7500,6923 + Point_3=12500,11875 + Point_4=25000,24250 + Point_5=50000,49100 + Point_6=75000,73875 + Point_7=150000,148515 + Point_8=250000,246500 + Point_9=275000,270325 + Point_10=0,0 + Point_11=0,0 + End of TCubicProfile + VNomEff=880.0,1174.0,1300.0, + EfficMaxV=98.260,99.040,98.860, + EfficEuroV=97.986,98.860,98.661, + + ProfilPIOV1=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=13012.7,12500.0 + Point_3=25720.2,25000.0 + Point_4=51093.4,50000.0 + Point_5=76437.0,75000.0 + Point_6=127213.5,125000.0 + Point_7=190995.2,187500.0 + Point_8=255440.9,250000.0 + Point_9=281301.1,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV2=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12850.8,12500.0 + Point_3=25401.3,25000.0 + Point_4=50581.7,50000.0 + Point_5=75795.9,75000.0 + Point_6=126211.6,125000.0 + Point_7=189623.8,187500.0 + Point_8=253138.9,250000.0 + Point_9=278763.3,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV3=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12953.4,12500.0 + Point_3=25512.8,25000.0 + Point_4=50679.1,50000.0 + Point_5=75895.6,75000.0 + Point_6=126441.4,125000.0 + Point_7=189835.0,187500.0 + Point_8=253472.6,250000.0 + Point_9=279017.9,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + End of TConverter + NbInputs=36 + NbMPPT=12 + TanPhiMin=-0.750 + TanPhiMax=0.750 + NbMSInterne=2 + MasterSlave=No_M_S + IsolSurvey =Yes + DC_Switch=Yes + MS_Thresh=0.8 + Night_Loss=5.00 +End of PVObject pvGcomperter +""" +f_obj = io.StringIO(fn_str) +ond_str = iotools.parse_panond(f_obj) + +fn_file = 'C:/Users/Contractor1/Downloads/ET-M772BH550GL.PAN' +mod_file = iotools.read_panond(fn_file) + +fn_str = """PVObject_=pvModule + Version=7.2 + Flags=$00900243 + + PVObject_Commercial=pvCommercial + Comment=ET SOLAR + Flags=$0041 + Manufacturer=ET SOLAR + Model=ET-M772BH550GL + DataSource=Manufacturer 2021 + YearBeg=2021 + Width=1.134 + Height=2.278 + Depth=0.035 + Weight=32.000 + NPieces=100 + PriceDate=06/04/22 12:39 + End of PVObject pvCommercial + + Technol=mtSiMono + NCelS=72 + NCelP=2 + NDiode=3 + SubModuleLayout=slTwinHalfCells + FrontSurface=fsARCoating + GRef=1000 + TRef=25.0 + PNom=550.0 + PNomTolUp=0.90 + BifacialityFactor=0.700 + Isc=14.000 + Voc=49.90 + Imp=13.110 + Vmp=41.96 + muISC=7.28 + muVocSpec=-128.0 + muPmpReq=-0.340 + RShunt=300 + Rp_0=2000 + Rp_Exp=5.50 + RSerie=0.203 + Gamma=0.980 + muGamma=-0.0001 + VMaxIEC=1500 + VMaxUL=1500 + Absorb=0.90 + ARev=3.200 + BRev=16.716 + RDiode=0.010 + VRevDiode=-0.70 + IMaxDiode=30.0 + AirMassRef=1.500 + CellArea=165.1 + SandiaAMCorr=50.000 + + PVObject_IAM=pvIAM + Flags=$00 + IAMMode=UserProfile + IAMProfile=TCubicProfile + NPtsMax=9 + NPtsEff=9 + LastCompile=$B18D + Mode=3 + Point_1=0.0,1.00000 + Point_2=20.0,1.00000 + Point_3=30.0,1.00000 + Point_4=40.0,0.99000 + Point_5=50.0,0.98000 + Point_6=60.0,0.96000 + Point_7=70.0,0.89000 + Point_8=80.0,0.66000 + Point_9=90.0,0.00000 + End of TCubicProfile + End of PVObject pvIAM +End of PVObject pvModule +""" +f_obj = io.StringIO(fn_str) +mod_str = iotools.parse_panond(f_obj) + +stop = 1 \ No newline at end of file From 907df2ce107c596b06045cd6a31842f738b2b8f1 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 08:21:56 -0700 Subject: [PATCH 03/47] More info in nested levels --- pvlib/iotools/__init__.py | 3 +-- pvlib/iotools/panond.py | 5 ++--- pvlib/tests/iotools/test_panond.py | 10 +++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index 2022689309..1121e7d682 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -21,5 +21,4 @@ from pvlib.iotools.sodapro import get_cams # noqa: F401 from pvlib.iotools.sodapro import read_cams # noqa: F401 from pvlib.iotools.sodapro import parse_cams # noqa: F401 -from pvlib.iotools.panond import read_panond -from pvlib.iotools.panond import parse_panond \ No newline at end of file +from pvlib.iotools.panond import read_panond, parse_panond \ No newline at end of file diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index d7a8c66409..6217ca80f2 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -57,7 +57,8 @@ def parse_panond(fbuf): new_level = {} current_level[key] = new_level dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] - #TODO: add value of indentation as a key in that new level + current_level = dict_levels[indent_lvl_1 + 1] + current_level[key] = value elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict current_level = dict_levels[indent_lvl_1] @@ -76,5 +77,3 @@ def read_panond(file): content = parse_panond(fbuf) return content - - diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 8c8d785423..8f7534e9c1 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -1,9 +1,9 @@ -from ... import iotools +from ...iotools import read_panond, parse_panond import io # Not sure if I am creating these test scenarios correctly fn_file = 'C:/Users/Contractor1/Downloads/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' -ond_file = iotools.read_panond(fn_file) +ond_file = read_panond(fn_file) fn_str = """PVObject_=pvGInverter Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 @@ -153,10 +153,10 @@ End of PVObject pvGcomperter """ f_obj = io.StringIO(fn_str) -ond_str = iotools.parse_panond(f_obj) +ond_str = parse_panond(f_obj) fn_file = 'C:/Users/Contractor1/Downloads/ET-M772BH550GL.PAN' -mod_file = iotools.read_panond(fn_file) +mod_file = read_panond(fn_file) fn_str = """PVObject_=pvModule Version=7.2 @@ -235,6 +235,6 @@ End of PVObject pvModule """ f_obj = io.StringIO(fn_str) -mod_str = iotools.parse_panond(f_obj) +mod_str = parse_panond(f_obj) stop = 1 \ No newline at end of file From 38c4b595ec62e0f3a63b6c5a99da1705794b7a7d Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 09:15:49 -0700 Subject: [PATCH 04/47] Added Comments --- pvlib/iotools/panond.py | 60 +++++++++++++++++++++++++++++- pvlib/tests/iotools/test_panond.py | 4 ++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 6217ca80f2..3d424de5e6 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -1,3 +1,7 @@ +""" +Get .PAN or .OND file data into a nested dictionary. +""" + import io def num_type(value): @@ -37,7 +41,33 @@ def element_type(element): def parse_panond(fbuf): - # Parse a .pan or .ond file provided a file-like object + """ + Parse a .pan or .ond text file into a nested dictionary. + + Parameters + ---------- + fbuf : File-like object + Buffer of a .pan or .ond file + + Returns + ------- + comp : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ comp = {} # Component dict_levels = [comp] @@ -68,7 +98,33 @@ def parse_panond(fbuf): def read_panond(file): - # Read a .pan file or string into a nested dictionary. + """ + Retrieve Module or Inverter data from a .pan or .ond text file, respectively. + + Parameters + ---------- + file : string or path object + Name or path of a .pan/.ond file + + Returns + ------- + content : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ with open(file, "r", encoding='utf-8-sig') as file: f_content = file.read() diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 8f7534e9c1..c4c0e503ee 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -1,3 +1,7 @@ +""" +test iotools for panond +""" + from ...iotools import read_panond, parse_panond import io # Not sure if I am creating these test scenarios correctly From 8dffc0179dc397fcb60ceab20a5f41a50947b75e Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 09:21:13 -0700 Subject: [PATCH 05/47] Added whatsnew documentation --- docs/sphinx/source/whatsnew/v0.10.0.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index a4406072f8..2279bca621 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -21,7 +21,7 @@ Deprecations Enhancements ~~~~~~~~~~~~ - +* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond' Bug fixes ~~~~~~~~~ @@ -45,3 +45,4 @@ Requirements Contributors ~~~~~~~~~~~~ * Taos Transue (:ghuser:`reepoi`) +* Connor Krening (:ghuser:'ckrening') From 01a9e182e61709eb55ef72e9053f21867dc87e33 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 09:25:00 -0700 Subject: [PATCH 06/47] Corrected formatting --- docs/sphinx/source/whatsnew/v0.10.0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index 2279bca621..334226d8f5 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -21,7 +21,9 @@ Deprecations Enhancements ~~~~~~~~~~~~ -* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond' +* Added .pan/.ond reader function +:py:func:`pvlib.iotools.panond` +(:issue:`1747` ) Bug fixes ~~~~~~~~~ From 258e3f5099e7acbec518374cd9eef1a1ddbb2469 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 12:09:43 -0700 Subject: [PATCH 07/47] Adressing stickler comments --- pvlib/iotools/panond.py | 135 --------------------------------------- pvlib/iotools/pvsyst.py | 137 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 135 deletions(-) delete mode 100644 pvlib/iotools/panond.py create mode 100644 pvlib/iotools/pvsyst.py diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py deleted file mode 100644 index 3d424de5e6..0000000000 --- a/pvlib/iotools/panond.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Get .PAN or .OND file data into a nested dictionary. -""" - -import io - -def num_type(value): - # Determine if a value is float, int or leave as string - if '.' in value: - try: # Detect float - value_out = float(value) - return value_out - - except ValueError: # Otherwise leave as string - value_out = value - return value_out - - else: - - try: # Detect int - value_out = int(value) - return value_out - - except ValueError: # Otherwise leave as string - value_out = value - return value_out - - -def element_type(element): - # Determine if an element is a list then pass to num_type() - if ',' in element: # Detect a list - values = element.split(',') - element_out = [] - for val in values: # Determine datatype of each value - element_out.append(num_type(val)) - - return element_out - - else: - return num_type(element) - - -def parse_panond(fbuf): - """ - Parse a .pan or .ond text file into a nested dictionary. - - Parameters - ---------- - fbuf : File-like object - Buffer of a .pan or .ond file - - Returns - ------- - comp : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. - - Raises - ------ - - Notes - ----- - - See Also - -------- - - References - ---------- - """ - comp = {} # Component - dict_levels = [comp] - - fbuf.seek(0) - lines = fbuf.getvalue().splitlines() - for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. - if lines[i] == '': # Skipping blank lines - continue - - indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 - indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 - line_data = lines[i].split('=') - key = line_data[0].strip() - value = element_type(line_data[1].strip()) if len(line_data) > 1 else None - if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. - current_level = dict_levels[indent_lvl_1] - new_level = {} - current_level[key] = new_level - dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] - current_level = dict_levels[indent_lvl_1 + 1] - current_level[key] = value - - elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict - current_level = dict_levels[indent_lvl_1] - current_level[key] = value - - return comp - - -def read_panond(file): - """ - Retrieve Module or Inverter data from a .pan or .ond text file, respectively. - - Parameters - ---------- - file : string or path object - Name or path of a .pan/.ond file - - Returns - ------- - content : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. - - Raises - ------ - - Notes - ----- - - See Also - -------- - - References - ---------- - """ - - with open(file, "r", encoding='utf-8-sig') as file: - f_content = file.read() - fbuf = io.StringIO(f_content) - - content = parse_panond(fbuf) - - return content diff --git a/pvlib/iotools/pvsyst.py b/pvlib/iotools/pvsyst.py new file mode 100644 index 0000000000..bcb1e95b06 --- /dev/null +++ b/pvlib/iotools/pvsyst.py @@ -0,0 +1,137 @@ +""" +Get .PAN or .OND file data into a nested dictionary. +""" + +import io + +def num_type(value): + # Determine if a value is float, int or leave as string + + if '.' in value: + try: # Detect float + value_out = float(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + else: + + try: # Detect int + value_out = int(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + +def element_type(element): + # Determine if an element is a list then pass to num_type() + + if ',' in element: # Detect a list + values = element.split(',') + element_out = [] + for val in values: # Determine datatype of each value + element_out.append(num_type(val)) + + return element_out + + else: + return num_type(element) + + +def parse_panond(fbuf): + """ + Parse a .pan or .ond text file into a nested dictionary. + + Parameters + ---------- + fbuf : File-like object + Buffer of a .pan or .ond file + + Returns + ------- + comp : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ + comp = {} # Component + dict_levels = [comp] + + fbuf.seek(0) + lines = fbuf.getvalue().splitlines() + for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. + if lines[i] == '': # Skipping blank lines + continue + + indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 + indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + line_data = lines[i].split('=') + key = line_data[0].strip() + value = element_type(line_data[1].strip()) if len(line_data) > 1 else None + if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. + current_level = dict_levels[indent_lvl_1] + new_level = {} + current_level[key] = new_level + dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] + current_level = dict_levels[indent_lvl_1 + 1] + current_level[key] = value + + elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict + current_level = dict_levels[indent_lvl_1] + current_level[key] = value + + return comp + + +def read_panond(file): + """ + Retrieve Module or Inverter data from a .pan or .ond text file, respectively. + + Parameters + ---------- + file : string or path object + Name or path of a .pan/.ond file + + Returns + ------- + content : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ + + with open(file, "r", encoding='utf-8-sig') as file: + f_content = file.read() + fbuf = io.StringIO(f_content) + + content = parse_panond(fbuf) + + return content From 99184512705637942d88e4dfd64ca20655814363 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 13:03:06 -0700 Subject: [PATCH 08/47] Updated testing script --- ...CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND | 146 ++++++++++++++++++ pvlib/data/ET-M772BH550GL.PAN | 75 +++++++++ pvlib/iotools/__init__.py | 2 +- .../{test_panond.py => test_pvsyst.py} | 9 +- 4 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND create mode 100644 pvlib/data/ET-M772BH550GL.PAN rename pvlib/tests/iotools/{test_panond.py => test_pvsyst.py} (96%) diff --git a/pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND b/pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND new file mode 100644 index 0000000000..c13700ce76 --- /dev/null +++ b/pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND @@ -0,0 +1,146 @@ +PVObject_=pvGInverter + Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 + Version=6.81 + ParObj1=2020 + Flags=$00381562 + + PVObject_Commercial=pvCommercial + Comment=www.chintpower.com (China) + Flags=$0041 + Manufacturer=ChintPower + Model=CPS SCH275KTL-DO/US-800 + DataSource=Manufacturer 2020 + YearBeg=2020 + Width=0.680 + Height=0.337 + Depth=1.100 + Weight=95.000 + NPieces=0 + PriceDate=02/06/20 00:02 + Currency=EUR + Remarks, Count=2 + Str_1=Protection: -30 - +60, IP 66: outdoor installable + Str_2 + End of Remarks + End of PVObject pvCommercial + Transfo=Without + + Converter=TConverter + PNomConv=250.000 + PMaxOUT=250.000 + VOutConv=800.0 + VMppMin=500 + VMPPMax=1500 + VAbsMax=1500 + PSeuil=500.0 + EfficMax=99.01 + EfficEuro=98.49 + FResNorm=0.00 + ModeOper=MPPT + CompPMax=Lim + CompVMax=Lim + MonoTri=Tri + ModeAffEnum=Efficf_POut + UnitAffEnum=kW + PNomDC=253.000 + PMaxDC=375.000 + IDCMax=0.0 + IMaxDC=360.0 + INomAC=181.0 + IMaxAC=199.0 + TPNom=45.0 + TPMax=40.0 + TPLim1=50.0 + TPLimAbs=60.0 + PLim1=225.000 + PLimAbs=90.000 + PInEffMax =150000.000 + PThreshEff=3332.4 + HasdefaultPThresh=False + + ProfilPIO=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8085 + Mode=1 + Point_1=1250,0 + Point_2=7500,6923 + Point_3=12500,11875 + Point_4=25000,24250 + Point_5=50000,49100 + Point_6=75000,73875 + Point_7=150000,148515 + Point_8=250000,246500 + Point_9=275000,270325 + Point_10=0,0 + Point_11=0,0 + End of TCubicProfile + VNomEff=880.0,1174.0,1300.0, + EfficMaxV=98.260,99.040,98.860, + EfficEuroV=97.986,98.860,98.661, + + ProfilPIOV1=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=13012.7,12500.0 + Point_3=25720.2,25000.0 + Point_4=51093.4,50000.0 + Point_5=76437.0,75000.0 + Point_6=127213.5,125000.0 + Point_7=190995.2,187500.0 + Point_8=255440.9,250000.0 + Point_9=281301.1,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV2=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12850.8,12500.0 + Point_3=25401.3,25000.0 + Point_4=50581.7,50000.0 + Point_5=75795.9,75000.0 + Point_6=126211.6,125000.0 + Point_7=189623.8,187500.0 + Point_8=253138.9,250000.0 + Point_9=278763.3,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV3=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12953.4,12500.0 + Point_3=25512.8,25000.0 + Point_4=50679.1,50000.0 + Point_5=75895.6,75000.0 + Point_6=126441.4,125000.0 + Point_7=189835.0,187500.0 + Point_8=253472.6,250000.0 + Point_9=279017.9,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + End of TConverter + NbInputs=36 + NbMPPT=12 + TanPhiMin=-0.750 + TanPhiMax=0.750 + NbMSInterne=2 + MasterSlave=No_M_S + IsolSurvey =Yes + DC_Switch=Yes + MS_Thresh=0.8 + Night_Loss=5.00 +End of PVObject pvGInverter diff --git a/pvlib/data/ET-M772BH550GL.PAN b/pvlib/data/ET-M772BH550GL.PAN new file mode 100644 index 0000000000..9b2a6a29af --- /dev/null +++ b/pvlib/data/ET-M772BH550GL.PAN @@ -0,0 +1,75 @@ +PVObject_=pvModule + Version=7.2 + Flags=$00900243 + + PVObject_Commercial=pvCommercial + Comment=ET SOLAR + Flags=$0041 + Manufacturer=ET SOLAR + Model=ET-M772BH550GL + DataSource=Manufacturer 2021 + YearBeg=2021 + Width=1.134 + Height=2.278 + Depth=0.035 + Weight=32.000 + NPieces=100 + PriceDate=06/04/22 12:39 + End of PVObject pvCommercial + + Technol=mtSiMono + NCelS=72 + NCelP=2 + NDiode=3 + SubModuleLayout=slTwinHalfCells + FrontSurface=fsARCoating + GRef=1000 + TRef=25.0 + PNom=550.0 + PNomTolUp=0.90 + BifacialityFactor=0.700 + Isc=14.000 + Voc=49.90 + Imp=13.110 + Vmp=41.96 + muISC=7.28 + muVocSpec=-128.0 + muPmpReq=-0.340 + RShunt=300 + Rp_0=2000 + Rp_Exp=5.50 + RSerie=0.203 + Gamma=0.980 + muGamma=-0.0001 + VMaxIEC=1500 + VMaxUL=1500 + Absorb=0.90 + ARev=3.200 + BRev=16.716 + RDiode=0.010 + VRevDiode=-0.70 + IMaxDiode=30.0 + AirMassRef=1.500 + CellArea=165.1 + SandiaAMCorr=50.000 + + PVObject_IAM=pvIAM + Flags=$00 + IAMMode=UserProfile + IAMProfile=TCubicProfile + NPtsMax=9 + NPtsEff=9 + LastCompile=$B18D + Mode=3 + Point_1=0.0,1.00000 + Point_2=20.0,1.00000 + Point_3=30.0,1.00000 + Point_4=40.0,0.99000 + Point_5=50.0,0.98000 + Point_6=60.0,0.96000 + Point_7=70.0,0.89000 + Point_8=80.0,0.66000 + Point_9=90.0,0.00000 + End of TCubicProfile + End of PVObject pvIAM +End of PVObject pvModule diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index 1121e7d682..4f7b0d9920 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -21,4 +21,4 @@ from pvlib.iotools.sodapro import get_cams # noqa: F401 from pvlib.iotools.sodapro import read_cams # noqa: F401 from pvlib.iotools.sodapro import parse_cams # noqa: F401 -from pvlib.iotools.panond import read_panond, parse_panond \ No newline at end of file +from pvlib.iotools.pvsyst import read_panond, parse_panond \ No newline at end of file diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_pvsyst.py similarity index 96% rename from pvlib/tests/iotools/test_panond.py rename to pvlib/tests/iotools/test_pvsyst.py index c4c0e503ee..626d23e506 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_pvsyst.py @@ -2,11 +2,12 @@ test iotools for panond """ -from ...iotools import read_panond, parse_panond +from pvlib.iotools import read_panond, parse_panond +from pvlib.tests.conftest import DATA_DIR import io # Not sure if I am creating these test scenarios correctly -fn_file = 'C:/Users/Contractor1/Downloads/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' +fn_file = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' ond_file = read_panond(fn_file) fn_str = """PVObject_=pvGInverter @@ -159,7 +160,7 @@ f_obj = io.StringIO(fn_str) ond_str = parse_panond(f_obj) -fn_file = 'C:/Users/Contractor1/Downloads/ET-M772BH550GL.PAN' +fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' mod_file = read_panond(fn_file) fn_str = """PVObject_=pvModule @@ -240,5 +241,3 @@ """ f_obj = io.StringIO(fn_str) mod_str = parse_panond(f_obj) - -stop = 1 \ No newline at end of file From eec40012afb38072524f38a503b6d44031b74b21 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 13:42:27 -0700 Subject: [PATCH 09/47] addressing stickler comments --- pvlib/iotools/__init__.py | 2 +- pvlib/iotools/{pvsyst.py => panond.py} | 60 +++++++++++-------- .../{test_pvsyst.py => test_panond.py} | 0 3 files changed, 36 insertions(+), 26 deletions(-) rename pvlib/iotools/{pvsyst.py => panond.py} (75%) rename pvlib/tests/iotools/{test_pvsyst.py => test_panond.py} (100%) diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index ecc70cab2b..adb4733a68 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -22,4 +22,4 @@ from pvlib.iotools.sodapro import get_cams # noqa: F401 from pvlib.iotools.sodapro import read_cams # noqa: F401 from pvlib.iotools.sodapro import parse_cams # noqa: F401 -from pvlib.iotools.pvsyst import read_panond, parse_panond \ No newline at end of file +from pvlib.iotools.panond import read_panond, parse_panond diff --git a/pvlib/iotools/pvsyst.py b/pvlib/iotools/panond.py similarity index 75% rename from pvlib/iotools/pvsyst.py rename to pvlib/iotools/panond.py index bcb1e95b06..87ad2764c8 100644 --- a/pvlib/iotools/pvsyst.py +++ b/pvlib/iotools/panond.py @@ -5,27 +5,28 @@ import io def num_type(value): - # Determine if a value is float, int or leave as string + + # Determine if a value is float, int or leave as string if '.' in value: try: # Detect float value_out = float(value) return value_out - + except ValueError: # Otherwise leave as string value_out = value return value_out - + else: try: # Detect int value_out = int(value) return value_out - + except ValueError: # Otherwise leave as string value_out = value return value_out - + def element_type(element): # Determine if an element is a list then pass to num_type() @@ -35,7 +36,7 @@ def element_type(element): element_out = [] for val in values: # Determine datatype of each value element_out.append(num_type(val)) - + return element_out else: @@ -50,13 +51,13 @@ def parse_panond(fbuf): ---------- fbuf : File-like object Buffer of a .pan or .ond file - + Returns ------- comp : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. + Contents of the .pan or .ond file following the indentation of the + file. The value of datatypes are assumed during reading. The value + units are the default used by PVsyst. Raises ------ @@ -72,19 +73,27 @@ def parse_panond(fbuf): """ comp = {} # Component dict_levels = [comp] - + fbuf.seek(0) lines = fbuf.getvalue().splitlines() - for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. - if lines[i] == '': # Skipping blank lines - continue - indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 - indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 - line_data = lines[i].split('=') + for i in range(0, len(lines) - 1): + if lines[i] == '': # Skipping blank lines + continue + # Reading blank lines. Stopping one short to avoid index error. + # Last line never contains important data. + indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 + indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + line_data = lines[i].split('=') key = line_data[0].strip() - value = element_type(line_data[1].strip()) if len(line_data) > 1 else None - if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. + # value = element_type(line_data[1].strip()) if len(line_data) > 1 else None + if len(line_data) > 1: + value = element_type(line_data[1].strip()) + else: + value = element_type = None + # add a level to the dict. The key here will be ignored. + # Not vital to file function. + if indent_lvl_2 > indent_lvl_1: current_level = dict_levels[indent_lvl_1] new_level = {} current_level[key] = new_level @@ -101,19 +110,20 @@ def parse_panond(fbuf): def read_panond(file): """ - Retrieve Module or Inverter data from a .pan or .ond text file, respectively. + Retrieve Module or Inverter data from a .pan or .ond text file, + respectively. Parameters ---------- file : string or path object Name or path of a .pan/.ond file - + Returns ------- content : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. + Contents of the .pan or .ond file following the indentation of the + file. The value of datatypes are assumed during reading. The value + units are the default used by PVsyst. Raises ------ @@ -131,7 +141,7 @@ def read_panond(file): with open(file, "r", encoding='utf-8-sig') as file: f_content = file.read() fbuf = io.StringIO(f_content) - + content = parse_panond(fbuf) return content diff --git a/pvlib/tests/iotools/test_pvsyst.py b/pvlib/tests/iotools/test_panond.py similarity index 100% rename from pvlib/tests/iotools/test_pvsyst.py rename to pvlib/tests/iotools/test_panond.py From 4e434b203a844e90baf5880cd834ee235e8735ac Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 14:29:15 -0700 Subject: [PATCH 10/47] stickler comments v3 --- pvlib/iotools/__init__.py | 2 +- pvlib/iotools/panond.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index adb4733a68..1951bc700e 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -22,4 +22,4 @@ from pvlib.iotools.sodapro import get_cams # noqa: F401 from pvlib.iotools.sodapro import read_cams # noqa: F401 from pvlib.iotools.sodapro import parse_cams # noqa: F401 -from pvlib.iotools.panond import read_panond, parse_panond +from pvlib.iotools.panond import read_panond, parse_panond # noqa: F401 diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 87ad2764c8..b3e5ec9136 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -4,10 +4,11 @@ import io -def num_type(value): - - # Determine if a value is float, int or leave as string +def num_type(value): + """ + Determine if a value is float, int or a string + """ if '.' in value: try: # Detect float value_out = float(value) @@ -26,11 +27,12 @@ def num_type(value): except ValueError: # Otherwise leave as string value_out = value return value_out - + def element_type(element): - # Determine if an element is a list then pass to num_type() - + """ + Determine if an element is a list then pass to num_type() + """ if ',' in element: # Detect a list values = element.split(',') element_out = [] @@ -86,11 +88,10 @@ def parse_panond(fbuf): indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 line_data = lines[i].split('=') key = line_data[0].strip() - # value = element_type(line_data[1].strip()) if len(line_data) > 1 else None if len(line_data) > 1: value = element_type(line_data[1].strip()) else: - value = element_type = None + value = None # add a level to the dict. The key here will be ignored. # Not vital to file function. if indent_lvl_2 > indent_lvl_1: From 7475e3c87b8c71c581ea13f023abedccd3f4922d Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 14:45:44 -0700 Subject: [PATCH 11/47] stickler comment correction --- pvlib/iotools/panond.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index b3e5ec9136..0de46c001b 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -27,7 +27,7 @@ def num_type(value): except ValueError: # Otherwise leave as string value_out = value return value_out - + def element_type(element): """ From 238db5cb10e033f58437326f5a8ca788b8a3a1bc Mon Sep 17 00:00:00 2001 From: Connor Date: Tue, 6 Jun 2023 09:08:43 -0700 Subject: [PATCH 12/47] Updated notes and comments --- pvlib/iotools/panond.py | 53 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 0de46c001b..5d1313682d 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -33,7 +33,9 @@ def element_type(element): """ Determine if an element is a list then pass to num_type() """ - if ',' in element: # Detect a list + if ',' in element: # Detect a list. + # .pan/.ond don't use ',' to indicate 1000. If that changes, + # a new method of list detection needs to be found. values = element.split(',') element_out = [] for val in values: # Determine datatype of each value @@ -66,6 +68,46 @@ def parse_panond(fbuf): Notes ----- + The parser was intended for use with .pan and .ond files that were created + for use by PVsyst. At time of publication, no documentation for these + files was available. So, this parser is based on inferred logic, rather + than anything specified by PVsyst. + + The parser assumes that the file being parsed uses indendation of two + spaces (' ') to create new level in a nested dicitonary, and that + key/values pairs of interest are separated using '='. This further means + that lines not containing '=' were ommitted from the final returned + dictionary. + + Additionally, the indented lines often contain values themselves. This + leads to a conflict with the .pan/.ond file and the ability of nested + dicitonaries to capture that information. The solution implemented here is + to repeat that key to the new nested dictioary within that new level. + Example below. + + Sample file: + + 'level1 = first level + key1 = value1 + level2 = second level + key2 = value2' + + output: + + { + level1: first level + key1: value1, + level2:{ + level2: second level, + key2: value2 + } + } + + The parser takes an additional step to infer the datatype present in + each value. The .pan/.ond files appear to have intentially left datatype + indicators (e.g. floats have '.' decimals). However, there is still the + possibility that the datatype applied from this parser is incorrect. In + that event the user would need to convert to the desired datatype. See Also -------- @@ -84,15 +126,19 @@ def parse_panond(fbuf): continue # Reading blank lines. Stopping one short to avoid index error. # Last line never contains important data. + # Creating variables to assist new level in dictionary creation logic indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + # Split the line into key/value pair line_data = lines[i].split('=') key = line_data[0].strip() + # Logical to make sure there is a value to extract if len(line_data) > 1: value = element_type(line_data[1].strip()) else: value = None - # add a level to the dict. The key here will be ignored. + # add a level to the dict. If a key/value pair triggers the new level, + # the key/value will be repeated in the new dict level. # Not vital to file function. if indent_lvl_2 > indent_lvl_1: current_level = dict_levels[indent_lvl_1] @@ -131,6 +177,9 @@ def read_panond(file): Notes ----- + The read function simply converts a file path to a file-like object and + passes it to the parser. At time of creation, tested .pan/.ond files used + UTF-8 encoding. See Also -------- From 360a7e7ea5fb76e984dda2b975ef5bb13153805f Mon Sep 17 00:00:00 2001 From: Connor Date: Tue, 6 Jun 2023 11:18:30 -0700 Subject: [PATCH 13/47] Minor comment updates and sphinx doc update --- docs/sphinx/source/reference/iotools.rst | 2 ++ pvlib/iotools/panond.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index 14271cf3ee..fb2e67ca56 100644 --- a/docs/sphinx/source/reference/iotools.rst +++ b/docs/sphinx/source/reference/iotools.rst @@ -38,6 +38,8 @@ of sources and file formats relevant to solar energy modeling. iotools.get_cams iotools.read_cams iotools.parse_cams + iotools.read_panond + iotools.parse_panond A :py:class:`~pvlib.location.Location` object may be created from metadata in some files. diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 5d1313682d..03fa6fa327 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -71,8 +71,8 @@ def parse_panond(fbuf): The parser was intended for use with .pan and .ond files that were created for use by PVsyst. At time of publication, no documentation for these files was available. So, this parser is based on inferred logic, rather - than anything specified by PVsyst. - + than anything specified by PVsyst. + The parser assumes that the file being parsed uses indendation of two spaces (' ') to create new level in a nested dicitonary, and that key/values pairs of interest are separated using '='. This further means @@ -91,7 +91,7 @@ def parse_panond(fbuf): key1 = value1 level2 = second level key2 = value2' - + output: { @@ -99,7 +99,7 @@ def parse_panond(fbuf): key1: value1, level2:{ level2: second level, - key2: value2 + key2: value2 } } From a8d56a4b6852a3267ee701c7c416ae7764ce6df2 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 22 Jun 2023 07:39:58 -0700 Subject: [PATCH 14/47] edited comments for grammar/spelling --- pvlib/iotools/panond.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 03fa6fa327..076ba2511a 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -68,21 +68,21 @@ def parse_panond(fbuf): Notes ----- - The parser was intended for use with .pan and .ond files that were created + The parser is intended for use with .pan and .ond files that were created for use by PVsyst. At time of publication, no documentation for these files was available. So, this parser is based on inferred logic, rather than anything specified by PVsyst. The parser assumes that the file being parsed uses indendation of two - spaces (' ') to create new level in a nested dicitonary, and that + spaces (' ') to create new level in a nested dictionary, and that key/values pairs of interest are separated using '='. This further means - that lines not containing '=' were ommitted from the final returned + that lines not containing '=' were omitted from the final returned dictionary. Additionally, the indented lines often contain values themselves. This - leads to a conflict with the .pan/.ond file and the ability of nested - dicitonaries to capture that information. The solution implemented here is - to repeat that key to the new nested dictioary within that new level. + leads to a conflict with the .pan/.ond file and the ability of nested a + dictionary to capture that information. The solution implemented here is + to repeat that key to the new nested dictionary within that new level. Example below. Sample file: @@ -177,9 +177,9 @@ def read_panond(file): Notes ----- - The read function simply converts a file path to a file-like object and - passes it to the parser. At time of creation, tested .pan/.ond files used - UTF-8 encoding. + The read_panond function simply converts a file path to a file-like object, + passes it to parse-panond, and returns the file content. At time of + creation, tested .pan/.ond files used UTF-8 encoding. See Also -------- From eb7f3eec0e99aad36635dbdbdab3cd09a8cf1eec Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 29 Jun 2023 11:25:37 -0700 Subject: [PATCH 15/47] improved testing panond reader --- pvlib/tests/iotools/test_panond.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 626d23e506..48c8d01bba 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -5,12 +5,13 @@ from pvlib.iotools import read_panond, parse_panond from pvlib.tests.conftest import DATA_DIR import io -# Not sure if I am creating these test scenarios correctly -fn_file = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' -ond_file = read_panond(fn_file) -fn_str = """PVObject_=pvGInverter +def test_read_ond_file(): + fn_file = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' + ond_file = read_panond(fn_file) + # expected + fn_str = fn_str = """PVObject_=pvGInverter Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 Version=6.81 ParObj1=2020 @@ -157,13 +158,16 @@ Night_Loss=5.00 End of PVObject pvGcomperter """ -f_obj = io.StringIO(fn_str) -ond_str = parse_panond(f_obj) + f_obj = io.StringIO(fn_str) + ond_str = parse_panond(f_obj) + assert ond_file.keys() == ond_str.keys() -fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' -mod_file = read_panond(fn_file) -fn_str = """PVObject_=pvModule +def test_read_pan_file(): + fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' + ond_file = read_panond(fn_file) + # expected + fn_str = """PVObject_=pvModule Version=7.2 Flags=$00900243 @@ -239,5 +243,6 @@ End of PVObject pvIAM End of PVObject pvModule """ -f_obj = io.StringIO(fn_str) -mod_str = parse_panond(f_obj) + f_obj = io.StringIO(fn_str) + ond_str = parse_panond(f_obj) + assert ond_file.keys() == ond_str.keys() From fa57d5367519301628f48b45c56baca231881247 Mon Sep 17 00:00:00 2001 From: Connor Date: Wed, 24 May 2023 15:06:30 -0700 Subject: [PATCH 16/47] pan ond reader --- pvlib/iotools/panond.py | 80 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 pvlib/iotools/panond.py diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py new file mode 100644 index 0000000000..d7a8c66409 --- /dev/null +++ b/pvlib/iotools/panond.py @@ -0,0 +1,80 @@ +import io + +def num_type(value): + # Determine if a value is float, int or leave as string + if '.' in value: + try: # Detect float + value_out = float(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + else: + + try: # Detect int + value_out = int(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + +def element_type(element): + # Determine if an element is a list then pass to num_type() + if ',' in element: # Detect a list + values = element.split(',') + element_out = [] + for val in values: # Determine datatype of each value + element_out.append(num_type(val)) + + return element_out + + else: + return num_type(element) + + +def parse_panond(fbuf): + # Parse a .pan or .ond file provided a file-like object + comp = {} # Component + dict_levels = [comp] + + fbuf.seek(0) + lines = fbuf.getvalue().splitlines() + for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. + if lines[i] == '': # Skipping blank lines + continue + + indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 + indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + line_data = lines[i].split('=') + key = line_data[0].strip() + value = element_type(line_data[1].strip()) if len(line_data) > 1 else None + if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. + current_level = dict_levels[indent_lvl_1] + new_level = {} + current_level[key] = new_level + dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] + #TODO: add value of indentation as a key in that new level + + elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict + current_level = dict_levels[indent_lvl_1] + current_level[key] = value + + return comp + + +def read_panond(file): + # Read a .pan file or string into a nested dictionary. + + with open(file, "r", encoding='utf-8-sig') as file: + f_content = file.read() + fbuf = io.StringIO(f_content) + + content = parse_panond(fbuf) + + return content + + From a8056e3d4b8c376794bd7b9c8acfdda148d77d31 Mon Sep 17 00:00:00 2001 From: Connor Date: Wed, 24 May 2023 15:26:17 -0700 Subject: [PATCH 17/47] pan ond reader --- pvlib/iotools/__init__.py | 2 + pvlib/tests/iotools/test_panond.py | 240 +++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 pvlib/tests/iotools/test_panond.py diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index a179bfbf9a..a3be3f15e2 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -26,3 +26,5 @@ from pvlib.iotools.acis import get_acis_mpe # noqa: F401 from pvlib.iotools.acis import get_acis_station_data # noqa: F401 from pvlib.iotools.acis import get_acis_available_stations # noqa: F401 +from pvlib.iotools.panond import read_panond # noqa: F401 +from pvlib.iotools.panond import parse_panond # noqa: F401 diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py new file mode 100644 index 0000000000..8c8d785423 --- /dev/null +++ b/pvlib/tests/iotools/test_panond.py @@ -0,0 +1,240 @@ +from ... import iotools +import io +# Not sure if I am creating these test scenarios correctly + +fn_file = 'C:/Users/Contractor1/Downloads/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' +ond_file = iotools.read_panond(fn_file) + +fn_str = """PVObject_=pvGInverter + Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 + Version=6.81 + ParObj1=2020 + Flags=$00381562 + + PVObject_Commercial=pvCommercial + Comment=www.chintpower.com (China) + Flags=$0041 + Manufacturer=ChintPower + Model=CPS SCH275KTL-DO/US-800 + DataSource=Manufacturer 2020 + YearBeg=2020 + Width=0.680 + Height=0.337 + Depth=1.100 + Weight=95.000 + NPieces=0 + PriceDate=02/06/20 00:02 + Currency=EUR + Remarks, Count=2 + Str_1=Protection: -30 - +60, IP 66: outdoor installable + Str_2 + End of Remarks + End of PVObject pvCommercial + Transfo=Without + + Converter=TConverter + PNomConv=250.000 + PMaxOUT=250.000 + VOutConv=800.0 + VMppMin=500 + VMPPMax=1500 + VAbsMax=1500 + PSeuil=500.0 + EfficMax=99.01 + EfficEuro=98.49 + FResNorm=0.00 + ModeOper=MPPT + CompPMax=Lim + CompVMax=Lim + MonoTri=Tri + ModeAffEnum=Efficf_POut + UnitAffEnum=kW + PNomDC=253.000 + PMaxDC=375.000 + IDCMax=0.0 + IMaxDC=360.0 + INomAC=181.0 + IMaxAC=199.0 + TPNom=45.0 + TPMax=40.0 + TPLim1=50.0 + TPLimAbs=60.0 + PLim1=225.000 + PLimAbs=90.000 + PInEffMax =150000.000 + PThreshEff=3332.4 + HasdefaultPThresh=False + + ProfilPIO=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8085 + Mode=1 + Point_1=1250,0 + Point_2=7500,6923 + Point_3=12500,11875 + Point_4=25000,24250 + Point_5=50000,49100 + Point_6=75000,73875 + Point_7=150000,148515 + Point_8=250000,246500 + Point_9=275000,270325 + Point_10=0,0 + Point_11=0,0 + End of TCubicProfile + VNomEff=880.0,1174.0,1300.0, + EfficMaxV=98.260,99.040,98.860, + EfficEuroV=97.986,98.860,98.661, + + ProfilPIOV1=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=13012.7,12500.0 + Point_3=25720.2,25000.0 + Point_4=51093.4,50000.0 + Point_5=76437.0,75000.0 + Point_6=127213.5,125000.0 + Point_7=190995.2,187500.0 + Point_8=255440.9,250000.0 + Point_9=281301.1,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV2=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12850.8,12500.0 + Point_3=25401.3,25000.0 + Point_4=50581.7,50000.0 + Point_5=75795.9,75000.0 + Point_6=126211.6,125000.0 + Point_7=189623.8,187500.0 + Point_8=253138.9,250000.0 + Point_9=278763.3,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV3=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12953.4,12500.0 + Point_3=25512.8,25000.0 + Point_4=50679.1,50000.0 + Point_5=75895.6,75000.0 + Point_6=126441.4,125000.0 + Point_7=189835.0,187500.0 + Point_8=253472.6,250000.0 + Point_9=279017.9,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + End of TConverter + NbInputs=36 + NbMPPT=12 + TanPhiMin=-0.750 + TanPhiMax=0.750 + NbMSInterne=2 + MasterSlave=No_M_S + IsolSurvey =Yes + DC_Switch=Yes + MS_Thresh=0.8 + Night_Loss=5.00 +End of PVObject pvGcomperter +""" +f_obj = io.StringIO(fn_str) +ond_str = iotools.parse_panond(f_obj) + +fn_file = 'C:/Users/Contractor1/Downloads/ET-M772BH550GL.PAN' +mod_file = iotools.read_panond(fn_file) + +fn_str = """PVObject_=pvModule + Version=7.2 + Flags=$00900243 + + PVObject_Commercial=pvCommercial + Comment=ET SOLAR + Flags=$0041 + Manufacturer=ET SOLAR + Model=ET-M772BH550GL + DataSource=Manufacturer 2021 + YearBeg=2021 + Width=1.134 + Height=2.278 + Depth=0.035 + Weight=32.000 + NPieces=100 + PriceDate=06/04/22 12:39 + End of PVObject pvCommercial + + Technol=mtSiMono + NCelS=72 + NCelP=2 + NDiode=3 + SubModuleLayout=slTwinHalfCells + FrontSurface=fsARCoating + GRef=1000 + TRef=25.0 + PNom=550.0 + PNomTolUp=0.90 + BifacialityFactor=0.700 + Isc=14.000 + Voc=49.90 + Imp=13.110 + Vmp=41.96 + muISC=7.28 + muVocSpec=-128.0 + muPmpReq=-0.340 + RShunt=300 + Rp_0=2000 + Rp_Exp=5.50 + RSerie=0.203 + Gamma=0.980 + muGamma=-0.0001 + VMaxIEC=1500 + VMaxUL=1500 + Absorb=0.90 + ARev=3.200 + BRev=16.716 + RDiode=0.010 + VRevDiode=-0.70 + IMaxDiode=30.0 + AirMassRef=1.500 + CellArea=165.1 + SandiaAMCorr=50.000 + + PVObject_IAM=pvIAM + Flags=$00 + IAMMode=UserProfile + IAMProfile=TCubicProfile + NPtsMax=9 + NPtsEff=9 + LastCompile=$B18D + Mode=3 + Point_1=0.0,1.00000 + Point_2=20.0,1.00000 + Point_3=30.0,1.00000 + Point_4=40.0,0.99000 + Point_5=50.0,0.98000 + Point_6=60.0,0.96000 + Point_7=70.0,0.89000 + Point_8=80.0,0.66000 + Point_9=90.0,0.00000 + End of TCubicProfile + End of PVObject pvIAM +End of PVObject pvModule +""" +f_obj = io.StringIO(fn_str) +mod_str = iotools.parse_panond(f_obj) + +stop = 1 \ No newline at end of file From b65eb63186047441f9abdd06536e1570c6a2553c Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 08:21:56 -0700 Subject: [PATCH 18/47] More info in nested levels --- pvlib/iotools/panond.py | 5 ++--- pvlib/tests/iotools/test_panond.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index d7a8c66409..6217ca80f2 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -57,7 +57,8 @@ def parse_panond(fbuf): new_level = {} current_level[key] = new_level dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] - #TODO: add value of indentation as a key in that new level + current_level = dict_levels[indent_lvl_1 + 1] + current_level[key] = value elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict current_level = dict_levels[indent_lvl_1] @@ -76,5 +77,3 @@ def read_panond(file): content = parse_panond(fbuf) return content - - diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 8c8d785423..8f7534e9c1 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -1,9 +1,9 @@ -from ... import iotools +from ...iotools import read_panond, parse_panond import io # Not sure if I am creating these test scenarios correctly fn_file = 'C:/Users/Contractor1/Downloads/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' -ond_file = iotools.read_panond(fn_file) +ond_file = read_panond(fn_file) fn_str = """PVObject_=pvGInverter Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 @@ -153,10 +153,10 @@ End of PVObject pvGcomperter """ f_obj = io.StringIO(fn_str) -ond_str = iotools.parse_panond(f_obj) +ond_str = parse_panond(f_obj) fn_file = 'C:/Users/Contractor1/Downloads/ET-M772BH550GL.PAN' -mod_file = iotools.read_panond(fn_file) +mod_file = read_panond(fn_file) fn_str = """PVObject_=pvModule Version=7.2 @@ -235,6 +235,6 @@ End of PVObject pvModule """ f_obj = io.StringIO(fn_str) -mod_str = iotools.parse_panond(f_obj) +mod_str = parse_panond(f_obj) stop = 1 \ No newline at end of file From cde8ce0828c044118105d4a3141fe20835c58255 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 09:15:49 -0700 Subject: [PATCH 19/47] Added Comments --- pvlib/iotools/panond.py | 60 +++++++++++++++++++++++++++++- pvlib/tests/iotools/test_panond.py | 4 ++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 6217ca80f2..3d424de5e6 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -1,3 +1,7 @@ +""" +Get .PAN or .OND file data into a nested dictionary. +""" + import io def num_type(value): @@ -37,7 +41,33 @@ def element_type(element): def parse_panond(fbuf): - # Parse a .pan or .ond file provided a file-like object + """ + Parse a .pan or .ond text file into a nested dictionary. + + Parameters + ---------- + fbuf : File-like object + Buffer of a .pan or .ond file + + Returns + ------- + comp : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ comp = {} # Component dict_levels = [comp] @@ -68,7 +98,33 @@ def parse_panond(fbuf): def read_panond(file): - # Read a .pan file or string into a nested dictionary. + """ + Retrieve Module or Inverter data from a .pan or .ond text file, respectively. + + Parameters + ---------- + file : string or path object + Name or path of a .pan/.ond file + + Returns + ------- + content : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ with open(file, "r", encoding='utf-8-sig') as file: f_content = file.read() diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 8f7534e9c1..c4c0e503ee 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -1,3 +1,7 @@ +""" +test iotools for panond +""" + from ...iotools import read_panond, parse_panond import io # Not sure if I am creating these test scenarios correctly From 6b271cc28a0ac9a9ca91d9c6eb4c0aa13ad5b615 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 09:21:13 -0700 Subject: [PATCH 20/47] Added whatsnew documentation --- docs/sphinx/source/whatsnew/v0.10.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index e7816020bd..e08d1cce08 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -120,6 +120,7 @@ Enhancements :py:func:`~pvlib.iotools.get_acis_station_data`, and :py:func:`~pvlib.iotools.get_acis_available_stations`. (:issue:`1293`, :pull:`1767`) +* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond' Bug fixes ~~~~~~~~~ @@ -184,3 +185,4 @@ Contributors * Devon Watt (:ghuser:`d-watt`) * Todd Karin (:ghuser:`toddkarin`) * Corey Pullium (:ghuser:`cpullium`) +* Connor Krening (:ghuser:'ckrening') From abb0c8485017a5f1963a2090ab0bdf6b4e3a176a Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 09:25:00 -0700 Subject: [PATCH 21/47] Corrected formatting --- docs/sphinx/source/whatsnew/v0.10.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index e08d1cce08..fa06e7d8a3 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -121,6 +121,9 @@ Enhancements :py:func:`~pvlib.iotools.get_acis_available_stations`. (:issue:`1293`, :pull:`1767`) * Added .pan/.ond reader function :py:func:'pvlib.iotools.panond' +* Added .pan/.ond reader function +:py:func:`pvlib.iotools.panond` +(:issue:`1747` ) Bug fixes ~~~~~~~~~ From 50f8b4adf203745f0fda86f07cb16da1e59de0ad Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 12:09:43 -0700 Subject: [PATCH 22/47] Adressing stickler comments --- pvlib/iotools/panond.py | 135 --------------------------------------- pvlib/iotools/pvsyst.py | 137 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 135 deletions(-) delete mode 100644 pvlib/iotools/panond.py create mode 100644 pvlib/iotools/pvsyst.py diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py deleted file mode 100644 index 3d424de5e6..0000000000 --- a/pvlib/iotools/panond.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Get .PAN or .OND file data into a nested dictionary. -""" - -import io - -def num_type(value): - # Determine if a value is float, int or leave as string - if '.' in value: - try: # Detect float - value_out = float(value) - return value_out - - except ValueError: # Otherwise leave as string - value_out = value - return value_out - - else: - - try: # Detect int - value_out = int(value) - return value_out - - except ValueError: # Otherwise leave as string - value_out = value - return value_out - - -def element_type(element): - # Determine if an element is a list then pass to num_type() - if ',' in element: # Detect a list - values = element.split(',') - element_out = [] - for val in values: # Determine datatype of each value - element_out.append(num_type(val)) - - return element_out - - else: - return num_type(element) - - -def parse_panond(fbuf): - """ - Parse a .pan or .ond text file into a nested dictionary. - - Parameters - ---------- - fbuf : File-like object - Buffer of a .pan or .ond file - - Returns - ------- - comp : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. - - Raises - ------ - - Notes - ----- - - See Also - -------- - - References - ---------- - """ - comp = {} # Component - dict_levels = [comp] - - fbuf.seek(0) - lines = fbuf.getvalue().splitlines() - for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. - if lines[i] == '': # Skipping blank lines - continue - - indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 - indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 - line_data = lines[i].split('=') - key = line_data[0].strip() - value = element_type(line_data[1].strip()) if len(line_data) > 1 else None - if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. - current_level = dict_levels[indent_lvl_1] - new_level = {} - current_level[key] = new_level - dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] - current_level = dict_levels[indent_lvl_1 + 1] - current_level[key] = value - - elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict - current_level = dict_levels[indent_lvl_1] - current_level[key] = value - - return comp - - -def read_panond(file): - """ - Retrieve Module or Inverter data from a .pan or .ond text file, respectively. - - Parameters - ---------- - file : string or path object - Name or path of a .pan/.ond file - - Returns - ------- - content : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. - - Raises - ------ - - Notes - ----- - - See Also - -------- - - References - ---------- - """ - - with open(file, "r", encoding='utf-8-sig') as file: - f_content = file.read() - fbuf = io.StringIO(f_content) - - content = parse_panond(fbuf) - - return content diff --git a/pvlib/iotools/pvsyst.py b/pvlib/iotools/pvsyst.py new file mode 100644 index 0000000000..bcb1e95b06 --- /dev/null +++ b/pvlib/iotools/pvsyst.py @@ -0,0 +1,137 @@ +""" +Get .PAN or .OND file data into a nested dictionary. +""" + +import io + +def num_type(value): + # Determine if a value is float, int or leave as string + + if '.' in value: + try: # Detect float + value_out = float(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + else: + + try: # Detect int + value_out = int(value) + return value_out + + except ValueError: # Otherwise leave as string + value_out = value + return value_out + + +def element_type(element): + # Determine if an element is a list then pass to num_type() + + if ',' in element: # Detect a list + values = element.split(',') + element_out = [] + for val in values: # Determine datatype of each value + element_out.append(num_type(val)) + + return element_out + + else: + return num_type(element) + + +def parse_panond(fbuf): + """ + Parse a .pan or .ond text file into a nested dictionary. + + Parameters + ---------- + fbuf : File-like object + Buffer of a .pan or .ond file + + Returns + ------- + comp : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ + comp = {} # Component + dict_levels = [comp] + + fbuf.seek(0) + lines = fbuf.getvalue().splitlines() + for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. + if lines[i] == '': # Skipping blank lines + continue + + indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 + indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + line_data = lines[i].split('=') + key = line_data[0].strip() + value = element_type(line_data[1].strip()) if len(line_data) > 1 else None + if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. + current_level = dict_levels[indent_lvl_1] + new_level = {} + current_level[key] = new_level + dict_levels = dict_levels[: indent_lvl_1 + 1] + [new_level] + current_level = dict_levels[indent_lvl_1 + 1] + current_level[key] = value + + elif indent_lvl_2 <= indent_lvl_1: # add key/value to dict + current_level = dict_levels[indent_lvl_1] + current_level[key] = value + + return comp + + +def read_panond(file): + """ + Retrieve Module or Inverter data from a .pan or .ond text file, respectively. + + Parameters + ---------- + file : string or path object + Name or path of a .pan/.ond file + + Returns + ------- + content : Nested Dictionary + Contents of the .pan or .ond file following the indentation of the file. + The value of datatypes are assumed during reading. The value units are + the default used by PVsyst. + + Raises + ------ + + Notes + ----- + + See Also + -------- + + References + ---------- + """ + + with open(file, "r", encoding='utf-8-sig') as file: + f_content = file.read() + fbuf = io.StringIO(f_content) + + content = parse_panond(fbuf) + + return content From 2f84976d5fd4cf1ff0db283aedfa6ea687d09f80 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 13:03:06 -0700 Subject: [PATCH 23/47] Updated testing script --- ...CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND | 146 ++++++++++++++++++ pvlib/data/ET-M772BH550GL.PAN | 75 +++++++++ .../{test_panond.py => test_pvsyst.py} | 9 +- 3 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND create mode 100644 pvlib/data/ET-M772BH550GL.PAN rename pvlib/tests/iotools/{test_panond.py => test_pvsyst.py} (96%) diff --git a/pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND b/pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND new file mode 100644 index 0000000000..c13700ce76 --- /dev/null +++ b/pvlib/data/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND @@ -0,0 +1,146 @@ +PVObject_=pvGInverter + Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 + Version=6.81 + ParObj1=2020 + Flags=$00381562 + + PVObject_Commercial=pvCommercial + Comment=www.chintpower.com (China) + Flags=$0041 + Manufacturer=ChintPower + Model=CPS SCH275KTL-DO/US-800 + DataSource=Manufacturer 2020 + YearBeg=2020 + Width=0.680 + Height=0.337 + Depth=1.100 + Weight=95.000 + NPieces=0 + PriceDate=02/06/20 00:02 + Currency=EUR + Remarks, Count=2 + Str_1=Protection: -30 - +60, IP 66: outdoor installable + Str_2 + End of Remarks + End of PVObject pvCommercial + Transfo=Without + + Converter=TConverter + PNomConv=250.000 + PMaxOUT=250.000 + VOutConv=800.0 + VMppMin=500 + VMPPMax=1500 + VAbsMax=1500 + PSeuil=500.0 + EfficMax=99.01 + EfficEuro=98.49 + FResNorm=0.00 + ModeOper=MPPT + CompPMax=Lim + CompVMax=Lim + MonoTri=Tri + ModeAffEnum=Efficf_POut + UnitAffEnum=kW + PNomDC=253.000 + PMaxDC=375.000 + IDCMax=0.0 + IMaxDC=360.0 + INomAC=181.0 + IMaxAC=199.0 + TPNom=45.0 + TPMax=40.0 + TPLim1=50.0 + TPLimAbs=60.0 + PLim1=225.000 + PLimAbs=90.000 + PInEffMax =150000.000 + PThreshEff=3332.4 + HasdefaultPThresh=False + + ProfilPIO=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8085 + Mode=1 + Point_1=1250,0 + Point_2=7500,6923 + Point_3=12500,11875 + Point_4=25000,24250 + Point_5=50000,49100 + Point_6=75000,73875 + Point_7=150000,148515 + Point_8=250000,246500 + Point_9=275000,270325 + Point_10=0,0 + Point_11=0,0 + End of TCubicProfile + VNomEff=880.0,1174.0,1300.0, + EfficMaxV=98.260,99.040,98.860, + EfficEuroV=97.986,98.860,98.661, + + ProfilPIOV1=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=13012.7,12500.0 + Point_3=25720.2,25000.0 + Point_4=51093.4,50000.0 + Point_5=76437.0,75000.0 + Point_6=127213.5,125000.0 + Point_7=190995.2,187500.0 + Point_8=255440.9,250000.0 + Point_9=281301.1,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV2=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12850.8,12500.0 + Point_3=25401.3,25000.0 + Point_4=50581.7,50000.0 + Point_5=75795.9,75000.0 + Point_6=126211.6,125000.0 + Point_7=189623.8,187500.0 + Point_8=253138.9,250000.0 + Point_9=278763.3,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + + ProfilPIOV3=TCubicProfile + NPtsMax=11 + NPtsEff=9 + LastCompile=$8089 + Mode=1 + Point_1=300.0,0.0 + Point_2=12953.4,12500.0 + Point_3=25512.8,25000.0 + Point_4=50679.1,50000.0 + Point_5=75895.6,75000.0 + Point_6=126441.4,125000.0 + Point_7=189835.0,187500.0 + Point_8=253472.6,250000.0 + Point_9=279017.9,275000.0 + Point_10=0.0,0.0 + Point_11=0.0,0.0 + End of TCubicProfile + End of TConverter + NbInputs=36 + NbMPPT=12 + TanPhiMin=-0.750 + TanPhiMax=0.750 + NbMSInterne=2 + MasterSlave=No_M_S + IsolSurvey =Yes + DC_Switch=Yes + MS_Thresh=0.8 + Night_Loss=5.00 +End of PVObject pvGInverter diff --git a/pvlib/data/ET-M772BH550GL.PAN b/pvlib/data/ET-M772BH550GL.PAN new file mode 100644 index 0000000000..9b2a6a29af --- /dev/null +++ b/pvlib/data/ET-M772BH550GL.PAN @@ -0,0 +1,75 @@ +PVObject_=pvModule + Version=7.2 + Flags=$00900243 + + PVObject_Commercial=pvCommercial + Comment=ET SOLAR + Flags=$0041 + Manufacturer=ET SOLAR + Model=ET-M772BH550GL + DataSource=Manufacturer 2021 + YearBeg=2021 + Width=1.134 + Height=2.278 + Depth=0.035 + Weight=32.000 + NPieces=100 + PriceDate=06/04/22 12:39 + End of PVObject pvCommercial + + Technol=mtSiMono + NCelS=72 + NCelP=2 + NDiode=3 + SubModuleLayout=slTwinHalfCells + FrontSurface=fsARCoating + GRef=1000 + TRef=25.0 + PNom=550.0 + PNomTolUp=0.90 + BifacialityFactor=0.700 + Isc=14.000 + Voc=49.90 + Imp=13.110 + Vmp=41.96 + muISC=7.28 + muVocSpec=-128.0 + muPmpReq=-0.340 + RShunt=300 + Rp_0=2000 + Rp_Exp=5.50 + RSerie=0.203 + Gamma=0.980 + muGamma=-0.0001 + VMaxIEC=1500 + VMaxUL=1500 + Absorb=0.90 + ARev=3.200 + BRev=16.716 + RDiode=0.010 + VRevDiode=-0.70 + IMaxDiode=30.0 + AirMassRef=1.500 + CellArea=165.1 + SandiaAMCorr=50.000 + + PVObject_IAM=pvIAM + Flags=$00 + IAMMode=UserProfile + IAMProfile=TCubicProfile + NPtsMax=9 + NPtsEff=9 + LastCompile=$B18D + Mode=3 + Point_1=0.0,1.00000 + Point_2=20.0,1.00000 + Point_3=30.0,1.00000 + Point_4=40.0,0.99000 + Point_5=50.0,0.98000 + Point_6=60.0,0.96000 + Point_7=70.0,0.89000 + Point_8=80.0,0.66000 + Point_9=90.0,0.00000 + End of TCubicProfile + End of PVObject pvIAM +End of PVObject pvModule diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_pvsyst.py similarity index 96% rename from pvlib/tests/iotools/test_panond.py rename to pvlib/tests/iotools/test_pvsyst.py index c4c0e503ee..626d23e506 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_pvsyst.py @@ -2,11 +2,12 @@ test iotools for panond """ -from ...iotools import read_panond, parse_panond +from pvlib.iotools import read_panond, parse_panond +from pvlib.tests.conftest import DATA_DIR import io # Not sure if I am creating these test scenarios correctly -fn_file = 'C:/Users/Contractor1/Downloads/CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' +fn_file = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' ond_file = read_panond(fn_file) fn_str = """PVObject_=pvGInverter @@ -159,7 +160,7 @@ f_obj = io.StringIO(fn_str) ond_str = parse_panond(f_obj) -fn_file = 'C:/Users/Contractor1/Downloads/ET-M772BH550GL.PAN' +fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' mod_file = read_panond(fn_file) fn_str = """PVObject_=pvModule @@ -240,5 +241,3 @@ """ f_obj = io.StringIO(fn_str) mod_str = parse_panond(f_obj) - -stop = 1 \ No newline at end of file From 68a2899773460c98e2898bd0e739b177b93831f4 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 13:42:27 -0700 Subject: [PATCH 24/47] addressing stickler comments --- pvlib/iotools/{pvsyst.py => panond.py} | 60 +++++++++++-------- .../{test_pvsyst.py => test_panond.py} | 0 2 files changed, 35 insertions(+), 25 deletions(-) rename pvlib/iotools/{pvsyst.py => panond.py} (75%) rename pvlib/tests/iotools/{test_pvsyst.py => test_panond.py} (100%) diff --git a/pvlib/iotools/pvsyst.py b/pvlib/iotools/panond.py similarity index 75% rename from pvlib/iotools/pvsyst.py rename to pvlib/iotools/panond.py index bcb1e95b06..87ad2764c8 100644 --- a/pvlib/iotools/pvsyst.py +++ b/pvlib/iotools/panond.py @@ -5,27 +5,28 @@ import io def num_type(value): - # Determine if a value is float, int or leave as string + + # Determine if a value is float, int or leave as string if '.' in value: try: # Detect float value_out = float(value) return value_out - + except ValueError: # Otherwise leave as string value_out = value return value_out - + else: try: # Detect int value_out = int(value) return value_out - + except ValueError: # Otherwise leave as string value_out = value return value_out - + def element_type(element): # Determine if an element is a list then pass to num_type() @@ -35,7 +36,7 @@ def element_type(element): element_out = [] for val in values: # Determine datatype of each value element_out.append(num_type(val)) - + return element_out else: @@ -50,13 +51,13 @@ def parse_panond(fbuf): ---------- fbuf : File-like object Buffer of a .pan or .ond file - + Returns ------- comp : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. + Contents of the .pan or .ond file following the indentation of the + file. The value of datatypes are assumed during reading. The value + units are the default used by PVsyst. Raises ------ @@ -72,19 +73,27 @@ def parse_panond(fbuf): """ comp = {} # Component dict_levels = [comp] - + fbuf.seek(0) lines = fbuf.getvalue().splitlines() - for i in range(0, len(lines) - 1): # Reading blank lines. Stopping one short to avoid index error. Last line never contains important data. - if lines[i] == '': # Skipping blank lines - continue - indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 - indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 - line_data = lines[i].split('=') + for i in range(0, len(lines) - 1): + if lines[i] == '': # Skipping blank lines + continue + # Reading blank lines. Stopping one short to avoid index error. + # Last line never contains important data. + indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 + indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + line_data = lines[i].split('=') key = line_data[0].strip() - value = element_type(line_data[1].strip()) if len(line_data) > 1 else None - if indent_lvl_2 > indent_lvl_1: # add a level to the dict. The key here will be ignored. Not vital to file function. + # value = element_type(line_data[1].strip()) if len(line_data) > 1 else None + if len(line_data) > 1: + value = element_type(line_data[1].strip()) + else: + value = element_type = None + # add a level to the dict. The key here will be ignored. + # Not vital to file function. + if indent_lvl_2 > indent_lvl_1: current_level = dict_levels[indent_lvl_1] new_level = {} current_level[key] = new_level @@ -101,19 +110,20 @@ def parse_panond(fbuf): def read_panond(file): """ - Retrieve Module or Inverter data from a .pan or .ond text file, respectively. + Retrieve Module or Inverter data from a .pan or .ond text file, + respectively. Parameters ---------- file : string or path object Name or path of a .pan/.ond file - + Returns ------- content : Nested Dictionary - Contents of the .pan or .ond file following the indentation of the file. - The value of datatypes are assumed during reading. The value units are - the default used by PVsyst. + Contents of the .pan or .ond file following the indentation of the + file. The value of datatypes are assumed during reading. The value + units are the default used by PVsyst. Raises ------ @@ -131,7 +141,7 @@ def read_panond(file): with open(file, "r", encoding='utf-8-sig') as file: f_content = file.read() fbuf = io.StringIO(f_content) - + content = parse_panond(fbuf) return content diff --git a/pvlib/tests/iotools/test_pvsyst.py b/pvlib/tests/iotools/test_panond.py similarity index 100% rename from pvlib/tests/iotools/test_pvsyst.py rename to pvlib/tests/iotools/test_panond.py From 6a0eee38665529454d87020fabead12b9ce1891c Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 14:29:15 -0700 Subject: [PATCH 25/47] stickler comments v3 --- pvlib/iotools/panond.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 87ad2764c8..b3e5ec9136 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -4,10 +4,11 @@ import io -def num_type(value): - - # Determine if a value is float, int or leave as string +def num_type(value): + """ + Determine if a value is float, int or a string + """ if '.' in value: try: # Detect float value_out = float(value) @@ -26,11 +27,12 @@ def num_type(value): except ValueError: # Otherwise leave as string value_out = value return value_out - + def element_type(element): - # Determine if an element is a list then pass to num_type() - + """ + Determine if an element is a list then pass to num_type() + """ if ',' in element: # Detect a list values = element.split(',') element_out = [] @@ -86,11 +88,10 @@ def parse_panond(fbuf): indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 line_data = lines[i].split('=') key = line_data[0].strip() - # value = element_type(line_data[1].strip()) if len(line_data) > 1 else None if len(line_data) > 1: value = element_type(line_data[1].strip()) else: - value = element_type = None + value = None # add a level to the dict. The key here will be ignored. # Not vital to file function. if indent_lvl_2 > indent_lvl_1: From 87ae181b26a07513c9f31a09658791819e099650 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 25 May 2023 14:45:44 -0700 Subject: [PATCH 26/47] stickler comment correction --- pvlib/iotools/panond.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index b3e5ec9136..0de46c001b 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -27,7 +27,7 @@ def num_type(value): except ValueError: # Otherwise leave as string value_out = value return value_out - + def element_type(element): """ From efc2bea95d2548112940df487a95e8c255288245 Mon Sep 17 00:00:00 2001 From: Connor Date: Tue, 6 Jun 2023 09:08:43 -0700 Subject: [PATCH 27/47] Updated notes and comments --- pvlib/iotools/panond.py | 53 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 0de46c001b..5d1313682d 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -33,7 +33,9 @@ def element_type(element): """ Determine if an element is a list then pass to num_type() """ - if ',' in element: # Detect a list + if ',' in element: # Detect a list. + # .pan/.ond don't use ',' to indicate 1000. If that changes, + # a new method of list detection needs to be found. values = element.split(',') element_out = [] for val in values: # Determine datatype of each value @@ -66,6 +68,46 @@ def parse_panond(fbuf): Notes ----- + The parser was intended for use with .pan and .ond files that were created + for use by PVsyst. At time of publication, no documentation for these + files was available. So, this parser is based on inferred logic, rather + than anything specified by PVsyst. + + The parser assumes that the file being parsed uses indendation of two + spaces (' ') to create new level in a nested dicitonary, and that + key/values pairs of interest are separated using '='. This further means + that lines not containing '=' were ommitted from the final returned + dictionary. + + Additionally, the indented lines often contain values themselves. This + leads to a conflict with the .pan/.ond file and the ability of nested + dicitonaries to capture that information. The solution implemented here is + to repeat that key to the new nested dictioary within that new level. + Example below. + + Sample file: + + 'level1 = first level + key1 = value1 + level2 = second level + key2 = value2' + + output: + + { + level1: first level + key1: value1, + level2:{ + level2: second level, + key2: value2 + } + } + + The parser takes an additional step to infer the datatype present in + each value. The .pan/.ond files appear to have intentially left datatype + indicators (e.g. floats have '.' decimals). However, there is still the + possibility that the datatype applied from this parser is incorrect. In + that event the user would need to convert to the desired datatype. See Also -------- @@ -84,15 +126,19 @@ def parse_panond(fbuf): continue # Reading blank lines. Stopping one short to avoid index error. # Last line never contains important data. + # Creating variables to assist new level in dictionary creation logic indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + # Split the line into key/value pair line_data = lines[i].split('=') key = line_data[0].strip() + # Logical to make sure there is a value to extract if len(line_data) > 1: value = element_type(line_data[1].strip()) else: value = None - # add a level to the dict. The key here will be ignored. + # add a level to the dict. If a key/value pair triggers the new level, + # the key/value will be repeated in the new dict level. # Not vital to file function. if indent_lvl_2 > indent_lvl_1: current_level = dict_levels[indent_lvl_1] @@ -131,6 +177,9 @@ def read_panond(file): Notes ----- + The read function simply converts a file path to a file-like object and + passes it to the parser. At time of creation, tested .pan/.ond files used + UTF-8 encoding. See Also -------- From dd70a0451d25f990a77cd24945755aa7a35da078 Mon Sep 17 00:00:00 2001 From: Connor Date: Tue, 6 Jun 2023 11:18:30 -0700 Subject: [PATCH 28/47] Minor comment updates and sphinx doc update --- docs/sphinx/source/reference/iotools.rst | 2 ++ pvlib/iotools/panond.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index c902e3181a..1b6af73f47 100644 --- a/docs/sphinx/source/reference/iotools.rst +++ b/docs/sphinx/source/reference/iotools.rst @@ -42,6 +42,8 @@ of sources and file formats relevant to solar energy modeling. iotools.get_acis_mpe iotools.get_acis_station_data iotools.get_acis_available_stations + iotools.read_panond + iotools.parse_panond A :py:class:`~pvlib.location.Location` object may be created from metadata in some files. diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 5d1313682d..03fa6fa327 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -71,8 +71,8 @@ def parse_panond(fbuf): The parser was intended for use with .pan and .ond files that were created for use by PVsyst. At time of publication, no documentation for these files was available. So, this parser is based on inferred logic, rather - than anything specified by PVsyst. - + than anything specified by PVsyst. + The parser assumes that the file being parsed uses indendation of two spaces (' ') to create new level in a nested dicitonary, and that key/values pairs of interest are separated using '='. This further means @@ -91,7 +91,7 @@ def parse_panond(fbuf): key1 = value1 level2 = second level key2 = value2' - + output: { @@ -99,7 +99,7 @@ def parse_panond(fbuf): key1: value1, level2:{ level2: second level, - key2: value2 + key2: value2 } } From dcd26a0642ec0730041904b996f51c2737a6bcc8 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 22 Jun 2023 07:39:58 -0700 Subject: [PATCH 29/47] edited comments for grammar/spelling --- pvlib/iotools/panond.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 03fa6fa327..076ba2511a 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -68,21 +68,21 @@ def parse_panond(fbuf): Notes ----- - The parser was intended for use with .pan and .ond files that were created + The parser is intended for use with .pan and .ond files that were created for use by PVsyst. At time of publication, no documentation for these files was available. So, this parser is based on inferred logic, rather than anything specified by PVsyst. The parser assumes that the file being parsed uses indendation of two - spaces (' ') to create new level in a nested dicitonary, and that + spaces (' ') to create new level in a nested dictionary, and that key/values pairs of interest are separated using '='. This further means - that lines not containing '=' were ommitted from the final returned + that lines not containing '=' were omitted from the final returned dictionary. Additionally, the indented lines often contain values themselves. This - leads to a conflict with the .pan/.ond file and the ability of nested - dicitonaries to capture that information. The solution implemented here is - to repeat that key to the new nested dictioary within that new level. + leads to a conflict with the .pan/.ond file and the ability of nested a + dictionary to capture that information. The solution implemented here is + to repeat that key to the new nested dictionary within that new level. Example below. Sample file: @@ -177,9 +177,9 @@ def read_panond(file): Notes ----- - The read function simply converts a file path to a file-like object and - passes it to the parser. At time of creation, tested .pan/.ond files used - UTF-8 encoding. + The read_panond function simply converts a file path to a file-like object, + passes it to parse-panond, and returns the file content. At time of + creation, tested .pan/.ond files used UTF-8 encoding. See Also -------- From c76c2c5cea50b4f9b822cef980c54eb799fc36e5 Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 29 Jun 2023 11:25:37 -0700 Subject: [PATCH 30/47] improved testing panond reader --- pvlib/tests/iotools/test_panond.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 626d23e506..48c8d01bba 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -5,12 +5,13 @@ from pvlib.iotools import read_panond, parse_panond from pvlib.tests.conftest import DATA_DIR import io -# Not sure if I am creating these test scenarios correctly -fn_file = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' -ond_file = read_panond(fn_file) -fn_str = """PVObject_=pvGInverter +def test_read_ond_file(): + fn_file = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' + ond_file = read_panond(fn_file) + # expected + fn_str = fn_str = """PVObject_=pvGInverter Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 Version=6.81 ParObj1=2020 @@ -157,13 +158,16 @@ Night_Loss=5.00 End of PVObject pvGcomperter """ -f_obj = io.StringIO(fn_str) -ond_str = parse_panond(f_obj) + f_obj = io.StringIO(fn_str) + ond_str = parse_panond(f_obj) + assert ond_file.keys() == ond_str.keys() -fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' -mod_file = read_panond(fn_file) -fn_str = """PVObject_=pvModule +def test_read_pan_file(): + fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' + ond_file = read_panond(fn_file) + # expected + fn_str = """PVObject_=pvModule Version=7.2 Flags=$00900243 @@ -239,5 +243,6 @@ End of PVObject pvIAM End of PVObject pvModule """ -f_obj = io.StringIO(fn_str) -mod_str = parse_panond(f_obj) + f_obj = io.StringIO(fn_str) + ond_str = parse_panond(f_obj) + assert ond_file.keys() == ond_str.keys() From 4f2e9d0efbee24e8e56ef52946f4c3ce9189fb07 Mon Sep 17 00:00:00 2001 From: ckrening <133819570+ckrening@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:22:15 -0700 Subject: [PATCH 31/47] Update pvlib/iotools/panond.py Co-authored-by: Kevin Anderson --- pvlib/iotools/panond.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 076ba2511a..f9efcb60cf 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -183,6 +183,7 @@ def read_panond(file): See Also -------- + parse_panond References ---------- From 108de80bd9b387e268d0ebec225676124f19b1f7 Mon Sep 17 00:00:00 2001 From: ckrening <133819570+ckrening@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:22:32 -0700 Subject: [PATCH 32/47] Update pvlib/iotools/panond.py Co-authored-by: Kevin Anderson --- pvlib/iotools/panond.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index f9efcb60cf..d99e7b5a4d 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -111,6 +111,7 @@ def parse_panond(fbuf): See Also -------- + read_panond References ---------- From b90b9a6279060c3c5080bd7401b0eaa6a2b45cab Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 14 Aug 2023 14:38:11 -0700 Subject: [PATCH 33/47] Adressing some github comments --- pvlib/iotools/panond.py | 18 +----------------- pvlib/tests/iotools/test_panond.py | 6 +++--- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 076ba2511a..fc158bcd35 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -63,9 +63,6 @@ def parse_panond(fbuf): file. The value of datatypes are assumed during reading. The value units are the default used by PVsyst. - Raises - ------ - Notes ----- The parser is intended for use with .pan and .ond files that were created @@ -87,7 +84,7 @@ def parse_panond(fbuf): Sample file: - 'level1 = first level + 'level1 = first level #TODO: kanderson github comment on output format key1 = value1 level2 = second level key2 = value2' @@ -109,11 +106,6 @@ def parse_panond(fbuf): possibility that the datatype applied from this parser is incorrect. In that event the user would need to convert to the desired datatype. - See Also - -------- - - References - ---------- """ comp = {} # Component dict_levels = [comp] @@ -172,20 +164,12 @@ def read_panond(file): file. The value of datatypes are assumed during reading. The value units are the default used by PVsyst. - Raises - ------ - Notes ----- The read_panond function simply converts a file path to a file-like object, passes it to parse-panond, and returns the file content. At time of creation, tested .pan/.ond files used UTF-8 encoding. - See Also - -------- - - References - ---------- """ with open(file, "r", encoding='utf-8-sig') as file: diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 48c8d01bba..63f0253e12 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -165,7 +165,7 @@ def test_read_ond_file(): def test_read_pan_file(): fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' - ond_file = read_panond(fn_file) + pan_file = read_panond(fn_file) # expected fn_str = """PVObject_=pvModule Version=7.2 @@ -244,5 +244,5 @@ def test_read_pan_file(): End of PVObject pvModule """ f_obj = io.StringIO(fn_str) - ond_str = parse_panond(f_obj) - assert ond_file.keys() == ond_str.keys() + pan_str = parse_panond(f_obj) + assert pan_file.keys() == pan_str.keys() From bd5b7fb423f29073c3411bf26c6cfa9a106cd34e Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 14 Aug 2023 14:56:55 -0700 Subject: [PATCH 34/47] Addressing github comments pt.2 --- pvlib/iotools/panond.py | 38 +++++++++++++++++------------- pvlib/tests/iotools/test_panond.py | 4 ++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index fc158bcd35..95f51d1a48 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -5,7 +5,7 @@ import io -def num_type(value): +def _num_type(value): """ Determine if a value is float, int or a string """ @@ -29,9 +29,9 @@ def num_type(value): return value_out -def element_type(element): +def _element_type(element): """ - Determine if an element is a list then pass to num_type() + Determine if an element is a list then pass to _num_type() """ if ',' in element: # Detect a list. # .pan/.ond don't use ',' to indicate 1000. If that changes, @@ -39,12 +39,12 @@ def element_type(element): values = element.split(',') element_out = [] for val in values: # Determine datatype of each value - element_out.append(num_type(val)) + element_out.append(_num_type(val)) return element_out else: - return num_type(element) + return _num_type(element) def parse_panond(fbuf): @@ -84,21 +84,27 @@ def parse_panond(fbuf): Sample file: - 'level1 = first level #TODO: kanderson github comment on output format + 'level1 = first level key1 = value1 - level2 = second level - key2 = value2' + key2 = value2 + level2 = second level + key3 = value3 + key4 = value4 + key5 = value5' output: - { - level1: first level - key1: value1, - level2:{ - level2: second level, - key2: value2 + level1:{ + level1: first level + key1: value1, + key2: value2, + level2:{ + level2: second level, + key3: value3, + key4: value4 + }, + key5: value5 } - } The parser takes an additional step to infer the datatype present in each value. The .pan/.ond files appear to have intentially left datatype @@ -126,7 +132,7 @@ def parse_panond(fbuf): key = line_data[0].strip() # Logical to make sure there is a value to extract if len(line_data) > 1: - value = element_type(line_data[1].strip()) + value = _element_type(line_data[1].strip()) else: value = None # add a level to the dict. If a key/value pair triggers the new level, diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 63f0253e12..3e828a8903 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -160,7 +160,7 @@ def test_read_ond_file(): """ f_obj = io.StringIO(fn_str) ond_str = parse_panond(f_obj) - assert ond_file.keys() == ond_str.keys() + assert ond_file['PVObject_'].keys() == ond_str['PVObject_'].keys() def test_read_pan_file(): @@ -245,4 +245,4 @@ def test_read_pan_file(): """ f_obj = io.StringIO(fn_str) pan_str = parse_panond(f_obj) - assert pan_file.keys() == pan_str.keys() + assert pan_file['PVObject_'].keys() == pan_str['PVObject_'].keys() From c3b80994883c858ff9f016bd832fe068c43d62ac Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 14 Aug 2023 15:04:47 -0700 Subject: [PATCH 35/47] github comments pt.3 --- docs/sphinx/source/whatsnew/v0.10.0.rst | 5 ----- docs/sphinx/source/whatsnew/v0.10.2.rst | 6 +++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index fa06e7d8a3..e7816020bd 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -120,10 +120,6 @@ Enhancements :py:func:`~pvlib.iotools.get_acis_station_data`, and :py:func:`~pvlib.iotools.get_acis_available_stations`. (:issue:`1293`, :pull:`1767`) -* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond' -* Added .pan/.ond reader function -:py:func:`pvlib.iotools.panond` -(:issue:`1747` ) Bug fixes ~~~~~~~~~ @@ -188,4 +184,3 @@ Contributors * Devon Watt (:ghuser:`d-watt`) * Todd Karin (:ghuser:`toddkarin`) * Corey Pullium (:ghuser:`cpullium`) -* Connor Krening (:ghuser:'ckrening') diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index 4b18a7fe9b..287b35c557 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -11,6 +11,10 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond' +* Added .pan/.ond reader function +:py:func:`pvlib.iotools.panond` +(:issue:`1747` ) Bug fixes @@ -31,4 +35,4 @@ Requirements Contributors ~~~~~~~~~~~~ - +* Connor Krening (:ghuser:`ckrening`) \ No newline at end of file From bbbb480c4592a76670e59c9e032df9575d92d765 Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 14 Aug 2023 15:29:24 -0700 Subject: [PATCH 36/47] flake8 formatting --- pvlib/iotools/panond.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 06ea46d093..4dfa0350f3 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -132,7 +132,7 @@ def parse_panond(fbuf): # Logical to make sure there is a value to extract if len(line_data) > 1: value = _element_type(line_data[1].strip()) - + else: value = None # add a level to the dict. If a key/value pair triggers the new level, From d84a8f0bf4fe102b38253a81dd7c45cae99449c3 Mon Sep 17 00:00:00 2001 From: Connor Date: Tue, 15 Aug 2023 08:31:51 -0700 Subject: [PATCH 37/47] Flake8 formatting --- pvlib/iotools/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index fa25a6abb6..61029c5639 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -28,6 +28,3 @@ from pvlib.iotools.acis import get_acis_mpe # noqa: F401 from pvlib.iotools.acis import get_acis_station_data # noqa: F401 from pvlib.iotools.acis import get_acis_available_stations # noqa: F401 -from pvlib.iotools.panond import read_panond # noqa: F401 -from pvlib.iotools.panond import parse_panond # noqa: F401 -from pvlib.iotools.acis import get_acis_available_stations # noqa: F401 From b37b98cb12304d48d5b3c651e3d46bb0404291a1 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 11:22:56 -0400 Subject: [PATCH 38/47] remove unnecessary doc entries --- docs/sphinx/source/reference/iotools.rst | 2 -- docs/sphinx/source/whatsnew/v0.10.0.rst | 4 ---- 2 files changed, 6 deletions(-) diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index 8a83af75e6..1b6af73f47 100644 --- a/docs/sphinx/source/reference/iotools.rst +++ b/docs/sphinx/source/reference/iotools.rst @@ -37,8 +37,6 @@ of sources and file formats relevant to solar energy modeling. iotools.get_cams iotools.read_cams iotools.parse_cams - iotools.read_panond - iotools.parse_panond iotools.get_acis_prism iotools.get_acis_nrcc iotools.get_acis_mpe diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index d9ec3d2adc..e7816020bd 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -72,9 +72,6 @@ Deprecations Enhancements ~~~~~~~~~~~~ -* Added .pan/.ond reader function -:py:func:`pvlib.iotools.panond` -(:issue:`1747` ) * Added two new irradiance decomposition models: :py:func:`pvlib.irradiance.orgill_hollands` (:pull:`1730`) and :py:func:`pvlib.irradiance.louche` (:pull:`1705`). * The return values of :py:func:`pvlib.pvsystem.calcparams_desoto`, @@ -163,7 +160,6 @@ Requirements Contributors ~~~~~~~~~~~~ * Taos Transue (:ghuser:`reepoi`) -* Connor Krening (:ghuser:`ckrening`) * Nicholas Riedel-Lyngskær (:ghuser:`nicorie`) * Adam R. Jensen (:ghuser:`AdamRJensen`) * Echedey Luis (:ghuser:`echedey-ls`) From b32270709219acfcaa14ae76c5d28e64e9834c1b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 11:23:04 -0400 Subject: [PATCH 39/47] whatsnew cleanup --- docs/sphinx/source/whatsnew/v0.10.2.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index c50dd90ab1..6830b1071b 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -11,10 +11,7 @@ Deprecations Enhancements ~~~~~~~~~~~~ -* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond' -* Added .pan/.ond reader function -:py:func:`pvlib.iotools.panond` -(:issue:`1747` ) +* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond'. (:issue:`1747`, :pull:`1749`) * Added support for dates to be specified as strings in the iotools get functions: :py:func:`pvlib.iotools.get_pvgis_hourly`, :py:func:`pvlib.iotools.get_cams`, :py:func:`pvlib.iotools.get_bsrn`, and :py:func:`pvlib.iotools.read_midc_raw_data_from_nrel`. From 9c8744f359967e46c572bfe68169bd25a3828b4e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 11:26:20 -0400 Subject: [PATCH 40/47] encoding parameter, and fix parsing both file buffers and file-like objects --- pvlib/iotools/panond.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 4dfa0350f3..e9d351275e 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -115,8 +115,7 @@ def parse_panond(fbuf): comp = {} # Component dict_levels = [comp] - fbuf.seek(0) - lines = fbuf.getvalue().splitlines() + lines = fbuf.read().splitlines() for i in range(0, len(lines) - 1): if lines[i] == '': # Skipping blank lines @@ -153,16 +152,20 @@ def parse_panond(fbuf): return comp -def read_panond(file): +def read_panond(filename, encoding=None): """ Retrieve Module or Inverter data from a .pan or .ond text file, respectively. Parameters ---------- - file : string or path object + filename : str or path object Name or path of a .pan/.ond file + encoding : str, optional + Encoding of the file. Some files may require specifying + ``encoding='utf-8-sig'`` to import correctly. + Returns ------- content : Nested Dictionary @@ -177,10 +180,7 @@ def read_panond(file): creation, tested .pan/.ond files used UTF-8 encoding. """ - with open(file, "r", encoding='utf-8-sig') as file: - f_content = file.read() - fbuf = io.StringIO(f_content) - - content = parse_panond(fbuf) + with open(filename, "r", encoding=encoding) as fbuf: + content = parse_panond(fbuf) return content From 5c03b30f3895cdc04f51b117402281dbd39ebeac Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 11:26:31 -0400 Subject: [PATCH 41/47] docstring improvements --- pvlib/iotools/panond.py | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index e9d351275e..050b89bb22 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -58,11 +58,15 @@ def parse_panond(fbuf): Returns ------- - comp : Nested Dictionary + comp : dict Contents of the .pan or .ond file following the indentation of the file. The value of datatypes are assumed during reading. The value units are the default used by PVsyst. + See Also + -------- + read_panond + Notes ----- The parser is intended for use with .pan and .ond files that were created @@ -80,31 +84,6 @@ def parse_panond(fbuf): leads to a conflict with the .pan/.ond file and the ability of nested a dictionary to capture that information. The solution implemented here is to repeat that key to the new nested dictionary within that new level. - Example below. - - Sample file: - - 'level1 = first level - key1 = value1 - key2 = value2 - level2 = second level - key3 = value3 - key4 = value4 - key5 = value5' - - output: - - level1:{ - level1: first level - key1: value1, - key2: value2, - level2:{ - level2: second level, - key3: value3, - key4: value4 - }, - key5: value5 - } The parser takes an additional step to infer the datatype present in each value. The .pan/.ond files appear to have intentially left datatype @@ -168,11 +147,15 @@ def read_panond(filename, encoding=None): Returns ------- - content : Nested Dictionary + content : dict Contents of the .pan or .ond file following the indentation of the file. The value of datatypes are assumed during reading. The value units are the default used by PVsyst. + See Also + -------- + parse_panond + Notes ----- The read_panond function simply converts a file path to a file-like object, From febfcfd9ed93fbee0c1f5eddb30095212c5a66ce Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 11:26:41 -0400 Subject: [PATCH 42/47] test improvements --- pvlib/tests/iotools/test_panond.py | 279 ++++------------------------- 1 file changed, 37 insertions(+), 242 deletions(-) diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 3e828a8903..f44248a757 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -4,245 +4,40 @@ from pvlib.iotools import read_panond, parse_panond from pvlib.tests.conftest import DATA_DIR -import io - - -def test_read_ond_file(): - fn_file = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' - ond_file = read_panond(fn_file) - # expected - fn_str = fn_str = """PVObject_=pvGInverter - Comment=ChintPower CPS SCH275KTL-DO/US-800 Manufacturer 2020 - Version=6.81 - ParObj1=2020 - Flags=$00381562 - - PVObject_Commercial=pvCommercial - Comment=www.chintpower.com (China) - Flags=$0041 - Manufacturer=ChintPower - Model=CPS SCH275KTL-DO/US-800 - DataSource=Manufacturer 2020 - YearBeg=2020 - Width=0.680 - Height=0.337 - Depth=1.100 - Weight=95.000 - NPieces=0 - PriceDate=02/06/20 00:02 - Currency=EUR - Remarks, Count=2 - Str_1=Protection: -30 - +60, IP 66: outdoor installable - Str_2 - End of Remarks - End of PVObject pvCommercial - Transfo=Without - - Converter=TConverter - PNomConv=250.000 - PMaxOUT=250.000 - VOutConv=800.0 - VMppMin=500 - VMPPMax=1500 - VAbsMax=1500 - PSeuil=500.0 - EfficMax=99.01 - EfficEuro=98.49 - FResNorm=0.00 - ModeOper=MPPT - CompPMax=Lim - CompVMax=Lim - MonoTri=Tri - ModeAffEnum=Efficf_POut - UnitAffEnum=kW - PNomDC=253.000 - PMaxDC=375.000 - IDCMax=0.0 - IMaxDC=360.0 - INomAC=181.0 - IMaxAC=199.0 - TPNom=45.0 - TPMax=40.0 - TPLim1=50.0 - TPLimAbs=60.0 - PLim1=225.000 - PLimAbs=90.000 - PInEffMax =150000.000 - PThreshEff=3332.4 - HasdefaultPThresh=False - - ProfilPIO=TCubicProfile - NPtsMax=11 - NPtsEff=9 - LastCompile=$8085 - Mode=1 - Point_1=1250,0 - Point_2=7500,6923 - Point_3=12500,11875 - Point_4=25000,24250 - Point_5=50000,49100 - Point_6=75000,73875 - Point_7=150000,148515 - Point_8=250000,246500 - Point_9=275000,270325 - Point_10=0,0 - Point_11=0,0 - End of TCubicProfile - VNomEff=880.0,1174.0,1300.0, - EfficMaxV=98.260,99.040,98.860, - EfficEuroV=97.986,98.860,98.661, - - ProfilPIOV1=TCubicProfile - NPtsMax=11 - NPtsEff=9 - LastCompile=$8089 - Mode=1 - Point_1=300.0,0.0 - Point_2=13012.7,12500.0 - Point_3=25720.2,25000.0 - Point_4=51093.4,50000.0 - Point_5=76437.0,75000.0 - Point_6=127213.5,125000.0 - Point_7=190995.2,187500.0 - Point_8=255440.9,250000.0 - Point_9=281301.1,275000.0 - Point_10=0.0,0.0 - Point_11=0.0,0.0 - End of TCubicProfile - - ProfilPIOV2=TCubicProfile - NPtsMax=11 - NPtsEff=9 - LastCompile=$8089 - Mode=1 - Point_1=300.0,0.0 - Point_2=12850.8,12500.0 - Point_3=25401.3,25000.0 - Point_4=50581.7,50000.0 - Point_5=75795.9,75000.0 - Point_6=126211.6,125000.0 - Point_7=189623.8,187500.0 - Point_8=253138.9,250000.0 - Point_9=278763.3,275000.0 - Point_10=0.0,0.0 - Point_11=0.0,0.0 - End of TCubicProfile - - ProfilPIOV3=TCubicProfile - NPtsMax=11 - NPtsEff=9 - LastCompile=$8089 - Mode=1 - Point_1=300.0,0.0 - Point_2=12953.4,12500.0 - Point_3=25512.8,25000.0 - Point_4=50679.1,50000.0 - Point_5=75895.6,75000.0 - Point_6=126441.4,125000.0 - Point_7=189835.0,187500.0 - Point_8=253472.6,250000.0 - Point_9=279017.9,275000.0 - Point_10=0.0,0.0 - Point_11=0.0,0.0 - End of TCubicProfile - End of TConverter - NbInputs=36 - NbMPPT=12 - TanPhiMin=-0.750 - TanPhiMax=0.750 - NbMSInterne=2 - MasterSlave=No_M_S - IsolSurvey =Yes - DC_Switch=Yes - MS_Thresh=0.8 - Night_Loss=5.00 -End of PVObject pvGcomperter -""" - f_obj = io.StringIO(fn_str) - ond_str = parse_panond(f_obj) - assert ond_file['PVObject_'].keys() == ond_str['PVObject_'].keys() - - -def test_read_pan_file(): - fn_file = DATA_DIR / 'ET-M772BH550GL.PAN' - pan_file = read_panond(fn_file) - # expected - fn_str = """PVObject_=pvModule - Version=7.2 - Flags=$00900243 - - PVObject_Commercial=pvCommercial - Comment=ET SOLAR - Flags=$0041 - Manufacturer=ET SOLAR - Model=ET-M772BH550GL - DataSource=Manufacturer 2021 - YearBeg=2021 - Width=1.134 - Height=2.278 - Depth=0.035 - Weight=32.000 - NPieces=100 - PriceDate=06/04/22 12:39 - End of PVObject pvCommercial - - Technol=mtSiMono - NCelS=72 - NCelP=2 - NDiode=3 - SubModuleLayout=slTwinHalfCells - FrontSurface=fsARCoating - GRef=1000 - TRef=25.0 - PNom=550.0 - PNomTolUp=0.90 - BifacialityFactor=0.700 - Isc=14.000 - Voc=49.90 - Imp=13.110 - Vmp=41.96 - muISC=7.28 - muVocSpec=-128.0 - muPmpReq=-0.340 - RShunt=300 - Rp_0=2000 - Rp_Exp=5.50 - RSerie=0.203 - Gamma=0.980 - muGamma=-0.0001 - VMaxIEC=1500 - VMaxUL=1500 - Absorb=0.90 - ARev=3.200 - BRev=16.716 - RDiode=0.010 - VRevDiode=-0.70 - IMaxDiode=30.0 - AirMassRef=1.500 - CellArea=165.1 - SandiaAMCorr=50.000 - - PVObject_IAM=pvIAM - Flags=$00 - IAMMode=UserProfile - IAMProfile=TCubicProfile - NPtsMax=9 - NPtsEff=9 - LastCompile=$B18D - Mode=3 - Point_1=0.0,1.00000 - Point_2=20.0,1.00000 - Point_3=30.0,1.00000 - Point_4=40.0,0.99000 - Point_5=50.0,0.98000 - Point_6=60.0,0.96000 - Point_7=70.0,0.89000 - Point_8=80.0,0.66000 - Point_9=90.0,0.00000 - End of TCubicProfile - End of PVObject pvIAM -End of PVObject pvModule -""" - f_obj = io.StringIO(fn_str) - pan_str = parse_panond(f_obj) - assert pan_file['PVObject_'].keys() == pan_str['PVObject_'].keys() +import pytest + +PAN_FILE = DATA_DIR / 'ET-M772BH550GL.PAN' +OND_FILE = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' + + +@pytest.mark.parametrize('filename', [PAN_FILE, OND_FILE]) +def test_read_panond(filename): + # test that reading a file returns the same as parsing its contents + data_read = read_panond(filename, encoding='utf-8-sig') + with open(filename, 'r', encoding='utf-8-sig') as f: + data_parse = parse_panond(f) + assert data_read == data_parse + + +def test_read_panond_contents(): + # test that returned contents have expected keys, types, and structure + + pan = read_panond(PAN_FILE, encoding='utf-8-sig') + assert list(pan.keys()) == ['PVObject_'] + pan = pan['PVObject_'] + assert pan['PVObject_Commercial']['Model'] == 'ET-M772BH550GL' + assert pan['Voc'] == 49.9 + assert pan['PVObject_IAM']['IAMProfile']['Point_5'] == [50.0, 0.98] + assert pan['BifacialityFactor'] == 0.7 + assert pan['FrontSurface'] == 'fsARCoating' + assert pan['Technol'] == 'mtSiMono' + + ond = read_panond(OND_FILE, encoding='utf-8-sig') + assert list(ond.keys()) == ['PVObject_'] + ond = ond['PVObject_'] + assert ond['PVObject_Commercial']['Model'] == 'CPS SCH275KTL-DO/US-800' + assert ond['TanPhiMin'] == -0.75 + assert ond['NbMPPT'] == 12 + assert ond['Converter']['ModeOper'] == 'MPPT' + assert ond['Converter']['ProfilPIOV2']['Point_5'] == [75795.9, 75000.0] + \ No newline at end of file From 92a8db258cbc1e7ff50a9e506323f63a2959f542 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 12:42:22 -0400 Subject: [PATCH 43/47] lint --- pvlib/iotools/panond.py | 2 -- pvlib/tests/iotools/test_panond.py | 1 - 2 files changed, 3 deletions(-) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 050b89bb22..81bea8ec05 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -2,8 +2,6 @@ Get .PAN or .OND file data into a nested dictionary. """ -import io - def _num_type(value): """ diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index f44248a757..841f793377 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -40,4 +40,3 @@ def test_read_panond_contents(): assert ond['NbMPPT'] == 12 assert ond['Converter']['ModeOper'] == 'MPPT' assert ond['Converter']['ProfilPIOV2']['Point_5'] == [75795.9, 75000.0] - \ No newline at end of file From 5f196bcfb8194c49834c3c785a87316e598d4532 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 12:42:47 -0400 Subject: [PATCH 44/47] update FAQ entry on PAN/OND files --- docs/sphinx/source/user_guide/faq.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/user_guide/faq.rst b/docs/sphinx/source/user_guide/faq.rst index eacbaddb3f..15d7a04ac4 100644 --- a/docs/sphinx/source/user_guide/faq.rst +++ b/docs/sphinx/source/user_guide/faq.rst @@ -66,9 +66,11 @@ irradiance datasets, including the BSRN, SURFRAD, SRML, and NREL's MIDC. Can I use PVsyst (PAN/OND) files with pvlib? -------------------------------------------- -Currently, pvlib does not have the ability to import any PVsyst file formats. -Certain formats of particular interest (e.g. PAN files) may be added in a future -version. Until then, these Google Group threads +Although pvlib does include a function to read PAN and OND files +(:py:func:`~pvlib.iotools.read_panond`), it is up to the user to determine +whether and how the imported parameter values can be used with pvlib's models. +Easier use of these parameter files with the rest of pvlib may be added +in a future version. Until then, these Google Group threads (`one `_ and `two `_) may be useful for some users. From 859151afbee670a79df40a0c48dddea03aa4b12a Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 16:14:10 -0400 Subject: [PATCH 45/47] Apply suggestions from code review Co-authored-by: Cliff Hansen --- docs/sphinx/source/user_guide/faq.rst | 2 +- pvlib/iotools/panond.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/user_guide/faq.rst b/docs/sphinx/source/user_guide/faq.rst index 15d7a04ac4..39e3bec841 100644 --- a/docs/sphinx/source/user_guide/faq.rst +++ b/docs/sphinx/source/user_guide/faq.rst @@ -66,7 +66,7 @@ irradiance datasets, including the BSRN, SURFRAD, SRML, and NREL's MIDC. Can I use PVsyst (PAN/OND) files with pvlib? -------------------------------------------- -Although pvlib does include a function to read PAN and OND files +Although pvlib includes a function to read PAN and OND files (:py:func:`~pvlib.iotools.read_panond`), it is up to the user to determine whether and how the imported parameter values can be used with pvlib's models. Easier use of these parameter files with the rest of pvlib may be added diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 81bea8ec05..7c524837fd 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -72,10 +72,10 @@ def parse_panond(fbuf): files was available. So, this parser is based on inferred logic, rather than anything specified by PVsyst. - The parser assumes that the file being parsed uses indendation of two - spaces (' ') to create new level in a nested dictionary, and that + The parser assumes that the file being parsed uses indentation of two + spaces (' ') to create a new level in a nested dictionary, and that key/values pairs of interest are separated using '='. This further means - that lines not containing '=' were omitted from the final returned + that lines not containing '=' are omitted from the final returned dictionary. Additionally, the indented lines often contain values themselves. This From 2bd20333b651b75da851c68a300289703ab3baaa Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 16:59:11 -0400 Subject: [PATCH 46/47] make parse function private --- docs/sphinx/source/reference/iotools.rst | 2 +- pvlib/iotools/__init__.py | 1 - pvlib/iotools/panond.py | 69 ++++++++++-------------- pvlib/tests/iotools/test_panond.py | 14 +---- 4 files changed, 31 insertions(+), 55 deletions(-) diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index 1b6af73f47..39bd4f2ad3 100644 --- a/docs/sphinx/source/reference/iotools.rst +++ b/docs/sphinx/source/reference/iotools.rst @@ -43,7 +43,7 @@ of sources and file formats relevant to solar energy modeling. iotools.get_acis_station_data iotools.get_acis_available_stations iotools.read_panond - iotools.parse_panond + A :py:class:`~pvlib.location.Location` object may be created from metadata in some files. diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index 61029c5639..9935719b29 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -22,7 +22,6 @@ from pvlib.iotools.sodapro import read_cams # noqa: F401 from pvlib.iotools.sodapro import parse_cams # noqa: F401 from pvlib.iotools.panond import read_panond # noqa: F401 -from pvlib.iotools.panond import parse_panond # noqa: F401 from pvlib.iotools.acis import get_acis_prism # noqa: F401 from pvlib.iotools.acis import get_acis_nrcc # noqa: F401 from pvlib.iotools.acis import get_acis_mpe # noqa: F401 diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 7c524837fd..2bc8363674 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -45,7 +45,7 @@ def _element_type(element): return _num_type(element) -def parse_panond(fbuf): +def _parse_panond(fbuf): """ Parse a .pan or .ond text file into a nested dictionary. @@ -56,41 +56,13 @@ def parse_panond(fbuf): Returns ------- - comp : dict + component_info : dict Contents of the .pan or .ond file following the indentation of the file. The value of datatypes are assumed during reading. The value units are the default used by PVsyst. - - See Also - -------- - read_panond - - Notes - ----- - The parser is intended for use with .pan and .ond files that were created - for use by PVsyst. At time of publication, no documentation for these - files was available. So, this parser is based on inferred logic, rather - than anything specified by PVsyst. - - The parser assumes that the file being parsed uses indentation of two - spaces (' ') to create a new level in a nested dictionary, and that - key/values pairs of interest are separated using '='. This further means - that lines not containing '=' are omitted from the final returned - dictionary. - - Additionally, the indented lines often contain values themselves. This - leads to a conflict with the .pan/.ond file and the ability of nested a - dictionary to capture that information. The solution implemented here is - to repeat that key to the new nested dictionary within that new level. - - The parser takes an additional step to infer the datatype present in - each value. The .pan/.ond files appear to have intentially left datatype - indicators (e.g. floats have '.' decimals). However, there is still the - possibility that the datatype applied from this parser is incorrect. In - that event the user would need to convert to the desired datatype. """ - comp = {} # Component - dict_levels = [comp] + component_info = {} # Component + dict_levels = [component_info] lines = fbuf.read().splitlines() @@ -126,7 +98,7 @@ def parse_panond(fbuf): current_level = dict_levels[indent_lvl_1] current_level[key] = value - return comp + return component_info def read_panond(filename, encoding=None): @@ -150,18 +122,33 @@ def read_panond(filename, encoding=None): file. The value of datatypes are assumed during reading. The value units are the default used by PVsyst. - See Also - -------- - parse_panond - Notes ----- - The read_panond function simply converts a file path to a file-like object, - passes it to parse-panond, and returns the file content. At time of - creation, tested .pan/.ond files used UTF-8 encoding. + The parser is intended for use with .pan and .ond files that were created + for use by PVsyst. At time of publication, no documentation for these + files was available. So, this parser is based on inferred logic, rather + than anything specified by PVsyst. At time of creation, tested + .pan/.ond files used UTF-8 encoding. + + The parser assumes that the file being parsed uses indentation of two + spaces (' ') to create a new level in a nested dictionary, and that + key/values pairs of interest are separated using '='. This further means + that lines not containing '=' are omitted from the final returned + dictionary. + + Additionally, the indented lines often contain values themselves. This + leads to a conflict with the .pan/.ond file and the ability of nested a + dictionary to capture that information. The solution implemented here is + to repeat that key to the new nested dictionary within that new level. + + The parser takes an additional step to infer the datatype present in + each value. The .pan/.ond files appear to have intentially left datatype + indicators (e.g. floats have '.' decimals). However, there is still the + possibility that the datatype applied from this parser is incorrect. In + that event the user would need to convert to the desired datatype. """ with open(filename, "r", encoding=encoding) as fbuf: - content = parse_panond(fbuf) + content = _parse_panond(fbuf) return content diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index 841f793377..a692d3e119 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -2,24 +2,14 @@ test iotools for panond """ -from pvlib.iotools import read_panond, parse_panond +from pvlib.iotools import read_panond from pvlib.tests.conftest import DATA_DIR -import pytest PAN_FILE = DATA_DIR / 'ET-M772BH550GL.PAN' OND_FILE = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' -@pytest.mark.parametrize('filename', [PAN_FILE, OND_FILE]) -def test_read_panond(filename): - # test that reading a file returns the same as parsing its contents - data_read = read_panond(filename, encoding='utf-8-sig') - with open(filename, 'r', encoding='utf-8-sig') as f: - data_parse = parse_panond(f) - assert data_read == data_parse - - -def test_read_panond_contents(): +def test_read_panond(): # test that returned contents have expected keys, types, and structure pan = read_panond(PAN_FILE, encoding='utf-8-sig') From 94d90d66c77f06338ff9aed81aac077eb3029b9e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 16:59:21 -0400 Subject: [PATCH 47/47] fix overlooked whatsnew issue --- docs/sphinx/source/whatsnew/v0.10.2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index 6830b1071b..c2e062d1a1 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -11,7 +11,7 @@ Deprecations Enhancements ~~~~~~~~~~~~ -* Added .pan/.ond reader function :py:func:'pvlib.iotools.panond'. (:issue:`1747`, :pull:`1749`) +* Added .pan/.ond reader function :py:func:`pvlib.iotools.read_panond`. (:issue:`1747`, :pull:`1749`) * Added support for dates to be specified as strings in the iotools get functions: :py:func:`pvlib.iotools.get_pvgis_hourly`, :py:func:`pvlib.iotools.get_cams`, :py:func:`pvlib.iotools.get_bsrn`, and :py:func:`pvlib.iotools.read_midc_raw_data_from_nrel`.