From 19b02b235640cadb80cbcb961f06d3665a5d4c70 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:47:32 -0700 Subject: [PATCH 01/15] update documentation --- docs/sphinx/source/reference/iotools.rst | 1 + docs/sphinx/source/whatsnew/v0.13.1.rst | 5 +- pvlib/iotools/__init__.py | 1 + pvlib/iotools/pan_binary.py | 383 +++++++++++++++++++++++ 4 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 pvlib/iotools/pan_binary.py diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index cbf89c71a7..f8da4e4186 100644 --- a/docs/sphinx/source/reference/iotools.rst +++ b/docs/sphinx/source/reference/iotools.rst @@ -226,6 +226,7 @@ Functions for reading irradiance/weather data files. iotools.read_epw iotools.parse_epw iotools.read_panond + iotools.read_pan_binary A :py:class:`~pvlib.location.Location` object may be created from metadata diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index 14ae31170b..58be138eff 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -19,6 +19,8 @@ Bug fixes Enhancements ~~~~~~~~~~~~ +* Add support for reading PAN binary files using :py:func:`~pvlib.iotools.pan_binary.read_pan_binary`. + (:issue:`2504`) Documentation @@ -48,4 +50,5 @@ Contributors ~~~~~~~~~~~~ * Elijah Passmore (:ghuser:`eljpsm`) * Rajiv Daxini (:ghuser:`RDaxini`) -* Omar Bahamida (:ghuser:`OmarBahamida`) \ No newline at end of file +* Omar Bahamida (:ghuser:`OmarBahamida`) +* Kurt Rhee (:ghuser:`kurt-rhee`) diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index 352044e5cd..05db24158b 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -27,6 +27,7 @@ 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.pan_binary import read_pan_binary # 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/pan_binary.py b/pvlib/iotools/pan_binary.py new file mode 100644 index 0000000000..723a485dd6 --- /dev/null +++ b/pvlib/iotools/pan_binary.py @@ -0,0 +1,383 @@ +""" +Older versions of PAN files created by PVsyst use a Borland Pascal Real48 format. + +This is based on: + https://github.com/CanadianSolar/CASSYS/blob/b5487bb4e9e77174c805d64e3c960c46d357b7e2/CASSYS%20Interface/DatabaseImportModule.vba#L4 +""" + +import struct + +# --- Constants --- +SEMICOLON_MARKER = 0x3B +DOT_MARKER = 0x09 +DOUBLE_DOT_MARKER = 0x0A +FORWARD_SLASH_MARKER = 0x2F +CR_MARKER = 0x0D # Carriage Return +VERTICAL_BAR_MARKER = 0xA6 + + +# --- Supporting Functions --- +def _read48_to_float(*, real48): + """ + Convert a 6-byte Delphi Real48 encoded value to a standard Python float. + + Parameters + ---------- + real48 : bytes + 6-byte Delphi Real48 encoded value + + Returns + ------- + value : float + Converted float value + + Notes + ----- + The format consists of: + - 1 byte: Exponent (offset by 129) + - 5 bytes: Mantissa, with the last bit of the 5th byte as the sign bit. + """ + if not real48 or len(real48) != 6 or real48[0] == 0: + return 0.0 + + # The exponent is the first byte, with an offset of 129 + exponent = float(real48[0] - 129) + + mantissa = 0.0 + + # Process the first 4 bytes of the mantissa + # The division by 256 (or multiplication by 0.00390625) shifts the bytes + for i in range(4, 0, -1): + mantissa += real48[i] + mantissa /= 256.0 + + # Process the 5th byte of the mantissa + mantissa += real48[5] & 0x7F # Use only the first 7 bits + mantissa /= 128.0 # equivalent to * 0.0078125 + mantissa += 1.0 + + # Check the sign bit (the last bit of the 6th byte) + if (real48[5] & 0x80) == 0x80: + mantissa = -mantissa + + # Final calculation using the exponent + return mantissa * (2.0**exponent) + + +def _find_marker_index(*, marker, start_index, byte_array): + """ + Find the index of the first occurrence of a hex marker after a start index. + + Parameters + ---------- + marker : int + Hex marker to search for + start_index : int + Starting index for the search + byte_array : bytes + Byte array to search in + + Returns + ------- + index : int + Index right after the marker, or raises ValueError if not found + """ + # bytearray.find is more efficient than a manual loop + found_index = byte_array.find(bytes([marker]), start_index) + if found_index != -1: + return found_index + 1 + if found_index is None: + raise ValueError(f"Marker {marker} not found in byte array") + return found_index + + +def _get_param_index(*, start_index, offset_num): + """ + Calculate the start index of a Real48 parameter. + + Parameters + ---------- + start_index : int + Starting index of the Real48 data block + offset_num : int + Offset number of the parameter + + Returns + ------- + index : int + Start index of the Real48 parameter + """ + return start_index + 6 * offset_num + + +def _extract_byte_parameters(*, byte_array, start_index, num_bytes): + """ + Extract bytes that form a single parameter from the original byte array. + + Parameters + ---------- + byte_array : bytes + Original byte array containing the whole file + start_index : int + Starting index for extraction + num_bytes : int + Number of bytes to extract + + Returns + ------- + param_bytes : bytes + Extracted byte sequence forming a single parameter + """ + # Check bounds to avoid index errors + if start_index + num_bytes > len(byte_array): + raise IndexError( + f"Not enough bytes: need {num_bytes} bytes starting at {start_index}" + ) + + # Extract the specified number of bytes starting at start_index + param_byte_sequence = byte_array[start_index : start_index + num_bytes] + + return param_byte_sequence + + +# This format might be specific to how PAN files format their floats +value_format = "{:.2f}" + + +def _extract_iam_profile(*, start_index, byte_array): + """ + Extract the IAM (Incidence Angle Modifier) profile. + + Parameters + ---------- + start_index : int + Starting index of the IAM data in the byte array + byte_array : bytes + Byte array containing the file data + + Returns + ------- + iam_profile : list of dict + List of dictionaries containing 'aoi' and 'modifier' values + """ + iam_profile = [] + + for i in range(0, 45, 5): # 0 to 44 step 5 (matches VB.NET loop) + # Extract AOI value + aoi_index = _get_param_index(start_index=start_index, offset_num=i) + aoi_bytes = _extract_byte_parameters( + byte_array=byte_array, start_index=aoi_index, num_bytes=6 + ) + aoi_raw = _read48_to_float(real48=aoi_bytes) + aoi_formatted = value_format.format(aoi_raw) # Keep for the check + + # Check if AOI is not null/empty (like VB.NET vbNullString check) + if aoi_formatted != "": + # Extract modifier value + modifier_index = _get_param_index(start_index=start_index, offset_num=i + 1) + modifier_bytes = _extract_byte_parameters( + byte_array=byte_array, start_index=modifier_index, num_bytes=6 + ) + modifier_raw = _read48_to_float(real48=modifier_bytes) + + # Add to profile (only if AOI is not empty) + iam_profile.append({"aoi": aoi_raw, "modifier": modifier_raw}) + # If AOI is empty, we skip this entry entirely (don't add to list) + return iam_profile + + +def read_pan_binary(*, filename): + """ + Retrieve Module data from a .pan binary file. + + Parameters + ---------- + filename : str or path object + Name or path of a .pan binary file + + Returns + ------- + content : dict + Contents of the .pan file. + + 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 can only be used on PVsyst pan files that were created via + the older binary format. For files that use the newer text format + please refer to `pvlib.iotools.panond.read_panond`. + + """ + data = {} + + # Read the file and convert to byte array + with open(filename, "rb") as file: + byte_array = file.read() + + if not byte_array: + raise ValueError("File is empty") + + # --- Find start indices for string parameters --- + try: + manu_start_index = _find_marker_index( + marker=SEMICOLON_MARKER, start_index=0, byte_array=byte_array + ) + panel_start_index = _find_marker_index( + marker=DOT_MARKER, start_index=0, byte_array=byte_array + ) + source_start_index = _find_marker_index( + marker=DOT_MARKER, start_index=panel_start_index, byte_array=byte_array + ) + version_start_index = _find_marker_index( + marker=DOUBLE_DOT_MARKER, + start_index=source_start_index, + byte_array=byte_array, + ) + version_end_index = _find_marker_index( + marker=SEMICOLON_MARKER, + start_index=version_start_index, + byte_array=byte_array, + ) + year_start_index = _find_marker_index( + marker=SEMICOLON_MARKER, + start_index=version_end_index, + byte_array=byte_array, + ) + technology_start_index = _find_marker_index( + marker=DOUBLE_DOT_MARKER, + start_index=year_start_index, + byte_array=byte_array, + ) + cells_in_series_start_index = _find_marker_index( + marker=SEMICOLON_MARKER, + start_index=technology_start_index, + byte_array=byte_array, + ) + cells_in_parallel_start_index = _find_marker_index( + marker=SEMICOLON_MARKER, + start_index=cells_in_series_start_index, + byte_array=byte_array, + ) + bypass_diodes_start_index = _find_marker_index( + marker=SEMICOLON_MARKER, + start_index=cells_in_parallel_start_index, + byte_array=byte_array, + ) + + # --- Find start of Real48 encoded data --- + cr_counter = 0 + real48_start_index = 0 + for i, byte in enumerate(byte_array): + if byte == CR_MARKER: + cr_counter += 1 + if cr_counter == 3: + real48_start_index = i + 2 # Skip + break + + if real48_start_index == 0: + return {"error": "Could not find start of Real48 data block."} + + # --- Extract string parameters --- + # Note: latin-1 is used as it can decode any byte value without error + data["Manufacturer"] = ( + byte_array[manu_start_index : panel_start_index - 1] + .decode("latin-1") + .strip() + ) + data["Model"] = ( + byte_array[panel_start_index : source_start_index - 1] + .decode("latin-1") + .strip() + ) + data["Source"] = ( + byte_array[source_start_index : version_start_index - 4] + .decode("latin-1") + .strip() + ) + data["Version"] = ( + byte_array[version_start_index : version_end_index - 2] + .decode("latin-1") + .replace("Version", "PVsyst") + .strip() + ) + data["Year"] = ( + byte_array[year_start_index : year_start_index + 4] + .decode("latin-1") + .strip() + ) + data["Technology"] = ( + byte_array[technology_start_index : cells_in_series_start_index - 1] + .decode("latin-1") + .strip() + ) + data["Cells_In_Series"] = ( + byte_array[cells_in_series_start_index : cells_in_parallel_start_index - 1] + .decode("latin-1") + .strip() + ) + data["Cells_In_Parallel"] = ( + byte_array[cells_in_parallel_start_index : bypass_diodes_start_index - 1] + .decode("latin-1") + .strip() + ) + + # --- Parse Real48 encoded parameters --- + param_map = { + "PNom": 0, + "VMax": 1, + "Tolerance": 2, + "AreaM": 3, + "CellArea": 4, + "GRef": 5, + "TRef": 6, + "Isc": 8, + "muISC": 9, + "Voc": 10, + "muVocSpec": 11, + "Imp": 12, + "Vmp": 13, + "BypassDiodeVoltage": 14, + "RShunt": 17, + "RSerie": 18, + "RShunt_0": 23, + "RShunt_exp": 24, + "muPmp": 25, + } + + for name, offset in param_map.items(): + start = _get_param_index(start_index=real48_start_index, offset_num=offset) + end = start + 6 + param_bytes = byte_array[start:end] + value = _read48_to_float(real48=param_bytes) + if name == "Tolerance": + value *= 100 # Convert to percentage + if value > 100: + value = 0.0 + data[name] = value + + # --- Check for and Parse IAM Profile --- + dot_counter = 0 + iam_start_index = 0 + dot_position = data["Version"].find(".") + major_version = int(data["Version"][dot_position - 1 : dot_position]) + if major_version < 6: + for i in range(real48_start_index + 170, len(byte_array)): + if byte_array[i] == DOT_MARKER: + dot_counter += 1 + if dot_counter == 2: + iam_start_index = i + 4 + break + + if iam_start_index > 0: + data["IAMProfile"] = _extract_iam_profile( + start_index=iam_start_index, byte_array=byte_array + ) + + except (IndexError, TypeError, struct.error) as e: + return {"error": f"Failed to parse binary PAN file: {e}"} + + return data From 870402d56c029b6f967826991da1ae496da2d21b Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:58:56 -0700 Subject: [PATCH 02/15] add binary reader --- pvlib/iotools/pan_binary.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 723a485dd6..73cc6bdf4b 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -17,7 +17,7 @@ # --- Supporting Functions --- -def _read48_to_float(*, real48): +def _read48_to_float(real48): """ Convert a 6-byte Delphi Real48 encoded value to a standard Python float. @@ -64,7 +64,7 @@ def _read48_to_float(*, real48): return mantissa * (2.0**exponent) -def _find_marker_index(*, marker, start_index, byte_array): +def _find_marker_index(marker, start_index, byte_array): """ Find the index of the first occurrence of a hex marker after a start index. @@ -91,7 +91,7 @@ def _find_marker_index(*, marker, start_index, byte_array): return found_index -def _get_param_index(*, start_index, offset_num): +def _get_param_index(start_index, offset_num): """ Calculate the start index of a Real48 parameter. @@ -110,7 +110,7 @@ def _get_param_index(*, start_index, offset_num): return start_index + 6 * offset_num -def _extract_byte_parameters(*, byte_array, start_index, num_bytes): +def _extract_byte_parameters(byte_array, start_index, num_bytes): """ Extract bytes that form a single parameter from the original byte array. @@ -144,7 +144,7 @@ def _extract_byte_parameters(*, byte_array, start_index, num_bytes): value_format = "{:.2f}" -def _extract_iam_profile(*, start_index, byte_array): +def _extract_iam_profile(start_index, byte_array): """ Extract the IAM (Incidence Angle Modifier) profile. @@ -186,7 +186,7 @@ def _extract_iam_profile(*, start_index, byte_array): return iam_profile -def read_pan_binary(*, filename): +def read_pan_binary(filename): """ Retrieve Module data from a .pan binary file. From c63e794eadd556662d5369ca36c2fb49b48ff247 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:06:12 -0700 Subject: [PATCH 03/15] update line lengths --- pvlib/iotools/pan_binary.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 73cc6bdf4b..0736410d16 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -1,8 +1,11 @@ """ -Older versions of PAN files created by PVsyst use a Borland Pascal Real48 format. +Older versions of PAN files created by PVsyst use a Borland Pascal Real48 +format. This is based on: - https://github.com/CanadianSolar/CASSYS/blob/b5487bb4e9e77174c805d64e3c960c46d357b7e2/CASSYS%20Interface/DatabaseImportModule.vba#L4 + https://github.com/CanadianSolar/CASSYS/blob/ + b5487bb4e9e77174c805d64e3c960c46d357b7e2/CASSYS%20Interface/ + DatabaseImportModule.vba#L4 """ import struct @@ -131,7 +134,8 @@ def _extract_byte_parameters(byte_array, start_index, num_bytes): # Check bounds to avoid index errors if start_index + num_bytes > len(byte_array): raise IndexError( - f"Not enough bytes: need {num_bytes} bytes starting at {start_index}" + f"Not enough bytes: need {num_bytes} bytes starting at " + f"{start_index}" ) # Extract the specified number of bytes starting at start_index @@ -174,7 +178,9 @@ def _extract_iam_profile(start_index, byte_array): # Check if AOI is not null/empty (like VB.NET vbNullString check) if aoi_formatted != "": # Extract modifier value - modifier_index = _get_param_index(start_index=start_index, offset_num=i + 1) + modifier_index = _get_param_index( + start_index=start_index, offset_num=i + 1 + ) modifier_bytes = _extract_byte_parameters( byte_array=byte_array, start_index=modifier_index, num_bytes=6 ) @@ -230,7 +236,9 @@ def read_pan_binary(filename): marker=DOT_MARKER, start_index=0, byte_array=byte_array ) source_start_index = _find_marker_index( - marker=DOT_MARKER, start_index=panel_start_index, byte_array=byte_array + marker=DOT_MARKER, + start_index=panel_start_index, + byte_array=byte_array ) version_start_index = _find_marker_index( marker=DOUBLE_DOT_MARKER, @@ -310,17 +318,23 @@ def read_pan_binary(filename): .strip() ) data["Technology"] = ( - byte_array[technology_start_index : cells_in_series_start_index - 1] + byte_array[ + technology_start_index : cells_in_series_start_index - 1 + ] .decode("latin-1") .strip() ) data["Cells_In_Series"] = ( - byte_array[cells_in_series_start_index : cells_in_parallel_start_index - 1] + byte_array[ + cells_in_series_start_index : cells_in_parallel_start_index - 1 + ] .decode("latin-1") .strip() ) data["Cells_In_Parallel"] = ( - byte_array[cells_in_parallel_start_index : bypass_diodes_start_index - 1] + byte_array[ + cells_in_parallel_start_index : bypass_diodes_start_index - 1 + ] .decode("latin-1") .strip() ) @@ -349,7 +363,9 @@ def read_pan_binary(filename): } for name, offset in param_map.items(): - start = _get_param_index(start_index=real48_start_index, offset_num=offset) + start = _get_param_index( + start_index=real48_start_index, offset_num=offset + ) end = start + 6 param_bytes = byte_array[start:end] value = _read48_to_float(real48=param_bytes) From b5f4bd89e9a9eaa6bcb3f8aa390550101788d9f1 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:10:13 -0700 Subject: [PATCH 04/15] flake8 errors --- pvlib/iotools/pan_binary.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 0736410d16..b9cfd6486e 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -139,7 +139,7 @@ def _extract_byte_parameters(byte_array, start_index, num_bytes): ) # Extract the specified number of bytes starting at start_index - param_byte_sequence = byte_array[start_index : start_index + num_bytes] + param_byte_sequence = byte_array[start_index:start_index + num_bytes] return param_byte_sequence @@ -292,48 +292,48 @@ def read_pan_binary(filename): # --- Extract string parameters --- # Note: latin-1 is used as it can decode any byte value without error data["Manufacturer"] = ( - byte_array[manu_start_index : panel_start_index - 1] + byte_array[manu_start_index: panel_start_index - 1] .decode("latin-1") .strip() ) data["Model"] = ( - byte_array[panel_start_index : source_start_index - 1] + byte_array[panel_start_index: source_start_index - 1] .decode("latin-1") .strip() ) data["Source"] = ( - byte_array[source_start_index : version_start_index - 4] + byte_array[source_start_index: version_start_index - 4] .decode("latin-1") .strip() ) data["Version"] = ( - byte_array[version_start_index : version_end_index - 2] + byte_array[version_start_index: version_end_index - 2] .decode("latin-1") .replace("Version", "PVsyst") .strip() ) data["Year"] = ( - byte_array[year_start_index : year_start_index + 4] + byte_array[year_start_index: year_start_index + 4] .decode("latin-1") .strip() ) data["Technology"] = ( byte_array[ - technology_start_index : cells_in_series_start_index - 1 + technology_start_index: cells_in_series_start_index - 1 ] .decode("latin-1") .strip() ) data["Cells_In_Series"] = ( byte_array[ - cells_in_series_start_index : cells_in_parallel_start_index - 1 + cells_in_series_start_index: cells_in_parallel_start_index - 1 ] .decode("latin-1") .strip() ) data["Cells_In_Parallel"] = ( byte_array[ - cells_in_parallel_start_index : bypass_diodes_start_index - 1 + cells_in_parallel_start_index: bypass_diodes_start_index - 1 ] .decode("latin-1") .strip() @@ -379,7 +379,7 @@ def read_pan_binary(filename): dot_counter = 0 iam_start_index = 0 dot_position = data["Version"].find(".") - major_version = int(data["Version"][dot_position - 1 : dot_position]) + major_version = int(data["Version"][dot_position - 1: dot_position]) if major_version < 6: for i in range(real48_start_index + 170, len(byte_array)): if byte_array[i] == DOT_MARKER: From 134e4653f9abd984886ab10bf04ab52ed89be9fb Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:19:47 -0700 Subject: [PATCH 05/15] flake8 --- pvlib/iotools/pan_binary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index b9cfd6486e..54bc2580c8 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -90,7 +90,7 @@ def _find_marker_index(marker, start_index, byte_array): if found_index != -1: return found_index + 1 if found_index is None: - raise ValueError(f"Marker {marker} not found in byte array") + raise ValueError(f"Marker {marker} is not in byte array") return found_index From 8479ac171fc9cc0e19693811d563705e6da493e9 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:47:49 -0700 Subject: [PATCH 06/15] Update pvlib/iotools/pan_binary.py Co-authored-by: Cliff Hansen --- pvlib/iotools/pan_binary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 54bc2580c8..5792d5d861 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -208,8 +208,8 @@ def read_pan_binary(filename): 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 + The parser is intended for use with binary .pan files that were created for + PVsyst version 6.39 or earlier. 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. From 0fdaf04b6d3dc5ecb38947623768a32b9634e56f Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:48:01 -0700 Subject: [PATCH 07/15] Update pvlib/iotools/pan_binary.py Co-authored-by: Cliff Hansen --- pvlib/iotools/pan_binary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 5792d5d861..4df7c7614c 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -213,8 +213,8 @@ def read_pan_binary(filename): files was available. So, this parser is based on inferred logic, rather than anything specified by PVsyst. - The parser can only be used on PVsyst pan files that were created via - the older binary format. For files that use the newer text format + The parser can only be used on binary .pan files. + For files that use the newer text format please refer to `pvlib.iotools.panond.read_panond`. """ From 5cc5467154cc25e5ee00c65871dfb52235904b5b Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:03:48 -0700 Subject: [PATCH 08/15] line length --- pvlib/iotools/pan_binary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 4df7c7614c..cc2d881770 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -209,9 +209,9 @@ def read_pan_binary(filename): Notes ----- The parser is intended for use with binary .pan files that were created for - PVsyst version 6.39 or earlier. 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. + PVsyst version 6.39 or earlier. 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 can only be used on binary .pan files. For files that use the newer text format From 116c0e16554c3d22138e31b46c99e794e0a8f208 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:13:42 -0700 Subject: [PATCH 09/15] Update docs/sphinx/source/whatsnew/v0.13.1.rst Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- docs/sphinx/source/whatsnew/v0.13.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index 58be138eff..54c6b958b2 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -19,7 +19,7 @@ Bug fixes Enhancements ~~~~~~~~~~~~ -* Add support for reading PAN binary files using :py:func:`~pvlib.iotools.pan_binary.read_pan_binary`. +* Add support for reading PAN binary files (PVsyst v6.39 and earlier) using :py:func:`pvlib.iotools.read_pan_binary`. (:issue:`2504`) From a6440f0f1d81f621ba9b2c580fb1131507fdc6fd Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:13:57 -0700 Subject: [PATCH 10/15] Update pvlib/iotools/pan_binary.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- pvlib/iotools/pan_binary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index cc2d881770..868a31d377 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -1,5 +1,5 @@ """ -Older versions of PAN files created by PVsyst use a Borland Pascal Real48 +Read older versions of PAN files created by PVsyst ( Date: Tue, 29 Jul 2025 15:14:05 -0700 Subject: [PATCH 11/15] Update pvlib/iotools/pan_binary.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- pvlib/iotools/pan_binary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 868a31d377..62fc6cdbdb 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -194,7 +194,7 @@ def _extract_iam_profile(start_index, byte_array): def read_pan_binary(filename): """ - Retrieve Module data from a .pan binary file. + Retreive module data from a .pan binary file, for PVsyst v6.39 and earlier . Parameters ---------- From 9689fe01966cb5754d528795fd1ab9d341868025 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:14:11 -0700 Subject: [PATCH 12/15] Update pvlib/iotools/pan_binary.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- pvlib/iotools/pan_binary.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 62fc6cdbdb..edc9ad9870 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -217,6 +217,9 @@ def read_pan_binary(filename): For files that use the newer text format please refer to `pvlib.iotools.panond.read_panond`. + See also + -------- + pvlib.iotools.panond.read_panond : for newer text-based data format """ data = {} From ea9afd8efdae230ab90946dfc350ac3215932da8 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:14:20 -0700 Subject: [PATCH 13/15] Update pvlib/iotools/pan_binary.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- pvlib/iotools/pan_binary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index edc9ad9870..7968721876 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -397,6 +397,6 @@ def read_pan_binary(filename): ) except (IndexError, TypeError, struct.error) as e: - return {"error": f"Failed to parse binary PAN file: {e}"} + raise ValueError(f"Unable to parse binary PAN file. Is this a binary file and compatible with PVsyst up to 6.39?" from e return data From b9fc09ea2f5e9acdf9f1a90bd58c81db9a632f73 Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:19:03 -0700 Subject: [PATCH 14/15] flake8 --- pvlib/iotools/pan_binary.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 7968721876..3ee1f7abc8 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -1,6 +1,6 @@ """ -Read older versions of PAN files created by PVsyst ( Date: Tue, 29 Jul 2025 16:41:04 -0700 Subject: [PATCH 15/15] Update pvlib/iotools/pan_binary.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- pvlib/iotools/pan_binary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/pan_binary.py b/pvlib/iotools/pan_binary.py index 3ee1f7abc8..a670bfd539 100644 --- a/pvlib/iotools/pan_binary.py +++ b/pvlib/iotools/pan_binary.py @@ -220,7 +220,7 @@ def read_pan_binary(filename): See also -------- - pvlib.iotools.panond.read_panond : for newer text-based data format + pvlib.iotools.read_panond : for newer text-based data format """ data = {}