إصلاح مصفوفة الترتيب 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 كلاً من المخزن المؤقت الخام وخطوات مصفوفة الإدخال. المشكلة هي عدم التطابق بين الاثنين:
- الترميز: يقوم
bytes(arr.data)بنسخ المخزن المؤقت للذاكرة الأساسي. بالنسبة للمصفوفات غير المتجاورة C، تتم إعادة ترتيب هذه النسخة بصمت إلى ترتيب C. - الخطوات المخزنة كما هي: يتم حفظ
stridesالمصاحبة بتنسيقها الأصلي (غير C)، نظرًا لعدم تطبيق أي إعادة ترتيب عليها. - فك الترميز: يقوم
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دون تغيير؛ لا توجد تغييرات مطلوبة من جانب فك الترميز.
كيفية الاختبار
- قم بتشغيل
pytest mcbackend/test_npproto.py. يجب أن تجتاز الحالات البارامترية الثلاث الجديدة (ترتيب C، ترتيب F، شريحة غير متجاورة) الاختبار. - تحقق من أن جميع حالات dtype الموجودة مسبقًا (int, float, str, datetime, object) لا تزال تجتاز الاختبار دون تغيير.