vcti-array-display 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vcti/arraydisplay/__init__.py +19 -0
- vcti/arraydisplay/_exclusion.py +60 -0
- vcti/arraydisplay/_flattening.py +46 -0
- vcti/arraydisplay/_formatting.py +45 -0
- vcti/arraydisplay/array_display.py +624 -0
- vcti/arraydisplay/array_display.pyi +92 -0
- vcti/arraydisplay/py.typed +0 -0
- vcti_array_display-1.1.0.dist-info/METADATA +222 -0
- vcti_array_display-1.1.0.dist-info/RECORD +13 -0
- vcti_array_display-1.1.0.dist-info/WHEEL +5 -0
- vcti_array_display-1.1.0.dist-info/licenses/LICENSE +8 -0
- vcti_array_display-1.1.0.dist-info/top_level.txt +1 -0
- vcti_array_display-1.1.0.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""vcti.arraydisplay — Presentation layer for NumPy structured arrays."""
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
|
|
7
|
+
from ._exclusion import FILLER_COLUMNS, LENGTH_COLUMNS, VOID_COLUMNS, ColumnExclusion
|
|
8
|
+
from .array_display import ArrayDisplay
|
|
9
|
+
|
|
10
|
+
__version__ = version("vcti-array-display")
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"__version__",
|
|
14
|
+
"ArrayDisplay",
|
|
15
|
+
"ColumnExclusion",
|
|
16
|
+
"FILLER_COLUMNS",
|
|
17
|
+
"LENGTH_COLUMNS",
|
|
18
|
+
"VOID_COLUMNS",
|
|
19
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Column exclusion rules and pre-built patterns.
|
|
4
|
+
|
|
5
|
+
Provides the generic exclusion mechanism used by ArrayDisplay to filter
|
|
6
|
+
columns by name, regex, or a callable predicate that receives both the
|
|
7
|
+
column name and its dtype.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
ColumnExclusion = str | re.Pattern[str] | Callable[[str, np.dtype], bool]
|
|
18
|
+
"""A single exclusion rule:
|
|
19
|
+
|
|
20
|
+
- ``str`` — exact column name match.
|
|
21
|
+
- ``re.Pattern`` — regex matched against the column name.
|
|
22
|
+
- ``callable(name: str, dtype: np.dtype) -> bool`` — predicate receiving
|
|
23
|
+
the column name *and* its dtype, for filtering based on type, shape,
|
|
24
|
+
or any combination.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
FILLER_COLUMNS: re.Pattern[str] = re.compile(r"^f\d+$")
|
|
28
|
+
"""Matches C++ memory-alignment filler fields by name (f0, f3, f123, …)."""
|
|
29
|
+
|
|
30
|
+
LENGTH_COLUMNS: re.Pattern[str] = re.compile(r"_len$")
|
|
31
|
+
"""Matches internal string-length columns by name (label_len, name_len, …)."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def VOID_COLUMNS(name: str, dtype: np.dtype) -> bool:
|
|
35
|
+
"""Exclude all void-dtype fields (C++ alignment padding by dtype)."""
|
|
36
|
+
return dtype.kind == "V"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_excluded(name: str, dtype: np.dtype, rules: Sequence[ColumnExclusion]) -> bool:
|
|
40
|
+
"""Test whether a column name/dtype matches any exclusion rule.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
name: Column name to test.
|
|
44
|
+
dtype: Column's dtype.
|
|
45
|
+
rules: Sequence of exclusion rules.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if the column should be excluded.
|
|
49
|
+
"""
|
|
50
|
+
for rule in rules:
|
|
51
|
+
if isinstance(rule, str):
|
|
52
|
+
if name == rule:
|
|
53
|
+
return True
|
|
54
|
+
elif isinstance(rule, re.Pattern):
|
|
55
|
+
if rule.search(name) is not None:
|
|
56
|
+
return True
|
|
57
|
+
elif callable(rule):
|
|
58
|
+
if rule(name, dtype):
|
|
59
|
+
return True
|
|
60
|
+
return False
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Dtype flattening helpers.
|
|
4
|
+
|
|
5
|
+
Maps original multi-component fields to their flattened scalar column names
|
|
6
|
+
after ``flatten_record_dtype`` has expanded vector/matrix fields.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_field_components(
|
|
17
|
+
original_dtype: np.dtype,
|
|
18
|
+
flattened_dtype: np.dtype,
|
|
19
|
+
) -> dict[str, list[str]]:
|
|
20
|
+
"""Map original multi-component fields to their flattened column names.
|
|
21
|
+
|
|
22
|
+
Walks both dtypes positionally: scalar fields consume one slot,
|
|
23
|
+
vector/matrix fields consume ``prod(shape)`` slots.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
original_dtype: Dtype before flattening.
|
|
27
|
+
flattened_dtype: Dtype after flattening.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict mapping original field name to list of flattened column names.
|
|
31
|
+
Only includes fields that were actually expanded (shape != ()).
|
|
32
|
+
"""
|
|
33
|
+
if flattened_dtype.names is None:
|
|
34
|
+
raise TypeError("flattened_dtype must be a structured dtype with named fields")
|
|
35
|
+
if original_dtype.names is None:
|
|
36
|
+
raise TypeError("original_dtype must be a structured dtype with named fields")
|
|
37
|
+
flat_names = list(flattened_dtype.names)
|
|
38
|
+
result: dict[str, list[str]] = {}
|
|
39
|
+
idx = 0
|
|
40
|
+
for name in original_dtype.names:
|
|
41
|
+
shape = original_dtype[name].shape
|
|
42
|
+
n_components = max(math.prod(shape), 1)
|
|
43
|
+
if n_components > 1:
|
|
44
|
+
result[name] = flat_names[idx : idx + n_components]
|
|
45
|
+
idx += n_components
|
|
46
|
+
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Value formatting and pandas import helpers for adapters."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import types
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def format_value(value: Any) -> str:
|
|
14
|
+
"""Format a single array value for text display.
|
|
15
|
+
|
|
16
|
+
Handles floats (6 significant digits), integers, bytes, and strings.
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(value, (np.floating, float)):
|
|
19
|
+
if np.isnan(value):
|
|
20
|
+
return "NaN"
|
|
21
|
+
return f"{value:.6g}"
|
|
22
|
+
if isinstance(value, (np.integer, int)):
|
|
23
|
+
return str(int(value))
|
|
24
|
+
if isinstance(value, (bytes, np.bytes_)):
|
|
25
|
+
return value.decode(errors="replace")
|
|
26
|
+
return str(value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def import_pandas() -> types.ModuleType:
|
|
30
|
+
"""Import pandas or raise a helpful error.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The pandas module.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ImportError: If pandas is not installed.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
import pandas as pd
|
|
40
|
+
except ImportError:
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"pandas is required for this method. "
|
|
43
|
+
"Install with: pip install vcti-array-display[pandas]"
|
|
44
|
+
) from None
|
|
45
|
+
return pd
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""ArrayDisplay — presentation layer for NumPy structured arrays.
|
|
4
|
+
|
|
5
|
+
Provides five capabilities over raw structured arrays:
|
|
6
|
+
|
|
7
|
+
1. **Column filtering** — hide columns by name, regex, or predicate.
|
|
8
|
+
2. **Dtype flattening** — expand vector/matrix fields into scalar columns
|
|
9
|
+
with user-controllable component names.
|
|
10
|
+
3. **Enum mapping** — lazily map integer ID columns to human-readable names.
|
|
11
|
+
4. **Array slicing** — create lightweight index arrays for bounded access.
|
|
12
|
+
5. **Adapters** — render as text table or pandas DataFrame on bounded slices.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import copy
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from vcti.nputils import flatten_record_dtype, join_struct_arrays
|
|
24
|
+
|
|
25
|
+
from ._exclusion import ColumnExclusion, is_excluded
|
|
26
|
+
from ._flattening import build_field_components
|
|
27
|
+
from ._formatting import format_value, import_pandas
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ArrayDisplay:
|
|
31
|
+
"""Presentation layer for NumPy structured arrays.
|
|
32
|
+
|
|
33
|
+
Wraps a structured array reference (no copy) and records how it should
|
|
34
|
+
be presented:
|
|
35
|
+
|
|
36
|
+
1. **Column filtering** — ``exclude_columns`` hides fields by name,
|
|
37
|
+
regex, or callable. Pre-built patterns ``FILLER_COLUMNS`` and
|
|
38
|
+
``LENGTH_COLUMNS`` cover common C++ interop noise.
|
|
39
|
+
2. **Dtype flattening** — ``flatten_dtype`` expands vector/matrix fields
|
|
40
|
+
into scalar columns. ``set_component_names()`` lets you replace
|
|
41
|
+
the default numeric suffixes (``_0``, ``_1``) with meaningful labels
|
|
42
|
+
(``_x``, ``_y``, ``_z``).
|
|
43
|
+
3. **Enum mapping** — ``add_name_columns()`` stores a recipe mapping
|
|
44
|
+
integer IDs to strings. Resolution is lazy — applied only when
|
|
45
|
+
a bounded slice is presented.
|
|
46
|
+
4. **Array slicing** — ``to_slice()`` returns a lightweight index array
|
|
47
|
+
for bounded access without copying the full data.
|
|
48
|
+
5. **Adapters** — ``to_table()`` (pure numpy) and ``to_dataframe()``
|
|
49
|
+
(optional pandas) render bounded slices with all mappings applied.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
array: The underlying structured array (reference, not copy).
|
|
53
|
+
original_dtype: Dtype before any flattening.
|
|
54
|
+
view_columns: Ordered list of user-visible column names (read-only copy).
|
|
55
|
+
name_columns: Enum mapping recipes (read-only view).
|
|
56
|
+
field_components: Flattened field grouping (read-only view).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
src_obj: np.ndarray | None = None,
|
|
62
|
+
column_list: list[str] | None = None,
|
|
63
|
+
flatten_dtype: bool = False,
|
|
64
|
+
exclude_columns: Sequence[ColumnExclusion] | None = None,
|
|
65
|
+
component_names: dict[str, list[str]] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Initialize an ArrayDisplay.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
src_obj: NumPy structured array, or None for empty.
|
|
71
|
+
column_list: Explicit columns to show. If None, all columns
|
|
72
|
+
minus those matching *exclude_columns*.
|
|
73
|
+
flatten_dtype: Expand vector/matrix fields into scalar columns.
|
|
74
|
+
exclude_columns: Rules for hiding columns — each element is a
|
|
75
|
+
``str`` (exact), ``re.Pattern`` (regex), or callable.
|
|
76
|
+
component_names: Custom suffixes for flattened fields, e.g.
|
|
77
|
+
``{'position': ['x', 'y', 'z']}`` renames ``position_0``
|
|
78
|
+
through ``position_2`` to ``position_x`` through ``position_z``.
|
|
79
|
+
Only meaningful when *flatten_dtype* is True.
|
|
80
|
+
"""
|
|
81
|
+
self.array: np.ndarray | None = None
|
|
82
|
+
self.original_dtype: np.dtype | None = None
|
|
83
|
+
self._view_columns: list[str] = []
|
|
84
|
+
self._name_columns: dict[str, tuple[str, dict[int, str]]] = {}
|
|
85
|
+
self._field_components: dict[str, list[str]] = {}
|
|
86
|
+
# Maps display names to actual dtype column names, used when
|
|
87
|
+
# set_component_names() renames flattened columns. Survives
|
|
88
|
+
# multiple renames (e.g. x→lat) by always tracking back to
|
|
89
|
+
# the original dtype column.
|
|
90
|
+
self._column_aliases: dict[str, str] = {}
|
|
91
|
+
|
|
92
|
+
if isinstance(src_obj, np.ndarray):
|
|
93
|
+
self.set_array(src_obj, column_list, flatten_dtype, exclude_columns, component_names)
|
|
94
|
+
elif src_obj is not None:
|
|
95
|
+
raise TypeError(f"Expected np.ndarray or None, got {type(src_obj).__name__}")
|
|
96
|
+
|
|
97
|
+
# -- Read-only properties ---------------------------------------------------
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def view_columns(self) -> list[str]:
|
|
101
|
+
"""Ordered list of user-visible column names (defensive copy)."""
|
|
102
|
+
return list(self._view_columns)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def name_columns(self) -> MappingProxyType[str, tuple[str, dict[int, str]]]:
|
|
106
|
+
"""Enum mapping recipes ``{name: (id_col, {int: str})}`` (read-only view)."""
|
|
107
|
+
return MappingProxyType(self._name_columns)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def field_components(self) -> MappingProxyType[str, list[str]]:
|
|
111
|
+
"""Flattened field grouping ``{field: [col, ...]}`` (read-only view)."""
|
|
112
|
+
return MappingProxyType(self._field_components)
|
|
113
|
+
|
|
114
|
+
# -- 1. Column filtering ----------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def set_view_columns(
|
|
117
|
+
self,
|
|
118
|
+
column_list: list[str] | None = None,
|
|
119
|
+
exclude_columns: Sequence[ColumnExclusion] | None = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Configure which columns are visible.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
column_list: Explicit columns. If None, use all array columns
|
|
125
|
+
minus those matching *exclude_columns*.
|
|
126
|
+
exclude_columns: Rules for hiding columns.
|
|
127
|
+
"""
|
|
128
|
+
if self.array is None:
|
|
129
|
+
self._view_columns = []
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if column_list is not None:
|
|
133
|
+
self._view_columns = list(column_list)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if self.array.dtype.names is None:
|
|
137
|
+
raise TypeError("Array has no named fields")
|
|
138
|
+
cols = list(self.array.dtype.names)
|
|
139
|
+
|
|
140
|
+
if exclude_columns:
|
|
141
|
+
dt = self.array.dtype
|
|
142
|
+
cols = [c for c in cols if not is_excluded(c, dt[c], exclude_columns)]
|
|
143
|
+
|
|
144
|
+
self._view_columns = cols
|
|
145
|
+
|
|
146
|
+
def include_view_columns(self, column_list: list[str]) -> None:
|
|
147
|
+
"""Add columns to the current view."""
|
|
148
|
+
self._view_columns.extend(column_list)
|
|
149
|
+
|
|
150
|
+
def exclude_view_columns(self, column_list: list[str]) -> None:
|
|
151
|
+
"""Remove columns from the current view."""
|
|
152
|
+
exclude = set(column_list)
|
|
153
|
+
self._view_columns = [c for c in self._view_columns if c not in exclude]
|
|
154
|
+
|
|
155
|
+
def replace_view_columns(self, mapping: dict[str, str]) -> None:
|
|
156
|
+
"""Replace column names in the view.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
mapping: Dict mapping old_name -> new_name.
|
|
160
|
+
"""
|
|
161
|
+
for i, col in enumerate(self._view_columns):
|
|
162
|
+
if col in mapping:
|
|
163
|
+
self._view_columns[i] = mapping[col]
|
|
164
|
+
|
|
165
|
+
# -- 2. Dtype flattening ----------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def set_array(
|
|
168
|
+
self,
|
|
169
|
+
src_array: np.ndarray,
|
|
170
|
+
column_list: list[str] | None = None,
|
|
171
|
+
flatten_dtype: bool = False,
|
|
172
|
+
exclude_columns: Sequence[ColumnExclusion] | None = None,
|
|
173
|
+
component_names: dict[str, list[str]] | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Set or replace the internal array.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
src_array: NumPy structured array with named fields.
|
|
179
|
+
column_list: Explicit columns to show.
|
|
180
|
+
flatten_dtype: Expand vector/matrix fields into scalar columns.
|
|
181
|
+
exclude_columns: Rules for hiding columns.
|
|
182
|
+
component_names: Custom suffixes for flattened fields.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
TypeError: If *src_array* is not a structured array.
|
|
186
|
+
"""
|
|
187
|
+
if src_array.dtype.names is None:
|
|
188
|
+
raise TypeError(
|
|
189
|
+
f"Expected a structured array with named fields, got dtype {src_array.dtype}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self.original_dtype = src_array.dtype
|
|
193
|
+
self._field_components = {}
|
|
194
|
+
self._name_columns = {}
|
|
195
|
+
self._column_aliases = {}
|
|
196
|
+
|
|
197
|
+
if flatten_dtype and src_array.ndim == 1:
|
|
198
|
+
flattened_dt, _ = flatten_record_dtype(src_array.dtype)
|
|
199
|
+
self.array = src_array.view(flattened_dt)
|
|
200
|
+
self._field_components = build_field_components(src_array.dtype, flattened_dt)
|
|
201
|
+
else:
|
|
202
|
+
self.array = src_array
|
|
203
|
+
|
|
204
|
+
self.set_view_columns(column_list, exclude_columns)
|
|
205
|
+
|
|
206
|
+
if component_names:
|
|
207
|
+
for field, names in component_names.items():
|
|
208
|
+
self.set_component_names(field, names)
|
|
209
|
+
|
|
210
|
+
def set_component_names(self, field: str, names: list[str]) -> None:
|
|
211
|
+
"""Rename flattened component columns for a multi-component field.
|
|
212
|
+
|
|
213
|
+
After dtype flattening, a field like ``position(3,)`` becomes
|
|
214
|
+
``position_0, position_1, position_2``. This method renames them
|
|
215
|
+
to ``position_x, position_y, position_z`` (given ``names=['x','y','z']``).
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
field: Original field name (before flattening).
|
|
219
|
+
names: New suffixes — one per component.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
ValueError: If *field* is not in ``field_components`` or the
|
|
223
|
+
number of names doesn't match the component count.
|
|
224
|
+
"""
|
|
225
|
+
if field not in self._field_components:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Field '{field}' has no flattened components. "
|
|
228
|
+
f"Available: {list(self._field_components)}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
current = self._field_components[field]
|
|
232
|
+
if len(names) != len(current):
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Field '{field}' has {len(current)} components, got {len(names)} names"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
rename = {}
|
|
238
|
+
new_cols = []
|
|
239
|
+
for old_col, suffix in zip(current, names):
|
|
240
|
+
new_col = f"{field}_{suffix}"
|
|
241
|
+
rename[old_col] = new_col
|
|
242
|
+
new_cols.append(new_col)
|
|
243
|
+
# Track the actual dtype column name for data access
|
|
244
|
+
dtype_col = self._column_aliases.get(old_col, old_col)
|
|
245
|
+
self._column_aliases[new_col] = dtype_col
|
|
246
|
+
self._column_aliases.pop(old_col, None)
|
|
247
|
+
|
|
248
|
+
self.replace_view_columns(rename)
|
|
249
|
+
self._field_components[field] = new_cols
|
|
250
|
+
|
|
251
|
+
# -- 3. Enum mapping --------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def add_name_columns(self, name_columns: dict[str, tuple[str, dict[int, str]]]) -> None:
|
|
254
|
+
"""Register enum ID -> name mappings (lazy — resolved at presentation).
|
|
255
|
+
|
|
256
|
+
Stores a recipe for each mapping. The actual resolution happens
|
|
257
|
+
only when ``to_table()`` or ``to_dataframe()`` processes a bounded
|
|
258
|
+
slice. Automatically replaces ID columns with name columns in
|
|
259
|
+
``view_columns``.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
name_columns: ``{name_column: (id_column, {int: str})}``.
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
>>> view.add_name_columns({
|
|
266
|
+
... 'element_type_name': ('element_type', {1: 'QUAD', 2: 'HEX'}),
|
|
267
|
+
... })
|
|
268
|
+
"""
|
|
269
|
+
self._name_columns.update(name_columns)
|
|
270
|
+
replacement_map = {v[0]: k for k, v in name_columns.items()}
|
|
271
|
+
self.replace_view_columns(replacement_map)
|
|
272
|
+
|
|
273
|
+
def _resolve_column_values(self, col: str, indices: np.ndarray) -> np.ndarray:
|
|
274
|
+
"""Resolve column values for a bounded set of row indices.
|
|
275
|
+
|
|
276
|
+
Regular columns return values directly. Name columns map integer
|
|
277
|
+
IDs to strings via the stored recipe. Uses a plain list
|
|
278
|
+
comprehension — faster than ``np.vectorize`` for the small slices
|
|
279
|
+
that presentation methods operate on.
|
|
280
|
+
"""
|
|
281
|
+
if self.array is None:
|
|
282
|
+
raise ValueError("No array set")
|
|
283
|
+
if col in self._name_columns:
|
|
284
|
+
id_col, mapping = self._name_columns[col]
|
|
285
|
+
id_values = self.array[id_col][indices]
|
|
286
|
+
return np.array([mapping.get(int(x), str(x)) for x in id_values])
|
|
287
|
+
dtype_col = self._column_aliases.get(col, col)
|
|
288
|
+
return self.array[dtype_col][indices]
|
|
289
|
+
|
|
290
|
+
# -- 4. Array slicing -------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
def to_slice(
|
|
293
|
+
self,
|
|
294
|
+
*,
|
|
295
|
+
head: int | None = None,
|
|
296
|
+
tail: int | None = None,
|
|
297
|
+
mask: np.ndarray | None = None,
|
|
298
|
+
indices: np.ndarray | None = None,
|
|
299
|
+
) -> np.ndarray:
|
|
300
|
+
"""Create an index array for bounded access.
|
|
301
|
+
|
|
302
|
+
Returns a lightweight integer index array suitable for fancy
|
|
303
|
+
indexing: ``view.array[view.to_slice(head=20)]``.
|
|
304
|
+
|
|
305
|
+
Exactly one slicing strategy should be provided. Priority when
|
|
306
|
+
multiple are given: *indices* > *mask* > *head*/*tail*.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
head: Number of rows from the start.
|
|
310
|
+
tail: Number of rows from the end.
|
|
311
|
+
mask: Boolean mask array.
|
|
312
|
+
indices: Explicit integer index array (returned as-is).
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Integer index array (dtype ``np.intp``).
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
ValueError: If no array is set, or if conflicting parameters
|
|
319
|
+
are provided (e.g. *mask* with *head*/*tail*).
|
|
320
|
+
"""
|
|
321
|
+
if self.array is None:
|
|
322
|
+
raise ValueError("No array set")
|
|
323
|
+
|
|
324
|
+
n = len(self.array)
|
|
325
|
+
|
|
326
|
+
# Validate parameter combinations
|
|
327
|
+
has_head_tail = head is not None or tail is not None
|
|
328
|
+
if indices is not None and (mask is not None or has_head_tail):
|
|
329
|
+
raise ValueError("Cannot combine 'indices' with other slicing parameters")
|
|
330
|
+
if mask is not None and has_head_tail:
|
|
331
|
+
raise ValueError("Cannot combine 'mask' with 'head'/'tail'")
|
|
332
|
+
|
|
333
|
+
if indices is not None:
|
|
334
|
+
return indices
|
|
335
|
+
|
|
336
|
+
if mask is not None:
|
|
337
|
+
return np.where(mask)[0]
|
|
338
|
+
|
|
339
|
+
if head is not None and tail is not None:
|
|
340
|
+
h = np.arange(min(head, n))
|
|
341
|
+
t = np.arange(max(0, n - tail), n)
|
|
342
|
+
return np.unique(np.concatenate([h, t]))
|
|
343
|
+
|
|
344
|
+
if head is not None:
|
|
345
|
+
return np.arange(min(head, n))
|
|
346
|
+
|
|
347
|
+
if tail is not None:
|
|
348
|
+
return np.arange(max(0, n - tail), n)
|
|
349
|
+
|
|
350
|
+
return np.arange(n)
|
|
351
|
+
|
|
352
|
+
# -- 5. Adapters ------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
def to_table(
|
|
355
|
+
self,
|
|
356
|
+
*,
|
|
357
|
+
head: int = 20,
|
|
358
|
+
tail: int = 5,
|
|
359
|
+
max_col_width: int = 30,
|
|
360
|
+
indices: np.ndarray | None = None,
|
|
361
|
+
) -> str:
|
|
362
|
+
"""Render a text table from a bounded slice (pure numpy).
|
|
363
|
+
|
|
364
|
+
Resolves enum name columns and formats the output as an aligned
|
|
365
|
+
plain-text table. Only the requested rows are processed.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
head: Rows from the start (ignored if *indices* given).
|
|
369
|
+
tail: Rows from the end (ignored if *indices* given).
|
|
370
|
+
max_col_width: Maximum column display width in characters.
|
|
371
|
+
indices: Explicit row indices (overrides head/tail).
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Formatted text table string.
|
|
375
|
+
"""
|
|
376
|
+
if self.array is None or not self._view_columns:
|
|
377
|
+
return repr(self)
|
|
378
|
+
|
|
379
|
+
n = len(self.array)
|
|
380
|
+
cols = self._view_columns
|
|
381
|
+
|
|
382
|
+
# Determine row blocks and whether to show an ellipsis separator
|
|
383
|
+
if indices is not None:
|
|
384
|
+
blocks: list[np.ndarray | None] = [indices]
|
|
385
|
+
elif n > head + tail:
|
|
386
|
+
blocks = [
|
|
387
|
+
np.arange(min(head, n)),
|
|
388
|
+
None, # ellipsis marker
|
|
389
|
+
np.arange(max(0, n - tail), n),
|
|
390
|
+
]
|
|
391
|
+
else:
|
|
392
|
+
blocks = [np.arange(n)]
|
|
393
|
+
|
|
394
|
+
# Resolve column values per block and convert to strings
|
|
395
|
+
str_rows: list[list[str] | None] = []
|
|
396
|
+
for block in blocks:
|
|
397
|
+
if block is None:
|
|
398
|
+
str_rows.append(None)
|
|
399
|
+
continue
|
|
400
|
+
col_values = {col: self._resolve_column_values(col, block) for col in cols}
|
|
401
|
+
for i in range(len(block)):
|
|
402
|
+
str_rows.append([format_value(col_values[col][i]) for col in cols])
|
|
403
|
+
|
|
404
|
+
# Calculate column widths
|
|
405
|
+
col_widths: list[int] = []
|
|
406
|
+
for ci, col in enumerate(cols):
|
|
407
|
+
max_val = max(
|
|
408
|
+
(len(row[ci]) for row in str_rows if row is not None),
|
|
409
|
+
default=0,
|
|
410
|
+
)
|
|
411
|
+
col_widths.append(min(max(len(col), max_val), max_col_width))
|
|
412
|
+
|
|
413
|
+
# Determine numeric alignment per column
|
|
414
|
+
dt = self.array.dtype
|
|
415
|
+
dtype_names = set(dt.names or ())
|
|
416
|
+
|
|
417
|
+
def _is_numeric(col: str) -> bool:
|
|
418
|
+
if col in self._name_columns:
|
|
419
|
+
return False
|
|
420
|
+
actual = self._column_aliases.get(col, col)
|
|
421
|
+
return actual in dtype_names and np.issubdtype(dt[actual], np.number)
|
|
422
|
+
|
|
423
|
+
col_numeric = [_is_numeric(col) for col in cols]
|
|
424
|
+
|
|
425
|
+
def fmt(val: str, ci: int) -> str:
|
|
426
|
+
w = col_widths[ci]
|
|
427
|
+
if len(val) > w:
|
|
428
|
+
val = val[: w - 1] + "\u2026"
|
|
429
|
+
return val.rjust(w) if col_numeric[ci] else val.ljust(w)
|
|
430
|
+
|
|
431
|
+
sep = " | "
|
|
432
|
+
lines: list[str] = []
|
|
433
|
+
|
|
434
|
+
# Header
|
|
435
|
+
lines.append(
|
|
436
|
+
sep.join(col.ljust(col_widths[ci])[: col_widths[ci]] for ci, col in enumerate(cols))
|
|
437
|
+
)
|
|
438
|
+
lines.append("-+-".join("-" * w for w in col_widths))
|
|
439
|
+
|
|
440
|
+
# Data rows
|
|
441
|
+
for row in str_rows:
|
|
442
|
+
if row is None:
|
|
443
|
+
lines.append(sep.join("...".center(col_widths[ci]) for ci in range(len(cols))))
|
|
444
|
+
else:
|
|
445
|
+
lines.append(sep.join(fmt(row[ci], ci) for ci in range(len(cols))))
|
|
446
|
+
|
|
447
|
+
lines.append(f"\n[{n} rows x {len(cols)} columns]")
|
|
448
|
+
return "\n".join(lines)
|
|
449
|
+
|
|
450
|
+
def to_dataframe(
|
|
451
|
+
self,
|
|
452
|
+
*,
|
|
453
|
+
indices: np.ndarray | None = None,
|
|
454
|
+
head: int | None = None,
|
|
455
|
+
tail: int | None = None,
|
|
456
|
+
resolve_names: bool = True,
|
|
457
|
+
) -> Any:
|
|
458
|
+
"""Convert a bounded slice to a pandas DataFrame.
|
|
459
|
+
|
|
460
|
+
Enum name columns are materialized using ``pd.Categorical`` for
|
|
461
|
+
memory efficiency.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
indices: Explicit row indices.
|
|
465
|
+
head: Rows from the start.
|
|
466
|
+
tail: Rows from the end.
|
|
467
|
+
resolve_names: If True, materialize enum name columns.
|
|
468
|
+
If False, show raw ID columns instead.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
pandas DataFrame with the configured view columns.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
ImportError: If pandas is not installed.
|
|
475
|
+
"""
|
|
476
|
+
pd = import_pandas()
|
|
477
|
+
|
|
478
|
+
if self.array is None:
|
|
479
|
+
return pd.DataFrame()
|
|
480
|
+
|
|
481
|
+
if indices is None and (head is not None or tail is not None):
|
|
482
|
+
indices = self.to_slice(head=head, tail=tail)
|
|
483
|
+
|
|
484
|
+
arr = self.array if indices is None else self.array[indices]
|
|
485
|
+
df = pd.DataFrame(arr)
|
|
486
|
+
|
|
487
|
+
cols = self._view_columns
|
|
488
|
+
|
|
489
|
+
# Rename aliased columns (from set_component_names) in the DataFrame
|
|
490
|
+
if self._column_aliases:
|
|
491
|
+
reverse = {v: k for k, v in self._column_aliases.items() if k in cols}
|
|
492
|
+
if reverse:
|
|
493
|
+
df = df.rename(columns=reverse)
|
|
494
|
+
|
|
495
|
+
if resolve_names:
|
|
496
|
+
for name_col, (id_col, mapping) in self._name_columns.items():
|
|
497
|
+
if name_col in cols:
|
|
498
|
+
df[name_col] = pd.Categorical(df[id_col].map(mapping))
|
|
499
|
+
return df[cols]
|
|
500
|
+
|
|
501
|
+
# resolve_names=False: swap name columns back to source ID columns
|
|
502
|
+
select = [self._name_columns[c][0] if c in self._name_columns else c for c in cols]
|
|
503
|
+
return df[select]
|
|
504
|
+
|
|
505
|
+
def _repr_html_(self) -> str | None:
|
|
506
|
+
"""Rich HTML display for Jupyter notebooks."""
|
|
507
|
+
if self.array is None or not self._view_columns:
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
pd = import_pandas()
|
|
512
|
+
except ImportError:
|
|
513
|
+
return f"<pre>{self.to_table()}</pre>"
|
|
514
|
+
|
|
515
|
+
n = len(self.array)
|
|
516
|
+
max_rows = pd.get_option("display.max_rows")
|
|
517
|
+
if max_rows is None:
|
|
518
|
+
max_rows = 60
|
|
519
|
+
|
|
520
|
+
if n <= max_rows:
|
|
521
|
+
df = self.to_dataframe()
|
|
522
|
+
else:
|
|
523
|
+
half = max_rows // 2
|
|
524
|
+
head_df = self.to_dataframe(head=half)
|
|
525
|
+
tail_df = self.to_dataframe(tail=half)
|
|
526
|
+
ellipsis_data = {col: ["\u22ee"] for col in self._view_columns}
|
|
527
|
+
ellipsis_df = pd.DataFrame(ellipsis_data)
|
|
528
|
+
df = pd.concat([head_df, ellipsis_df, tail_df], ignore_index=True)
|
|
529
|
+
|
|
530
|
+
html = df.to_html(index=False, na_rep="NaN")
|
|
531
|
+
return f"{html}\n<p>{n} rows \u00d7 {len(self._view_columns)} columns</p>"
|
|
532
|
+
|
|
533
|
+
# -- Misc -------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
def append_columns(
|
|
536
|
+
self, columns: list[np.ndarray] | tuple[np.ndarray, ...] | np.ndarray
|
|
537
|
+
) -> None:
|
|
538
|
+
"""Append additional structured columns to the array.
|
|
539
|
+
|
|
540
|
+
Joins the existing array with new structured column(s) using
|
|
541
|
+
``vcti.nputils.join_struct_arrays``. The resulting dtype is the
|
|
542
|
+
union of all field names. Field names must not overlap — duplicate
|
|
543
|
+
names will raise an error from the underlying join function.
|
|
544
|
+
|
|
545
|
+
The new columns are added to the underlying array but **not**
|
|
546
|
+
automatically added to ``view_columns``. Call
|
|
547
|
+
``include_view_columns()`` afterwards if they should be visible.
|
|
548
|
+
|
|
549
|
+
Can be called on an empty ArrayDisplay (no array set) — the
|
|
550
|
+
provided columns become the array.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
columns: Structured array(s) to join. All arrays must have
|
|
554
|
+
the same number of rows as the existing array.
|
|
555
|
+
"""
|
|
556
|
+
arrays: list[np.ndarray] = []
|
|
557
|
+
if self.array is not None:
|
|
558
|
+
arrays.append(self.array)
|
|
559
|
+
|
|
560
|
+
if isinstance(columns, (list, tuple)):
|
|
561
|
+
arrays.extend(columns)
|
|
562
|
+
elif isinstance(columns, np.ndarray):
|
|
563
|
+
arrays.append(columns)
|
|
564
|
+
else:
|
|
565
|
+
raise TypeError(f"Expected list, tuple, or np.ndarray, got {type(columns).__name__}")
|
|
566
|
+
|
|
567
|
+
self.array = join_struct_arrays(arrays)
|
|
568
|
+
|
|
569
|
+
def copy(self) -> ArrayDisplay:
|
|
570
|
+
"""Create a shallow copy of this ArrayDisplay.
|
|
571
|
+
|
|
572
|
+
The underlying array is shared (not copied), but all configuration
|
|
573
|
+
state (view columns, name columns, field components, aliases) is
|
|
574
|
+
independently mutable.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
A new ArrayDisplay with the same array reference and a copy
|
|
578
|
+
of all configuration state.
|
|
579
|
+
"""
|
|
580
|
+
new = ArrayDisplay.__new__(ArrayDisplay)
|
|
581
|
+
new.array = self.array
|
|
582
|
+
new.original_dtype = self.original_dtype
|
|
583
|
+
new._view_columns = list(self._view_columns)
|
|
584
|
+
new._name_columns = copy.deepcopy(self._name_columns)
|
|
585
|
+
new._field_components = copy.deepcopy(self._field_components)
|
|
586
|
+
new._column_aliases = dict(self._column_aliases)
|
|
587
|
+
return new
|
|
588
|
+
|
|
589
|
+
def __len__(self) -> int:
|
|
590
|
+
"""Return the number of rows in the array (0 if no array set)."""
|
|
591
|
+
if self.array is None:
|
|
592
|
+
return 0
|
|
593
|
+
return len(self.array)
|
|
594
|
+
|
|
595
|
+
def __repr__(self) -> str:
|
|
596
|
+
n_rows = len(self)
|
|
597
|
+
n_cols = len(self._view_columns)
|
|
598
|
+
parts = [f"rows={n_rows}", f"cols={n_cols}"]
|
|
599
|
+
if self._field_components:
|
|
600
|
+
parts.append(f"flattened={len(self._field_components)}")
|
|
601
|
+
if self._name_columns:
|
|
602
|
+
parts.append(f"enums={len(self._name_columns)}")
|
|
603
|
+
return f"ArrayDisplay({', '.join(parts)})"
|
|
604
|
+
|
|
605
|
+
def __str__(self) -> str:
|
|
606
|
+
parts = ["ArrayDisplay:"]
|
|
607
|
+
|
|
608
|
+
if self.array is not None:
|
|
609
|
+
parts.append(f" Shape: {self.array.shape}")
|
|
610
|
+
parts.append(f" Dtype: {self.array.dtype}")
|
|
611
|
+
parts.append(f" View columns ({len(self._view_columns)}):")
|
|
612
|
+
for col in self._view_columns[:10]:
|
|
613
|
+
parts.append(f" - {col}")
|
|
614
|
+
if len(self._view_columns) > 10:
|
|
615
|
+
parts.append(f" ... and {len(self._view_columns) - 10} more")
|
|
616
|
+
else:
|
|
617
|
+
parts.append(" Array: None")
|
|
618
|
+
|
|
619
|
+
if self._field_components:
|
|
620
|
+
parts.append(f" Flattened fields: {len(self._field_components)}")
|
|
621
|
+
if self._name_columns:
|
|
622
|
+
parts.append(f" Enum mappings: {len(self._name_columns)}")
|
|
623
|
+
|
|
624
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Type stubs for array_display module — improved IDE support."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ._exclusion import ColumnExclusion
|
|
10
|
+
|
|
11
|
+
class ArrayDisplay:
|
|
12
|
+
array: np.ndarray | None
|
|
13
|
+
original_dtype: np.dtype | None
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
src_obj: np.ndarray | None = ...,
|
|
18
|
+
column_list: list[str] | None = ...,
|
|
19
|
+
flatten_dtype: bool = ...,
|
|
20
|
+
exclude_columns: Sequence[ColumnExclusion] | None = ...,
|
|
21
|
+
component_names: dict[str, list[str]] | None = ...,
|
|
22
|
+
) -> None: ...
|
|
23
|
+
|
|
24
|
+
# Read-only properties
|
|
25
|
+
@property
|
|
26
|
+
def view_columns(self) -> list[str]: ...
|
|
27
|
+
@property
|
|
28
|
+
def name_columns(self) -> MappingProxyType[str, tuple[str, dict[int, str]]]: ...
|
|
29
|
+
@property
|
|
30
|
+
def field_components(self) -> MappingProxyType[str, list[str]]: ...
|
|
31
|
+
|
|
32
|
+
# 1. Column filtering
|
|
33
|
+
def set_view_columns(
|
|
34
|
+
self,
|
|
35
|
+
column_list: list[str] | None = ...,
|
|
36
|
+
exclude_columns: Sequence[ColumnExclusion] | None = ...,
|
|
37
|
+
) -> None: ...
|
|
38
|
+
def include_view_columns(self, column_list: list[str]) -> None: ...
|
|
39
|
+
def exclude_view_columns(self, column_list: list[str]) -> None: ...
|
|
40
|
+
def replace_view_columns(self, mapping: dict[str, str]) -> None: ...
|
|
41
|
+
|
|
42
|
+
# 2. Dtype flattening
|
|
43
|
+
def set_array(
|
|
44
|
+
self,
|
|
45
|
+
src_array: np.ndarray,
|
|
46
|
+
column_list: list[str] | None = ...,
|
|
47
|
+
flatten_dtype: bool = ...,
|
|
48
|
+
exclude_columns: Sequence[ColumnExclusion] | None = ...,
|
|
49
|
+
component_names: dict[str, list[str]] | None = ...,
|
|
50
|
+
) -> None: ...
|
|
51
|
+
def set_component_names(self, field: str, names: list[str]) -> None: ...
|
|
52
|
+
|
|
53
|
+
# 3. Enum mapping
|
|
54
|
+
def add_name_columns(self, name_columns: dict[str, tuple[str, dict[int, str]]]) -> None: ...
|
|
55
|
+
|
|
56
|
+
# 4. Array slicing
|
|
57
|
+
def to_slice(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
head: int | None = ...,
|
|
61
|
+
tail: int | None = ...,
|
|
62
|
+
mask: np.ndarray | None = ...,
|
|
63
|
+
indices: np.ndarray | None = ...,
|
|
64
|
+
) -> np.ndarray: ...
|
|
65
|
+
|
|
66
|
+
# 5. Adapters
|
|
67
|
+
def to_table(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
head: int = ...,
|
|
71
|
+
tail: int = ...,
|
|
72
|
+
max_col_width: int = ...,
|
|
73
|
+
indices: np.ndarray | None = ...,
|
|
74
|
+
) -> str: ...
|
|
75
|
+
def to_dataframe(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
indices: np.ndarray | None = ...,
|
|
79
|
+
head: int | None = ...,
|
|
80
|
+
tail: int | None = ...,
|
|
81
|
+
resolve_names: bool = ...,
|
|
82
|
+
) -> pd.DataFrame: ...
|
|
83
|
+
def _repr_html_(self) -> str | None: ...
|
|
84
|
+
|
|
85
|
+
# Misc
|
|
86
|
+
def append_columns(
|
|
87
|
+
self, columns: list[np.ndarray] | tuple[np.ndarray, ...] | np.ndarray
|
|
88
|
+
) -> None: ...
|
|
89
|
+
def copy(self) -> ArrayDisplay: ...
|
|
90
|
+
def __len__(self) -> int: ...
|
|
91
|
+
def __repr__(self) -> str: ...
|
|
92
|
+
def __str__(self) -> str: ...
|
|
File without changes
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-array-display
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Presentation layer for NumPy structured arrays — column filtering, dtype flattening, enum mapping, array slicing, and adapters
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
Requires-Python: <3.15,>=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: vcti-nputils>=1.0.0
|
|
11
|
+
Provides-Extra: pandas
|
|
12
|
+
Requires-Dist: pandas>=2.0; extra == "pandas"
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
|
15
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
16
|
+
Requires-Dist: pandas>=2.0; extra == "test"
|
|
17
|
+
Provides-Extra: lint
|
|
18
|
+
Requires-Dist: ruff; extra == "lint"
|
|
19
|
+
Provides-Extra: typecheck
|
|
20
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
21
|
+
Requires-Dist: pandas-stubs; extra == "typecheck"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Array Display
|
|
25
|
+
|
|
26
|
+
Presentation layer for NumPy structured arrays.
|
|
27
|
+
|
|
28
|
+
ArrayDisplay wraps a structured NumPy array reference (no copy) and provides
|
|
29
|
+
five capabilities for controlling how the data is presented:
|
|
30
|
+
|
|
31
|
+
1. **Column filtering** — hide columns by name, regex, or predicate
|
|
32
|
+
2. **Dtype flattening** — expand vector/matrix fields into scalar columns with custom component names
|
|
33
|
+
3. **Enum mapping** — lazily map integer ID columns to human-readable names
|
|
34
|
+
4. **Array slicing** — create lightweight index arrays for bounded access
|
|
35
|
+
5. **Adapters** — render as text table or pandas DataFrame on bounded slices
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install vcti-array-display>=1.1.0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With pandas support (optional):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install vcti-array-display[pandas]>=1.1.0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 1. Column Filtering
|
|
52
|
+
|
|
53
|
+
Hide unwanted columns using any combination of exact names, regex patterns,
|
|
54
|
+
and callable predicates.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import re
|
|
58
|
+
import numpy as np
|
|
59
|
+
from vcti.arraydisplay import ArrayDisplay, FILLER_COLUMNS, LENGTH_COLUMNS, VOID_COLUMNS
|
|
60
|
+
|
|
61
|
+
dt = np.dtype([
|
|
62
|
+
('id', 'i4'), ('f0', 'V4'), ('label', 'U20'),
|
|
63
|
+
('label_len', 'i4'), ('value', 'f8'), ('f3', 'V8'),
|
|
64
|
+
])
|
|
65
|
+
arr = np.zeros(10, dtype=dt)
|
|
66
|
+
|
|
67
|
+
# Pre-built patterns for C++ interop noise
|
|
68
|
+
view = ArrayDisplay(arr, exclude_columns=[FILLER_COLUMNS, LENGTH_COLUMNS])
|
|
69
|
+
view.view_columns # ['id', 'label', 'value']
|
|
70
|
+
|
|
71
|
+
# Or exclude by dtype — catches all void padding regardless of name
|
|
72
|
+
view = ArrayDisplay(arr, exclude_columns=[VOID_COLUMNS, LENGTH_COLUMNS])
|
|
73
|
+
|
|
74
|
+
# Mix exact names, regex, and callables
|
|
75
|
+
view = ArrayDisplay(arr, exclude_columns=[
|
|
76
|
+
FILLER_COLUMNS, # regex: ^f\d+$
|
|
77
|
+
LENGTH_COLUMNS, # regex: _len$
|
|
78
|
+
"debug_flag", # exact name
|
|
79
|
+
re.compile(r"^tmp_"), # custom regex
|
|
80
|
+
lambda name, dtype: dtype.kind == 'V', # by dtype
|
|
81
|
+
])
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
No magic defaults — `ArrayDisplay(arr)` shows all columns. Pre-built patterns
|
|
85
|
+
`FILLER_COLUMNS`, `LENGTH_COLUMNS`, and `VOID_COLUMNS` are opt-in.
|
|
86
|
+
Callables receive `(name, dtype)` for filtering by type, shape, or both.
|
|
87
|
+
|
|
88
|
+
## 2. Dtype Flattening
|
|
89
|
+
|
|
90
|
+
Expand vector and matrix fields into individual scalar columns, with
|
|
91
|
+
optional user-defined component names.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
dt = np.dtype([('id', 'i4'), ('position', 'f8', (3,))])
|
|
95
|
+
arr = np.zeros(5, dtype=dt)
|
|
96
|
+
|
|
97
|
+
# Default numeric suffixes
|
|
98
|
+
view = ArrayDisplay(arr, flatten_dtype=True)
|
|
99
|
+
view.view_columns # ['id', 'position_0', 'position_1', 'position_2']
|
|
100
|
+
|
|
101
|
+
# Custom component names
|
|
102
|
+
view = ArrayDisplay(arr, flatten_dtype=True,
|
|
103
|
+
component_names={'position': ['x', 'y', 'z']})
|
|
104
|
+
view.view_columns # ['id', 'position_x', 'position_y', 'position_z']
|
|
105
|
+
|
|
106
|
+
# Component grouping is tracked for consumers (e.g., header spanning)
|
|
107
|
+
view.field_components
|
|
108
|
+
# {'position': ['position_x', 'position_y', 'position_z']}
|
|
109
|
+
|
|
110
|
+
# Rename after construction
|
|
111
|
+
view.set_component_names('position', ['lat', 'lon', 'alt'])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> **Note:** Default flattened names (e.g., `position_0`) are generated by
|
|
115
|
+
> `vcti-nputils` and may truncate long field names. Use `component_names`
|
|
116
|
+
> to ensure predictable, readable column names regardless of the defaults.
|
|
117
|
+
|
|
118
|
+
## 3. Enum Mapping
|
|
119
|
+
|
|
120
|
+
Map integer ID columns to human-readable names. The mapping is stored as
|
|
121
|
+
a recipe and resolved lazily — only when presenting a bounded slice.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
dt = np.dtype([('id', 'i4'), ('element_type', 'i4'), ('value', 'f8')])
|
|
125
|
+
arr = np.array([(1, 1, 10.5), (2, 2, 20.3), (3, 1, 15.0)], dtype=dt)
|
|
126
|
+
view = ArrayDisplay(arr)
|
|
127
|
+
|
|
128
|
+
view.add_name_columns({
|
|
129
|
+
'element_type_name': ('element_type', {1: 'QUAD', 2: 'HEX'}),
|
|
130
|
+
})
|
|
131
|
+
view.view_columns # ['id', 'element_type_name', 'value']
|
|
132
|
+
|
|
133
|
+
# The mapping is NOT materialized yet — it's a recipe:
|
|
134
|
+
view.name_columns
|
|
135
|
+
# {'element_type_name': ('element_type', {1: 'QUAD', 2: 'HEX'})}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 4. Array Slicing
|
|
139
|
+
|
|
140
|
+
Create lightweight index arrays for bounded access. The index array is
|
|
141
|
+
tiny regardless of the underlying array size.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
idx = view.to_slice(head=20) # first 20 rows
|
|
145
|
+
idx = view.to_slice(tail=10) # last 10 rows
|
|
146
|
+
idx = view.to_slice(head=10, tail=5) # first 10 + last 5
|
|
147
|
+
idx = view.to_slice(mask=arr['value'] > 50) # boolean filter
|
|
148
|
+
idx = view.to_slice(indices=np.array([0, 100, 999])) # explicit
|
|
149
|
+
|
|
150
|
+
view.array[idx] # sliced data
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 5. Adapters
|
|
154
|
+
|
|
155
|
+
### Text table (pure numpy — no extra dependencies)
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
print(view.to_table(head=10, tail=5))
|
|
159
|
+
# id | element_type_name | value
|
|
160
|
+
# ---+-------------------+------
|
|
161
|
+
# 1 | QUAD | 10.5
|
|
162
|
+
# 2 | HEX | 20.3
|
|
163
|
+
# ...
|
|
164
|
+
# [1000 rows x 3 columns]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### pandas DataFrame (optional dependency)
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
df = view.to_dataframe() # full array
|
|
171
|
+
df = view.to_dataframe(head=100) # first 100 rows
|
|
172
|
+
df = view.to_dataframe(resolve_names=False) # raw ID columns
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Enum names are materialized using `pd.Categorical` for memory efficiency.
|
|
176
|
+
|
|
177
|
+
### Jupyter notebooks
|
|
178
|
+
|
|
179
|
+
ArrayDisplay provides `_repr_html_()` — displays a bounded row window with
|
|
180
|
+
enum names resolved automatically.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Performance
|
|
185
|
+
|
|
186
|
+
Designed for large CAE arrays (millions of rows, GBs of data):
|
|
187
|
+
|
|
188
|
+
- **No array copy** — wraps a reference to the original numpy array
|
|
189
|
+
- **Lazy enum resolution** — resolved only on the displayed slice
|
|
190
|
+
- **Index arrays** — `to_slice()` returns kilobytes regardless of array size
|
|
191
|
+
- **Bounded presentation** — `to_table()` and `to_dataframe(head=N)` never touch the full array
|
|
192
|
+
- **`pd.Categorical`** — ~100x less memory than string columns for enum values
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## API Summary
|
|
197
|
+
|
|
198
|
+
| Method | Pillar | Description |
|
|
199
|
+
|--------|--------|-------------|
|
|
200
|
+
| `set_view_columns(...)` | Filtering | Configure visible columns |
|
|
201
|
+
| `include_view_columns(cols)` | Filtering | Add columns to the view |
|
|
202
|
+
| `exclude_view_columns(cols)` | Filtering | Remove columns from the view |
|
|
203
|
+
| `replace_view_columns(mapping)` | Filtering | Rename columns in the view |
|
|
204
|
+
| `set_array(arr, ...)` | Flattening | Set array with optional flattening |
|
|
205
|
+
| `set_component_names(field, names)` | Flattening | Rename flattened components |
|
|
206
|
+
| `add_name_columns(mappings)` | Enum mapping | Register lazy enum mappings |
|
|
207
|
+
| `to_slice(...)` | Slicing | Create index array for bounded access |
|
|
208
|
+
| `to_table(...)` | Adapter | Render text table (pure numpy) |
|
|
209
|
+
| `to_dataframe(...)` | Adapter | Convert to pandas DataFrame |
|
|
210
|
+
| `copy()` | Misc | Shallow copy with independent config |
|
|
211
|
+
| `append_columns(cols)` | Misc | Join additional structured columns |
|
|
212
|
+
|
|
213
|
+
## Examples
|
|
214
|
+
|
|
215
|
+
See [examples/full_pipeline.py](examples/full_pipeline.py) for a complete
|
|
216
|
+
end-to-end script demonstrating all five pillars.
|
|
217
|
+
|
|
218
|
+
## Dependencies
|
|
219
|
+
|
|
220
|
+
- [numpy](https://numpy.org/) (>=1.24) — required
|
|
221
|
+
- [vcti-nputils](https://pypi.org/project/vcti-nputils/) (>=1.0.0) — required
|
|
222
|
+
- [pandas](https://pandas.pydata.org/) (>=2.0) — optional, for `to_dataframe()` and Jupyter display
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
vcti/arraydisplay/__init__.py,sha256=w9Mh7ILQwDwXlH7TJrzsuWpktxfL3rU2_Jvc6-nYBAM,528
|
|
2
|
+
vcti/arraydisplay/_exclusion.py,sha256=VC6kzVEQivmI8c-EcT7thX4OvcCzzSXuOuF-lxFXDes,1930
|
|
3
|
+
vcti/arraydisplay/_flattening.py,sha256=IwqNxCKEO8Z0FyrZT-VVS_KbDJewzkq-hhlyDt7z5f0,1555
|
|
4
|
+
vcti/arraydisplay/_formatting.py,sha256=qJp9atqMHNCcWEtue0Tt-873V-mttlOWdrL-xWaXlSA,1191
|
|
5
|
+
vcti/arraydisplay/array_display.py,sha256=XFqtpyV84JNhEEjkarup5pg-Zi4NV5XbquQvVOB11Ks,23977
|
|
6
|
+
vcti/arraydisplay/array_display.pyi,sha256=fa_NkQ8chC0s_Uii2oYN6-V9lK4FI3a5-1raZNp-4Ss,2783
|
|
7
|
+
vcti/arraydisplay/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
vcti_array_display-1.1.0.dist-info/licenses/LICENSE,sha256=gqRj-E4YRsT7mZ52W76LG6aTTFv6iEOK9QR_fV5EdrI,369
|
|
9
|
+
vcti_array_display-1.1.0.dist-info/METADATA,sha256=pQ-dNbBYaObnx41BXMYzc2u-x5nACi_HVYW18bV6P9Y,7707
|
|
10
|
+
vcti_array_display-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
vcti_array_display-1.1.0.dist-info/top_level.txt,sha256=Jl6AIAI3Xhru_BFQAhD_13VeXLmZQd9BqBNUaAKNgKs,5
|
|
12
|
+
vcti_array_display-1.1.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
13
|
+
vcti_array_display-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
|
|
2
|
+
All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This software is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
distribution, or use of this software, via any medium, is strictly
|
|
6
|
+
prohibited. Access is granted only to authorized VCollab developers
|
|
7
|
+
and individuals explicitly authorized by Visual Collaboration
|
|
8
|
+
Technologies Inc.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|