|
|
@ -1,4 +1,9 @@ |
|
|
|
"""This module contains definitions and utilities for 2-, 4-, and 8-valued logic operations. |
|
|
|
"""This module contains definitions and tools for 2-, 4-, and 8-valued logic operations. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logic values are stored in numpy arrays with data type ``np.uint8``. |
|
|
|
|
|
|
|
There are no explicit data structures in KyuPy for holding patterns, pattern sets or vectors. |
|
|
|
|
|
|
|
However, there are conventions on logic value encoding and on the order of axes. |
|
|
|
|
|
|
|
Utility functions defined here follow these conventions. |
|
|
|
|
|
|
|
|
|
|
|
8 logic values are defined as integer constants. |
|
|
|
8 logic values are defined as integer constants. |
|
|
|
|
|
|
|
|
|
|
@ -21,10 +26,22 @@ Except when bit0 differs from bit1, but bit2 (activity) is 0: |
|
|
|
4-valued logic only considers bit0 and bit1. |
|
|
|
4-valued logic only considers bit0 and bit1. |
|
|
|
8-valued logic considers all 3 bits. |
|
|
|
8-valued logic considers all 3 bits. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logic values are stored in numpy arrays of data type ``np.uint8``. |
|
|
|
|
|
|
|
The axis convention is as follows: |
|
|
|
|
|
|
|
* The **last** axis goes along patterns/vectors. I.e. ``values[...,0]`` is pattern 0, ``values[...,1]`` is pattern 1, etc. |
|
|
|
|
|
|
|
* The **second-to-last** axis goes along the I/O and flip-flops of circuits. For a circuit ``c``, this axis is usually |
|
|
|
|
|
|
|
``len(c.s_nodes)`` long. The values of all inputs, outputs and flip-flops are stored within the same array and the location |
|
|
|
|
|
|
|
along the second-to-last axis is determined by the order in ``c.s_nodes``. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Two storage formats are used in KyuPy: |
|
|
|
|
|
|
|
* ``mv...`` (for "multi-valued"): Each logic value is stored in the least significant 3 bits of ``np.uint8``. |
|
|
|
|
|
|
|
* ``bp...`` (for "bit-parallel"): Groups of 8 logic values are stored as three ``np.uint8``. This format is used |
|
|
|
|
|
|
|
for bit-parallel logic simulations. It is also more memory-efficient. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The functions in this module use the ``mv...`` and ``bp...`` prefixes to signify the storage format they operate on. |
|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
import math |
|
|
|
|
|
|
|
from collections.abc import Iterable |
|
|
|
from collections.abc import Iterable |
|
|
|
|
|
|
|
|
|
|
|
import numpy as np |
|
|
|
import numpy as np |
|
|
@ -80,41 +97,12 @@ def interpret(value): |
|
|
|
return UNKNOWN |
|
|
|
return UNKNOWN |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_bit_in_lut = np.array([2 ** x for x in range(7, -1, -1)], dtype='uint8') |
|
|
|
def mvarray(*a): |
|
|
|
|
|
|
|
"""Converts (lists of) Boolean values or strings into a multi-valued array. |
|
|
|
|
|
|
|
|
|
|
|
@numba.njit |
|
|
|
|
|
|
|
def bit_in(a, pos): |
|
|
|
|
|
|
|
return a[pos >> 3] & _bit_in_lut[pos & 7] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unpackbits(a : np.ndarray): |
|
|
|
|
|
|
|
"""Unpacks the bits of given ndarray `a`. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Similar to `np.unpackbits`, but accepts any dtype, preserves the shape of `a` and |
|
|
|
|
|
|
|
adds a new last axis with the bits of each item. Bits are in 'little'-order, i.e., |
|
|
|
|
|
|
|
a[...,0] is the least significant bit. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
return np.unpackbits(a.view(np.uint8), bitorder='little').reshape(*a.shape, 8*a.itemsize) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def packbits(a, dtype=np.uint8): |
|
|
|
|
|
|
|
"""Packs the values of a boolean-valued array `a` along its last axis into bits. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Similary to `np.packbits`, but returns an array of given dtype and the shape of `a` with the last axis removed. |
|
|
|
The given values are interpreted and the axes are arranged as per KyuPy's convention. |
|
|
|
The last axis of `a` is truncated or padded according to the bit-width of the given dtype. |
|
|
|
Use this function to convert strings into multi-valued arrays. |
|
|
|
Signed integer datatypes are padded with the most significant bit, all others are padded with `0`. |
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
dtype = np.dtype(dtype) |
|
|
|
|
|
|
|
bits = 8 * dtype.itemsize |
|
|
|
|
|
|
|
a = a[...,:bits] |
|
|
|
|
|
|
|
if a.shape[-1] < bits: |
|
|
|
|
|
|
|
p = [(0,0)]*(len(a.shape)-1) + [(0, bits-a.shape[-1])] |
|
|
|
|
|
|
|
a = np.pad(a, p, 'edge') if dtype.name[0] == 'i' else np.pad(a, p, 'constant', constant_values=0) |
|
|
|
|
|
|
|
return np.packbits(a, bitorder='little').view(dtype).reshape(a.shape[:-1]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mvarray(*a): |
|
|
|
|
|
|
|
mva = np.array(interpret(a), dtype=np.uint8) |
|
|
|
mva = np.array(interpret(a), dtype=np.uint8) |
|
|
|
if mva.ndim < 2: return mva |
|
|
|
if mva.ndim < 2: return mva |
|
|
|
if mva.shape[-2] > 1: return mva.swapaxes(-1, -2) |
|
|
|
if mva.shape[-2] > 1: return mva.swapaxes(-1, -2) |
|
|
@ -122,24 +110,13 @@ def mvarray(*a): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mv_str(mva, delim='\n'): |
|
|
|
def mv_str(mva, delim='\n'): |
|
|
|
|
|
|
|
"""Renders a given multi-valued array into a string. |
|
|
|
|
|
|
|
""" |
|
|
|
sa = np.choose(mva, np.array([*'0X-1PRFN'], dtype=np.unicode_)) |
|
|
|
sa = np.choose(mva, np.array([*'0X-1PRFN'], dtype=np.unicode_)) |
|
|
|
if mva.ndim == 1: return ''.join(sa) |
|
|
|
if mva.ndim == 1: return ''.join(sa) |
|
|
|
return delim.join([''.join(c) for c in sa.swapaxes(-1,-2)]) |
|
|
|
return delim.join([''.join(c) for c in sa.swapaxes(-1,-2)]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mv_to_bp(mva): |
|
|
|
|
|
|
|
if mva.ndim == 1: mva = mva[..., np.newaxis] |
|
|
|
|
|
|
|
return np.packbits(unpackbits(mva)[...,:3], axis=-2, bitorder='little').swapaxes(-1,-2) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bparray(*a): |
|
|
|
|
|
|
|
return mv_to_bp(mvarray(*a)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bp_to_mv(bpa): |
|
|
|
|
|
|
|
return packbits(np.unpackbits(bpa, axis=-1, bitorder='little').swapaxes(-1,-2)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mv_not(out, inp): |
|
|
|
def _mv_not(out, inp): |
|
|
|
np.bitwise_xor(inp, 0b11, out=out) # this also exchanges UNASSIGNED <-> UNKNOWN |
|
|
|
np.bitwise_xor(inp, 0b11, out=out) # this also exchanges UNASSIGNED <-> UNKNOWN |
|
|
|
np.putmask(out, (inp == UNKNOWN), UNKNOWN) # restore UNKNOWN |
|
|
|
np.putmask(out, (inp == UNKNOWN), UNKNOWN) # restore UNKNOWN |
|
|
@ -148,13 +125,10 @@ def _mv_not(out, inp): |
|
|
|
def mv_not(x1 : np.ndarray, out=None): |
|
|
|
def mv_not(x1 : np.ndarray, out=None): |
|
|
|
"""A multi-valued NOT operator. |
|
|
|
"""A multi-valued NOT operator. |
|
|
|
|
|
|
|
|
|
|
|
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param x1: A multi-valued array. |
|
|
|
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` |
|
|
|
:param out: An optional storage destination. If None, a new multi-valued array is returned. |
|
|
|
is returned. |
|
|
|
:return: A multi-valued array with the result. |
|
|
|
:return: An :py:class:`MVArray` with the result. |
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
#m = mv_getm(x1) |
|
|
|
|
|
|
|
#x1 = mv_cast(x1, m=m)[0] |
|
|
|
|
|
|
|
out = out or np.empty(x1.shape, dtype=np.uint8) |
|
|
|
out = out or np.empty(x1.shape, dtype=np.uint8) |
|
|
|
_mv_not(out, x1) |
|
|
|
_mv_not(out, x1) |
|
|
|
return out |
|
|
|
return out |
|
|
@ -176,14 +150,11 @@ def _mv_or(out, *ins): |
|
|
|
def mv_or(x1, x2, out=None): |
|
|
|
def mv_or(x1, x2, out=None): |
|
|
|
"""A multi-valued OR operator. |
|
|
|
"""A multi-valued OR operator. |
|
|
|
|
|
|
|
|
|
|
|
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param x1: A multi-valued array. |
|
|
|
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param x2: A multi-valued array. |
|
|
|
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` |
|
|
|
:param out: An optional storage destination. If None, a new multi-valued array is returned. |
|
|
|
is returned. |
|
|
|
:return: A multi-valued array with the result. |
|
|
|
:return: An :py:class:`MVArray` with the result. |
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
#m = mv_getm(x1, x2) |
|
|
|
|
|
|
|
#x1, x2 = mv_cast(x1, x2, m=m) |
|
|
|
|
|
|
|
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) |
|
|
|
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) |
|
|
|
_mv_or(out, x1, x2) |
|
|
|
_mv_or(out, x1, x2) |
|
|
|
return out |
|
|
|
return out |
|
|
@ -206,14 +177,11 @@ def _mv_and(out, *ins): |
|
|
|
def mv_and(x1, x2, out=None): |
|
|
|
def mv_and(x1, x2, out=None): |
|
|
|
"""A multi-valued AND operator. |
|
|
|
"""A multi-valued AND operator. |
|
|
|
|
|
|
|
|
|
|
|
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param x1: A multi-valued array. |
|
|
|
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param x2: A multi-valued array. |
|
|
|
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` |
|
|
|
:param out: An optional storage destination. If None, a new multi-valued array is returned. |
|
|
|
is returned. |
|
|
|
:return: A multi-valued array with the result. |
|
|
|
:return: An :py:class:`MVArray` with the result. |
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
#m = mv_getm(x1, x2) |
|
|
|
|
|
|
|
#x1, x2 = mv_cast(x1, x2, m=m) |
|
|
|
|
|
|
|
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) |
|
|
|
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) |
|
|
|
_mv_and(out, x1, x2) |
|
|
|
_mv_and(out, x1, x2) |
|
|
|
return out |
|
|
|
return out |
|
|
@ -233,24 +201,28 @@ def _mv_xor(out, *ins): |
|
|
|
def mv_xor(x1, x2, out=None): |
|
|
|
def mv_xor(x1, x2, out=None): |
|
|
|
"""A multi-valued XOR operator. |
|
|
|
"""A multi-valued XOR operator. |
|
|
|
|
|
|
|
|
|
|
|
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param x1: A multi-valued array. |
|
|
|
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param x2: A multi-valued array. |
|
|
|
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` |
|
|
|
:param out: An optional storage destination. If None, a new multi-valued array is returned. |
|
|
|
is returned. |
|
|
|
:return: A multi-valued array with the result. |
|
|
|
:return: An :py:class:`MVArray` with the result. |
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
#m = mv_getm(x1, x2) |
|
|
|
|
|
|
|
#x1, x2 = mv_cast(x1, x2, m=m) |
|
|
|
|
|
|
|
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) |
|
|
|
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) |
|
|
|
_mv_xor(out, x1, x2) |
|
|
|
_mv_xor(out, x1, x2) |
|
|
|
return out |
|
|
|
return out |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mv_latch(d, t, q_prev, out=None): |
|
|
|
def mv_latch(d, t, q_prev, out=None): |
|
|
|
"""A latch that is transparent if `t` is high. `q_prev` has to be the output value from the previous clock cycle. |
|
|
|
"""A multi-valued latch operator. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A latch outputs ``d`` when transparent (``t`` is high). |
|
|
|
|
|
|
|
It outputs ``q_prev`` when in latched state (``t`` is low). |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:param d: A multi-valued array for the data input. |
|
|
|
|
|
|
|
:param t: A multi-valued array for the control input. |
|
|
|
|
|
|
|
:param q_prev: A multi-valued array with the output value of this latch from the previous clock cycle. |
|
|
|
|
|
|
|
:param out: An optional storage destination. If None, a new multi-valued array is returned. |
|
|
|
|
|
|
|
:return: A multi-valued array for the latch output ``q``. |
|
|
|
""" |
|
|
|
""" |
|
|
|
#m = mv_getm(d, t, q_prev) |
|
|
|
|
|
|
|
#d, t, q_prev = mv_cast(d, t, q_prev, m=m) |
|
|
|
|
|
|
|
out = out or np.empty(np.broadcast(d, t, q_prev).shape, dtype=np.uint8) |
|
|
|
out = out or np.empty(np.broadcast(d, t, q_prev).shape, dtype=np.uint8) |
|
|
|
out[...] = t & d & 0b011 |
|
|
|
out[...] = t & d & 0b011 |
|
|
|
out[...] |= ~t & 0b010 & (q_prev << 1) |
|
|
|
out[...] |= ~t & 0b010 & (q_prev << 1) |
|
|
@ -268,16 +240,11 @@ def mv_transition(init, final, out=None): |
|
|
|
Pulses in the input data are ignored. If any of the inputs are ``UNKNOWN``, the result is ``UNKNOWN``. |
|
|
|
Pulses in the input data are ignored. If any of the inputs are ``UNKNOWN``, the result is ``UNKNOWN``. |
|
|
|
If both inputs are ``UNASSIGNED``, the result is ``UNASSIGNED``. |
|
|
|
If both inputs are ``UNASSIGNED``, the result is ``UNASSIGNED``. |
|
|
|
|
|
|
|
|
|
|
|
:param init: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param init: A multi-valued array. |
|
|
|
:param final: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. |
|
|
|
:param final: A multi-valued array. |
|
|
|
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` |
|
|
|
:param out: An optional storage destination. If None, a new multi-valued array is returned. |
|
|
|
is returned. |
|
|
|
:return: A multi-valued array with the result. |
|
|
|
:return: An :py:class:`MVArray` with the result. |
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
#m = mv_getm(init, final) |
|
|
|
|
|
|
|
#init, final = mv_cast(init, final, m=m) |
|
|
|
|
|
|
|
#init = init.data |
|
|
|
|
|
|
|
#final = final.data |
|
|
|
|
|
|
|
out = out or np.empty(np.broadcast(init, final).shape, dtype=np.uint8) |
|
|
|
out = out or np.empty(np.broadcast(init, final).shape, dtype=np.uint8) |
|
|
|
out[...] = (init & 0b010) | (final & 0b001) |
|
|
|
out[...] = (init & 0b010) | (final & 0b001) |
|
|
|
out[...] |= ((out << 1) ^ (out << 2)) & 0b100 |
|
|
|
out[...] |= ((out << 1) ^ (out << 2)) & 0b100 |
|
|
@ -288,6 +255,28 @@ def mv_transition(init, final, out=None): |
|
|
|
return out |
|
|
|
return out |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mv_to_bp(mva): |
|
|
|
|
|
|
|
"""Converts a multi-valued array into a bit-parallel array. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
if mva.ndim == 1: mva = mva[..., np.newaxis] |
|
|
|
|
|
|
|
return np.packbits(unpackbits(mva)[...,:3], axis=-2, bitorder='little').swapaxes(-1,-2) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bparray(*a): |
|
|
|
|
|
|
|
"""Converts (lists of) Boolean values or strings into a bit-parallel array. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The given values are interpreted and the axes are arranged as per KyuPy's convention. |
|
|
|
|
|
|
|
Use this function to convert strings into bit-parallel arrays. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
return mv_to_bp(mvarray(*a)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bp_to_mv(bpa): |
|
|
|
|
|
|
|
"""Converts a bit-parallel array into a multi-valued array. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
return packbits(np.unpackbits(bpa, axis=-1, bitorder='little').swapaxes(-1,-2)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bp_buf(out, inp): |
|
|
|
def bp_buf(out, inp): |
|
|
|
unknown = (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :] |
|
|
|
unknown = (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :] |
|
|
|
out[..., 0, :] = inp[..., 0, :] | unknown |
|
|
|
out[..., 0, :] = inp[..., 0, :] | unknown |
|
|
@ -356,3 +345,37 @@ def bp_latch(out, d, t, q_prev): |
|
|
|
out[..., 1, :] &= ~any_unknown |
|
|
|
out[..., 1, :] &= ~any_unknown |
|
|
|
out[..., 2, :] &= ~any_unknown |
|
|
|
out[..., 2, :] &= ~any_unknown |
|
|
|
return out |
|
|
|
return out |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_bit_in_lut = np.array([2 ** x for x in range(7, -1, -1)], dtype='uint8') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@numba.njit |
|
|
|
|
|
|
|
def bit_in(a, pos): |
|
|
|
|
|
|
|
return a[pos >> 3] & _bit_in_lut[pos & 7] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unpackbits(a : np.ndarray): |
|
|
|
|
|
|
|
"""Unpacks the bits of given ndarray ``a``. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Similar to ``np.unpackbits``, but accepts any dtype, preserves the shape of ``a`` and |
|
|
|
|
|
|
|
adds a new last axis with the bits of each item. Bits are in 'little'-order, i.e., |
|
|
|
|
|
|
|
a[...,0] is the least significant bit of each item. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
return np.unpackbits(a.view(np.uint8), bitorder='little').reshape(*a.shape, 8*a.itemsize) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def packbits(a, dtype=np.uint8): |
|
|
|
|
|
|
|
"""Packs the values of a boolean-valued array ``a`` along its last axis into bits. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Similar to ``np.packbits``, but returns an array of given dtype and the shape of ``a`` with the last axis removed. |
|
|
|
|
|
|
|
The last axis of `a` is truncated or padded according to the bit-width of the given dtype. |
|
|
|
|
|
|
|
Signed integer datatypes are padded with the most significant bit, all others are padded with `0`. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
dtype = np.dtype(dtype) |
|
|
|
|
|
|
|
bits = 8 * dtype.itemsize |
|
|
|
|
|
|
|
a = a[...,:bits] |
|
|
|
|
|
|
|
if a.shape[-1] < bits: |
|
|
|
|
|
|
|
p = [(0,0)]*(len(a.shape)-1) + [(0, bits-a.shape[-1])] |
|
|
|
|
|
|
|
a = np.pad(a, p, 'edge') if dtype.name[0] == 'i' else np.pad(a, p, 'constant', constant_values=0) |
|
|
|
|
|
|
|
return np.packbits(a, bitorder='little').view(dtype).reshape(a.shape[:-1]) |
|
|
|