إنتقل إلى المحتوى الرئيسي

إصلاح مصفوفة الترتيب F

بيان المشكلة

يفشل ترميز/فك ترميز Protobuf لمصفوفات NumPy إذا كانت بترتيب F

الاختبار

from mcbackend.npproto.utils import ndarray_to_numpy, ndarray_from_numpy

arrC = numpy.array([
[1,2,3],
[4,5,6],
], order="C")

arrF = numpy.array([
[1,2,3],
[4,5,6],
], order="F")

assert arrC.strides != arrF.strides

numpy.testing.assert_array_equal(
arrC,
ndarray_to_numpy(ndarray_from_numpy(arrC)),
)
numpy.testing.assert_array_equal(
arrF,
ndarray_to_numpy(ndarray_from_numpy(arrF)),
)

تغييرات الكود (Code diff)

mcbackend/__init__.py

diff --git a/mcbackend/__init__.py b/mcbackend/__init__.py
index 35ef7b9..c6a43eb 100644
--- a/mcbackend/__init__.py
+++ b/mcbackend/__init__.py
@@ -1,23 +1,23 @@
"""
A framework agnostic implementation for storage of MCMC draws.
"""
from .backends.numpy import NumPyBackend
from .core import Backend, Chain, Run
from .meta import ChainMeta, Coordinate, DataVariable, ExtendedValue, RunMeta, Variable

# Backends
try:
from .backends import clickhouse
from .backends.clickhouse import ClickHouseBackend
except ModuleNotFoundError:
pass

# Adapters
try:
from .adapters import pymc
from .adapters.pymc import TraceBackend
except ModuleNotFoundError:
pass


-__version__ = "0.2.3"
+__version__ = "0.2.4"

mcbackend/npproto/utils.py

diff --git a/mcbackend/npproto/utils.py b/mcbackend/npproto/utils.py
index 27bc98c..8ae5396 100644
--- a/mcbackend/npproto/utils.py
+++ b/mcbackend/npproto/utils.py
@@ -1,40 +1,46 @@
"""
Helper functions such as converters between ``ndarray`` and ``Ndarray``.
"""
import numpy

from . import Ndarray


def ndarray_from_numpy(arr: numpy.ndarray) -> Ndarray:
dt = str(arr.dtype)
if "datetime64" in dt:
# datetime64 doesn't support the buffer protocol.
# See https://github.com/numpy/numpy/issues/4983
# This is a hack that automatically encodes it as int64.
arr = arr.astype("int64")
+ # With non-C-ordered arrays (e.g. Fortran-ordered) the underlying buffer
+ # does not match the strides of the original array anymore, because
+ # ``bytes(arr.data)`` always returns the data in C-order.
+ # Therefore, the array is made C-contiguous before extracting data and strides.
+ if not arr.flags.c_contiguous:
+ arr = numpy.ascontiguousarray(arr)
return Ndarray(
shape=list(arr.shape),
dtype=dt,
data=bytes(arr.data),
strides=list(arr.strides),
)


def ndarray_to_numpy(nda: Ndarray) -> numpy.ndarray:
arr: numpy.ndarray
if "datetime64" in nda.dtype:
# Backwards conversion: The data was stored as int64.
arr = numpy.ndarray(
buffer=nda.data,
shape=nda.shape,
dtype="int64",
strides=nda.strides,
).astype(nda.dtype)
else:
arr = numpy.ndarray(
buffer=nda.data,
shape=nda.shape,
dtype=numpy.dtype(nda.dtype),
strides=nda.strides,
)

mcbackend/test_npproto.py

diff --git a/mcbackend/test_npproto.py b/mcbackend/test_npproto.py
index fc88d29..fd88e7c 100644
--- a/mcbackend/test_npproto.py
+++ b/mcbackend/test_npproto.py
@@ -1,45 +1,48 @@
from datetime import datetime

import numpy
import pytest

from mcbackend import npproto
from mcbackend.npproto import utils


class TestUtils:
@pytest.mark.parametrize(
"arr",
[
numpy.arange(5),
numpy.random.uniform(size=(2, 3)),
numpy.array(5),
numpy.array(["hello", "world"]),
numpy.array([datetime(2020, 3, 4, 5, 6, 7, 8), datetime(2020, 3, 4, 5, 6, 7, 9)]),
numpy.array(
[datetime(2020, 3, 4, 5, 6, 7, 8), datetime(2020, 3, 4, 5, 6, 7, 9)],
dtype="datetime64",
),
numpy.array([(1, 2), (3, 2, 1)], dtype=object),
+ numpy.array([[1, 2, 3], [4, 5, 6]], order="C"),
+ numpy.array([[1, 2, 3], [4, 5, 6]], order="F"),
+ numpy.arange(12).reshape(3, 4)[::2, ::2],
],
)
def test_conversion(self, arr: numpy.ndarray):
nda = utils.ndarray_from_numpy(arr)
enc = bytes(nda)
dec = npproto.Ndarray().parse(enc)
assert isinstance(dec.data, bytes)
result = utils.ndarray_to_numpy(dec)
numpy.testing.assert_array_equal(result, arr)
pass

@pytest.mark.parametrize("shape", [(5,), (2, 3), (2, 3, 5), (5, 2, 1, 7)])
@pytest.mark.parametrize("order", "CF")
def test_byteorders(self, shape, order):
arr = numpy.arange(numpy.prod(shape)).reshape(shape, order=order)

nda = utils.ndarray_from_numpy(arr)
assert nda.order == "CF"[arr.flags.f_contiguous]

dec = utils.ndarray_to_numpy(nda)
numpy.testing.assert_array_equal(arr, dec)
pass

رسالة الإيداع (Commit message)

إصلاح ترميز مصفوفات NumPy غير المتجاورة

يقوم `bytes(arr.data)` دائمًا بإرجاع محتويات المخزن المؤقت بترتيب C، ولكن
يؤدي ترميز مصفوفة بترتيب Fortran (أو ترتيب آخر غير متجاور C) إلى
دورة ذهاب وإياب تالفة عبر `ndarray_from_numpy` و `ndarray_to_numpy`.
تصبح المصفوفة مشوشة عند تخزين خطواتها (strides) الأصلية.

استخدم `numpy.ascontiguousarray` لجعل المصفوفة متجاورة C قبل
استخراج الخطوات والبيانات. الآن يظل المخزن المؤقت والخطوات
متسقين.

يغلق #2124

ملاحظة: يضيف الإصدار `0.2.4` اختبارات ذهاب وإياب للمصفوفات بترتيب C وترتيب F والمصفوفات غير المتجاورة.

طلب السحب (Pull request)

ملخص

تؤدي الدورة الكاملة (round-tripping) لـ Protobuf لمصفوفات NumPy إلى إتلاف البيانات بصمت عندما لا تكون المصفوفة المصدر متجاورة C (على سبيل المثال، مرتبة Fortran). يضمن هذا الإصلاح أن تنجو جميع تخطيطات الذاكرة من دورة ترميز/فك ترميز.

arrF = numpy.array([[1, 2, 3], [4, 5, 6]], order="F")
numpy.testing.assert_array_equal(
arrF,
ndarray_to_numpy(ndarray_from_numpy(arrF)), # فشل قبل هذا الإصلاح
)

السبب الجذري

يخزن مسار الترميز في ndarray_from_numpy كلاً من المخزن المؤقت الخام وخطوات مصفوفة الإدخال. المشكلة هي عدم التطابق بين الاثنين:

  1. الترميز: يقوم bytes(arr.data) بنسخ المخزن المؤقت للذاكرة الأساسي. بالنسبة للمصفوفات غير المتجاورة C، تتم إعادة ترتيب هذه النسخة بصمت إلى ترتيب C.
  2. الخطوات المخزنة كما هي: يتم حفظ strides المصاحبة بتنسيقها الأصلي (غير C)، نظرًا لعدم تطبيق أي إعادة ترتيب عليها.
  3. فك الترميز: يقوم numpy.ndarray(buffer=..., strides=...) بإعادة بناء المصفوفة عن طريق تفسير المخزن المؤقت بترتيب C مع الخطوات الأصلية، وهو عدم تطابق ينتج عنه مصفوفة مشوشة.

الإصلاح

استدعاء numpy.ascontiguousarray قبل استخراج المخزن المؤقت والخطوات، بحيث يعكس كلاهما نفس تخطيط الذاكرة:

  • mcbackend/npproto/utils.py: اجعل المصفوفة متجاورة C قبل استخراج data و strides. تتطابق الخطوات المخزنة الآن دائمًا مع تخطيط المخزن المؤقت المخزن. المصفوفات التي هي بالفعل متجاورة C لا تتأثر.
  • mcbackend/test_npproto.py: إضافة حالات اختبار دورة كاملة صريحة للمصفوفات بترتيب C، وترتيب F، وغير المتجاورة (مقطوعة).

الاختبارات

تغطي مصفوفة الاختبار في TestUtils.test_conversion الآن:

  • numpy.array([[1, 2, 3], [4, 5, 6]], order="C")
  • numpy.array([[1, 2, 3], [4, 5, 6]], order="F")
  • numpy.arange(12).reshape(3, 4)[::2, ::2] (عرض غير متجاور)

تستمر جميع حالات dtype الحالية (int, float, str, datetime, object) في المرور دون تغيير.

ملاحظات

  • يرفع الإصدار إلى 0.2.4.
  • يظل ndarray_to_numpy دون تغيير؛ لا توجد تغييرات مطلوبة من جانب فك الترميز.

كيفية الاختبار

  1. قم بتشغيل pytest mcbackend/test_npproto.py. يجب أن تجتاز الحالات البارامترية الثلاث الجديدة (ترتيب C، ترتيب F، شريحة غير متجاورة) الاختبار.
  2. تحقق من أن جميع حالات dtype الموجودة مسبقًا (int, float, str, datetime, object) لا تزال تجتاز الاختبار دون تغيير.