Source code for axisfuzzy.core.backend

#  Copyright (c) yibocat 2025 All Rights Reserved
#  Python: 3.12.7
#  Date: 2025/8/18 18:23
#  Author: yibow
#  Email: yibocat@yeah.net
#  Software: AxisFuzzy
"""
Abstract backend interface for Fuzzarray (Struct-of-Arrays).

This module defines the :class:`~.base.FuzzarrayBackend` abstract base class which is the
primary contract between high-level :class:`~/base.Fuzzarray` containers and concrete,
mtype-specific, NumPy-backed implementations.
"""

from abc import ABC, abstractmethod
from typing import Any, Tuple, Optional, Callable, TYPE_CHECKING

import numpy as np

from ..config import get_config

if TYPE_CHECKING:
    from .fuzznums import Fuzznum


[docs] class FuzzarrayBackend(ABC): """ Abstract base class for SoA (Struct-of-Arrays) fuzzy-number backends. The backend owns the NumPy arrays that store components for each element (e.g. membership / non-membership arrays depending on `mtype`) and exposes array-level operations used by higher-level containers. Parameters ---------- shape : tuple of int Logical shape of the array. q : int, optional Effective q-rung for q-rung fuzzy types. If None, uses :func:`axisfuzzy.config.get_config().DEFAULT_Q`. **kwargs Backend-specific options. Attributes ---------- shape : tuple of int Logical shape of the array. size : int Total number of elements (product of ``shape``). q : int Effective q-rung used by this backend instance. kwargs : dict Backend-specific extra parameters. mtype : str Backend-reported mtype; default read from configuration. Notes ----- - Concrete subclasses must implement array initialization, element views, slicing/copy semantics and formatting helpers. - Implementations should prefer views (shared memory) for slicing when possible to avoid unnecessary copies. Examples -------- Create a concrete backend subclass and instantiate it: .. code-block:: python class MyBackend(FuzzarrayBackend): def _initialize_arrays(self): self._a = np.zeros(self.shape, dtype=float) self._b = np.ones(self.shape, dtype=float) # implement other abstract methods... be = MyBackend((2, 3), q=2) print(be.shape, be.size) """ def __init__(self, shape: Tuple[int, ...], q: Optional[int] = None, **kwargs): self.shape = shape self.size = int(np.prod(shape)) self.q = q if q is not None else get_config().DEFAULT_Q self.kwargs = kwargs # 子类需要在这里初始化具体的 NumPy 数组 self._initialize_arrays() @abstractmethod def _initialize_arrays(self): """ Initialize underlying NumPy arrays used by the backend. Notes ----- - Must assign all arrays used by other methods (e.g., membership / non-membership arrays) as instance attributes. - Called from the base class constructor. Examples -------- .. code-block:: python def _initialize_arrays(self): # For a hypothetical 2-component mtype: self._comp1 = np.zeros(self.shape, dtype=float) self._comp2 = np.zeros(self.shape, dtype=float) """ pass
[docs] @abstractmethod def get_fuzznum_view(self, index: Any) -> 'Fuzznum': """ Return a lightweight Fuzznum view for the element at ``index``. The view should be a thin proxy that reads/writes directly into the backend arrays without performing a deep copy. Parameters ---------- index : int or tuple or slice Indexing key following NumPy semantics for single-element access. Returns ------- Fuzznum A Fuzznum-like proxy object. Notes ----- - Implementations may return a proxy class that reflects changes back into the underlying arrays. Examples -------- .. code-block:: python view = backend.get_fuzznum_view((0, 1)) view.membership = 0.7 # updates backend arrays in-place """ pass
[docs] @abstractmethod def set_fuzznum_data(self, index: Any, fuzznum: 'Fuzznum'): """ Assign values at ``index`` using a Fuzznum-like object. Parameters ---------- index : int or tuple or slice Target location to set. fuzznum : Fuzznum Source providing component values. Raises ------ IndexError If ``index`` is out of bounds. TypeError If ``fuzznum`` is incompatible with this backend's ``mtype``. Examples -------- .. code-block:: python backend.set_fuzznum_data(0, some_fuzznum) """ pass
[docs] @abstractmethod def copy(self) -> 'FuzzarrayBackend': """ Produce a deep copy of this backend and its arrays. Returns ------- FuzzarrayBackend New backend instance with duplicated arrays (no shared memory). Notes ----- - The returned instance must not share memory with the original. Examples -------- .. code-block:: python new_backend = backend.copy() assert new_backend is not backend """ pass
[docs] @abstractmethod def slice_view(self, key) -> 'FuzzarrayBackend': """ Return a backend representing a view/slice of this backend. Parameters ---------- key : slice, tuple of slices or other valid indexing key Indexing key describing the requested view. Returns ------- FuzzarrayBackend Backend representing the requested slice. Should share memory with the original wherever feasible. Notes ----- - Prefer returning a view that supports read/write semantics without unnecessary copies. Examples -------- .. code-block:: python view = backend.slice_view(np.s_[0:2, :]) """ pass
[docs] @staticmethod def from_arrays(*components, **kwargs) -> 'FuzzarrayBackend': """ Factory to construct a backend from raw component arrays. Parameters ---------- *components : array_like Component arrays representing the SoA layout for a specific ``mtype``. **kwargs Backend-specific keyword arguments (e.g., shape, q, dtype). Returns ------- FuzzarrayBackend New backend instance initialised from the component arrays. Notes ----- - Concrete subclasses should validate shapes and dtypes and may accept already-viewed arrays to avoid copies. - Subclasses MUST implement fuzzy constraint validation for the component arrays to ensure all elements satisfy the fuzzy number constraints specific to their mtype. Examples -------- .. code-block:: python be = ConcreteBackend.from_arrays(m_arr, n_arr) """ pass
[docs] def fill_from_values(self, *values: float): """ Broadcast provided component values to every element. Parameters ---------- *values : float Values to broadcast to each component across the whole backend. Notes ----- - Subclasses should validate the number of values against the expected number of components for the backend's ``mtype``. """ pass
[docs] def get_component_arrays(self) -> tuple: """ Return the underlying component arrays. In fact, it is the attribute component of a specific mtype. Returns ------- tuple Tuple of NumPy arrays that form the SoA backend. Notes ----- - The order and meaning of the returned arrays is backend-specific. """ pass
def __repr__(self): return f"{self.__class__.__name__}(shape={self.shape}, mtype='{self.mtype}')" @property def ndim(self) -> int: """ Number of dimensions of the logical Fuzzarray. Returns ------- int The number of dimensions (len(shape)). """ return len(self.shape) # ================== Metadata for Validation and Introspection ================== @property @abstractmethod def cmpnum(self) -> int: """ Return the number of component arrays expected by this backend. Returns ------- int The number of component arrays (e.g., 2 for q-ROFNs). """ pass @property @abstractmethod def cmpnames(self) -> Tuple[str, ...]: """ Return the names of the component arrays. Returns ------- Tuple[str, ...] A tuple of component names, e.g., ('md', 'nmd'). """ pass @property @abstractmethod def dtype(self) -> np.dtype: """ Return the expected numpy dtype for component arrays. Returns ------- np.dtype The expected data type, e.g., np.float64. """ pass # ================== Smart Display General Implementation ==================
[docs] def format_elements(self, format_spec: str = "") -> np.ndarray: """ Smart, truncated formatting for element display. For small arrays the entire content is formatted. For larger arrays a head/tail strategy is used and middle elements are replaced by ellipses. Parameters ---------- format_spec : str, optional Format specification passed from higher-level formatting calls. Returns ------- numpy.ndarray Array of formatted strings with the same logical shape. Examples -------- .. code-block:: python formatted = backend.format_elements('.3f') """ if self.size == 0: return np.array([], dtype=object).reshape(self.shape) config = get_config() if self.size <= config.DISPLAY_THRESHOLD_SMALL: return self._format_all_elements(format_spec) else: return self._format_partial_elements(format_spec)
def _format_all_elements(self, format_spec: str) -> np.ndarray: """Fully format all elements (for small datasets)""" # For small datasets, subclasses can optionally override this method for optimization # Default implementation: element-wise formatting result = np.empty(self.shape, dtype=object) formatter = self._get_element_formatter(format_spec) it = np.nditer(result, flags=['multi_index', 'refs_ok'], op_flags=['writeonly']) while not it.finished: idx = it.multi_index result[idx] = self._format_single_element(idx, formatter, format_spec) it.iternext() return result def _format_partial_elements(self, format_spec: str) -> np.ndarray: """ Partially format elements for large arrays. Only selected indices are formatted; other entries are set to '...'. """ # Calculate display parameters display_params = self._calculate_display_parameters() # Create result array, default filled with ellipses result = np.full(self.shape, '...', dtype=object) # Get indices to format indices_to_format = self._get_display_indices(display_params) # Only format the elements that need to be displayed formatter = self._get_element_formatter(format_spec) for idx in indices_to_format: result[idx] = self._format_single_element(idx, formatter, format_spec) return result def _calculate_display_parameters(self) -> dict: """ Compute display parameters based on size and dimension. Returns ------- dict Display parameters including 'edge_items', 'threshold', 'ndim', 'shape' and 'size'. """ config = get_config() if self.size < config.DISPLAY_THRESHOLD_MEDIUM: edge_items = config.DISPLAY_EDGE_ITEMS_MEDIUM threshold = config.DISPLAY_THRESHOLD_MEDIUM elif self.size < config.DISPLAY_THRESHOLD_LARGE: edge_items = config.DISPLAY_EDGE_ITEMS_LARGE threshold = config.DISPLAY_THRESHOLD_LARGE else: edge_items = config.DISPLAY_EDGE_ITEMS_HUGE threshold = config.DISPLAY_THRESHOLD_HUGE return { 'edge_items': edge_items, 'threshold': threshold, 'ndim': self.ndim, 'shape': self.shape, 'size': self.size } def _get_display_indices(self, params: dict) -> list: """ Determine which indices should be formatted for display. Parameters ---------- params : dict Display parameters returned by :meth:`_calculate_display_parameters`. Returns ------- list List of indices (tuples for ndim>1 or ints for 1D) to format. """ indices = [] edge_items = params['edge_items'] shape = params['shape'] if self.ndim == 1: if shape[0] <= 2 * edge_items + 1: indices = list(range(shape[0])) else: indices.extend(list(range(edge_items))) indices.extend(list(range(shape[0] - edge_items, shape[0]))) elif self.ndim == 2: # 2D arrays: display corner regions rows, cols = shape # Determine which rows and columns to display if rows <= 2 * edge_items + 1: row_indices = list(range(rows)) else: row_indices = (list(range(edge_items)) + list(range(rows - edge_items, rows))) if cols <= 2 * edge_items + 1: col_indices = list(range(cols)) else: col_indices = (list(range(edge_items)) + list(range(cols - edge_items, cols))) # Generate all required (row, col) combinations for r in row_indices: for c in col_indices: indices.append((r, c)) else: # High-dimensional array: Recursive processing (simplified implementation) indices = self._get_high_dim_indices(shape, edge_items) return indices def _get_high_dim_indices(self, shape: tuple, edge_items: int) -> list: """Generate indices to display for high-dimensional arrays.""" indices = [] def generate_edge_indices(current_shape, current_idx=None): if current_idx is None: current_idx = [] if not current_shape: indices.append(tuple(current_idx)) return dim_size = current_shape[0] remaining_shape = current_shape[1:] if dim_size <= 2 * edge_items + 1: for i in range(dim_size): generate_edge_indices(remaining_shape, current_idx + [i]) else: for i in range(edge_items): generate_edge_indices(remaining_shape, current_idx + [i]) for i in range(dim_size - edge_items, dim_size): generate_edge_indices(remaining_shape, current_idx + [i]) generate_edge_indices(shape) return indices @abstractmethod def _get_element_formatter(self, format_spec: str) -> Callable: """ Return a callable that formats a single element. The returned callable is used by formatting helpers and should accept the minimal information required to produce a string for a single element at a given index. Parameters ---------- format_spec : str Format specification forwarded from higher layer. Returns ------- Callable A callable used to format an element. Examples -------- .. code-block:: python def formatter(idx, *args, **kwargs): return "<fuzznum>" return formatter """ pass @abstractmethod def _format_single_element(self, index: Any, formatter: Callable, format_spec: str) -> str: """ Format a single element located at ``index``. Parameters ---------- index : int or tuple Index of the element. formatter : Callable Callable returned by :meth:`_get_element_formatter`. format_spec : str Format specification. Returns ------- str Formatted string for the element. Notes ----- - Implementations decide how to extract component values and present them (e.g. as "(m,n)" or other textual form). Examples -------- .. code-block:: python return formatter(index, self._comp1[index], self._comp2[index]) """ pass