diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index c902e3181a..39bd4f2ad3 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 + A :py:class:`~pvlib.location.Location` object may be created from metadata in some files. diff --git a/docs/sphinx/source/user_guide/faq.rst b/docs/sphinx/source/user_guide/faq.rst index eacbaddb3f..39e3bec841 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 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 +in a future version. Until then, these Google Group threads (`one `_ and `two `_) may be useful for some users. diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index d542c2c4c8..c2e062d1a1 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -11,6 +11,7 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* 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`. @@ -63,6 +64,7 @@ Requirements Contributors ~~~~~~~~~~~~ +* Connor Krening (:ghuser:`ckrening`) * Adam R. Jensen (:ghuser:`AdamRJensen`) * Abigail Jones (:ghuser:`ajonesr`) * Taos Transue (:ghuser:`reepoi`) 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 a179bfbf9a..9935719b29 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -21,6 +21,7 @@ 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 # 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 new file mode 100644 index 0000000000..2bc8363674 --- /dev/null +++ b/pvlib/iotools/panond.py @@ -0,0 +1,154 @@ +""" +Get .PAN or .OND file data into a nested dictionary. +""" + + +def _num_type(value): + """ + Determine if a value is float, int or a 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. + # .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 + 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 + ------- + 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. + """ + component_info = {} # Component + dict_levels = [component_info] + + lines = fbuf.read().splitlines() + + 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. + # 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. 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] + 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 component_info + + +def read_panond(filename, encoding=None): + """ + Retrieve Module or Inverter data from a .pan or .ond text file, + respectively. + + Parameters + ---------- + 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 : 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. + + 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. 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) + + return content diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py new file mode 100644 index 0000000000..a692d3e119 --- /dev/null +++ b/pvlib/tests/iotools/test_panond.py @@ -0,0 +1,32 @@ +""" +test iotools for panond +""" + +from pvlib.iotools import read_panond +from pvlib.tests.conftest import DATA_DIR + +PAN_FILE = DATA_DIR / 'ET-M772BH550GL.PAN' +OND_FILE = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' + + +def test_read_panond(): + # 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]