Source code for spynnaker.pyNN.models.spike_source.spike_source_poisson_machine_vertex

# Copyright (c) 2016 The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from enum import IntEnum
from collections.abc import Sized
import struct
from typing import (
    Iterable, List, Optional, Sequence, TypeVar, Union,
    cast, TYPE_CHECKING)

import numpy
from numpy import uint16, uint32

from spinn_utilities.overrides import overrides

from spinnman.model.enums import ExecutableType

from pacman.model.graphs import AbstractEdgePartition
from pacman.model.graphs.common import Slice
from pacman.model.graphs.machine import (
    MachineVertex, SourceSegmentedSDRAMMachinePartition, SDRAMMachineEdge)
from pacman.model.placements import Placement
from pacman.model.resources import AbstractSDRAM
from pacman.utilities.utility_calls import get_keys

from spinn_front_end_common.abstract_models import (
    AbstractHasAssociatedBinary,
    AbstractRewritesDataSpecification, AbstractGeneratesDataSpecification)
from spinn_front_end_common.interface.buffer_management import (
    recording_utilities)
from spinn_front_end_common.interface.buffer_management.buffer_models import (
    AbstractReceiveBuffersToHost)
from spinn_front_end_common.interface.ds import (
    DataType, DataSpecificationBase, DataSpecificationGenerator,
    DataSpecificationReloader)
from spinn_front_end_common.interface.profiling import (
    AbstractHasProfileData, ProfileData, profile_utils)
from spinn_front_end_common.interface.provenance import (
    ProvidesProvenanceDataFromMachineImpl)
from spinn_front_end_common.interface.simulation import simulation_utilities
from spinn_front_end_common.utilities import helpful_functions
from spinn_front_end_common.utilities.constants import (
    SIMULATION_N_BYTES, BYTES_PER_WORD, BYTES_PER_SHORT)
from spinn_front_end_common.utilities.helpful_functions import (
    locate_memory_region_for_placement)
from spinn_front_end_common.interface.provenance import ProvenanceWriter

from spynnaker.pyNN.data import SpynnakerDataView
from spynnaker.pyNN.exceptions import SynapticConfigurationException
from spynnaker.pyNN.models.abstract_models import (
    AbstractMaxSpikes, SendsSynapticInputsOverSDRAM,
    ReceivesSynapticInputsOverSDRAM)
from spynnaker.pyNN.utilities.constants import (
    LIVE_POISSON_CONTROL_PARTITION_ID)
from spynnaker.pyNN.models.neuron.synapse_dynamics.types import (
    NUMPY_CONNECTORS_DTYPE, ConnectionsArray)

if TYPE_CHECKING:
    from .spike_source_poisson_vertex import SpikeSourcePoissonVertex
    from spynnaker.pyNN.models.neural_projections import SynapseInformation
    from spynnaker.pyNN.models.neural_projections.connectors import (
        AbstractGenerateConnectorOnHost)

#: :meta private:
T = TypeVar("T")


def _flatten(alist: Iterable[Union[T, Iterable[T]]]) -> Iterable[T]:
    for item in alist:
        if hasattr(item, "__iter__"):
            yield from _flatten(item)
        else:
            yield item


def get_n_rates(vertex_slice: Slice, rate_data: Sequence[Sized]) -> int:
    """
    :returns: How many rates there are to be stored in total.
    """
    return sum(len(rate_data[i]) for i in vertex_slice.get_raster_ids())


def get_params_bytes(n_atoms: int) -> int:
    """
    :param n_atoms: How many atoms to account for
    :returns: The size of the Poisson parameters in bytes.
    """
    return (PARAMS_BASE_WORDS + n_atoms) * BYTES_PER_WORD


def get_rates_bytes(n_atoms: int, n_rates: int) -> int:
    """
    :param n_atoms: How many atoms to account for
    :param n_rates: How many rates to account for
    :returns: The size of the Poisson rates in bytes.
    """
    return ((n_atoms * PARAMS_WORDS_PER_NEURON) +
            (n_rates * PARAMS_WORDS_PER_RATE)) * BYTES_PER_WORD


def get_expander_rates_bytes(n_atoms: int, n_rates: int) -> int:
    """
    :param n_atoms: How many atoms to account for
    :param n_rates: How many rates to account for
    :returns: The size of the Poisson rates in bytes.
    """
    return ((n_atoms * EXPANDER_WORDS_PER_NEURON) +
            (n_rates * PARAMS_WORDS_PER_RATE) +
            EXPANDER_HEADER_WORDS) * BYTES_PER_WORD


def get_sdram_edge_params_bytes(vertex_slice: Slice) -> int:
    """
    :param vertex_slice:
    :returns: The size of the Poisson SDRAM region in bytes.
    """
    return SDRAM_EDGE_PARAMS_BASE_BYTES + (
        vertex_slice.n_atoms * SDRAM_EDGE_PARAMS_BYTES_PER_WEIGHT)


def _u3232_to_uint64(array: numpy.ndarray) -> numpy.ndarray:
    """
    Convert data to be written in U3232 to uint64 array.
    """
    return numpy.round(array * float(DataType.U3232.scale)).astype(
        DataType.U3232.numpy_typename)


# 1. uint32_t has_key;
# 2. uint32_t set_rate_neuron_id_mask;
# 3. UFRACT seconds_per_tick; 4. REAL ticks_per_second;
# 5. REAL slow_rate_per_tick_cut-off; 6. REAL fast_rate_per_tick_cut-off;
# 7. unt32_t first_source_id; 8. uint32_t n_spike_sources;
# 9. uint32_t max_spikes_per_timestep;
# 10. uint32_t n_colour_bits;
# 11,12,13,14 mars_kiss64_seed_t (uint[4]) spike_source_seed;
# 15. Rate changed flag
PARAMS_BASE_WORDS = 15

# uint32_t n_rates; uint32_t index
PARAMS_WORDS_PER_NEURON = 2

# unsigned long accum rate, start, duration
PARAMS_WORDS_PER_RATE = 6

# uint32_t count; one per neuron in worst case (= every neuron different)
EXPANDER_WORDS_PER_NEURON = PARAMS_WORDS_PER_NEURON + 1

# uint32_t n_items
EXPANDER_HEADER_WORDS = 2

# The size of each weight to be stored for SDRAM transfers
SDRAM_EDGE_PARAMS_BYTES_PER_WEIGHT = BYTES_PER_SHORT

# SDRAM edge param base size:
# 1. address, 2. size of transfer,
# 3. offset to start writing, 4. Array of weights (not counted here)
SDRAM_EDGE_PARAMS_BASE_BYTES = 3 * BYTES_PER_WORD

_ONE_WORD = struct.Struct("<I")
_FOUR_WORDS = struct.Struct("<4I")


class SpikeSourcePoissonMachineVertex(
        MachineVertex, AbstractReceiveBuffersToHost,
        ProvidesProvenanceDataFromMachineImpl,
        AbstractHasProfileData,
        AbstractHasAssociatedBinary, AbstractRewritesDataSpecification,
        AbstractGeneratesDataSpecification,
        SendsSynapticInputsOverSDRAM):
    """
    Vertex that implements a Poisson-distributed spike source.
    """

    __slots__ = (
        "__buffered_sdram_per_timestep",
        "__is_recording",
        "__minimum_buffer_sdram",
        "__sdram",
        "__sdram_partition",
        "__rate_changed")

    class _PoissonSpikeSourceRegions(IntEnum):
        """
        Memory region IDs for the the Poisson source code.
        """
        #: System control information (simulation timestep, etc.)
        SYSTEM_REGION = 0
        #: The parameters for the Poisson generator.
        POISSON_PARAMS_REGION = 1
        #: Spike rates (and the times at which they apply).
        RATES_REGION = 2
        #: Record of when spikes were actually sent.
        SPIKE_HISTORY_REGION = 3
        #: Provenance data.
        PROVENANCE_REGION = 4
        #: Profiler data.
        PROFILER_REGION = 5
        #: Parameters for an SDRAM edge.
        SDRAM_EDGE_PARAMS = 6
        #: Data for the on-chip connection generator binaries.
        EXPANDER_REGION = 7

    PROFILE_TAG_LABELS = {
        0: "TIMER",
        1: "PROB_FUNC"}

    # The maximum timestep - this is the maximum value of a uint32
    _MAX_TIMESTEP = 0xFFFFFFFF

    # between Exp and Knuth
    SLOW_RATE_PER_TICK_CUTOFF = 0.01

    # between Knuth algorithm and Gaussian approx.
    FAST_RATE_PER_TICK_CUTOFF = 10

    # Seed offset in parameters and size on bytes
    SEED_SIZE_BYTES = 4 * BYTES_PER_WORD
    SEED_OFFSET_BYTES = (PARAMS_BASE_WORDS * 4) - SEED_SIZE_BYTES

    def __init__(
            self, sdram: AbstractSDRAM, is_recording: bool,
            label: Optional[str], app_vertex: SpikeSourcePoissonVertex,
            vertex_slice: Slice):
        """
        :param sdram: SDRAM usage of this vertex
        :param is_recording: Flag to say spikes are to be recorded
        :param label: The optional name of the vertex
        :param app_vertex:
            The application vertex that caused this machine vertex to be
            created. If `None`, there is no such application vertex.
        :param vertex_slice:
            The slice of the application vertex that this machine vertex
            implements.
        """

        super().__init__(
            label, app_vertex=app_vertex, vertex_slice=vertex_slice)
        self.__is_recording = is_recording
        self.__sdram = sdram
        self.__sdram_partition: Optional[
            SourceSegmentedSDRAMMachinePartition] = None
        self.__rate_changed = True

    @property
    def _pop_vertex(self) -> SpikeSourcePoissonVertex:
        return cast('SpikeSourcePoissonVertex', self.app_vertex)

[docs] @overrides(SendsSynapticInputsOverSDRAM.set_sdram_partition) def set_sdram_partition( self, sdram_partition: SourceSegmentedSDRAMMachinePartition) -> None: if self.__sdram_partition is not None: raise SynapticConfigurationException( "Cannot set SDRAM partition again") self.__sdram_partition = sdram_partition
@property @overrides(MachineVertex.sdram_required) def sdram_required(self) -> AbstractSDRAM: return self.__sdram @property @overrides(ProvidesProvenanceDataFromMachineImpl._provenance_region_id) def _provenance_region_id(self) -> int: return self._PoissonSpikeSourceRegions.PROVENANCE_REGION
[docs] @overrides(ProvidesProvenanceDataFromMachineImpl .parse_extra_provenance_items) def parse_extra_provenance_items( self, label: str, x: int, y: int, p: int, provenance_data: Sequence[int]) -> None: (n_saturations,) = provenance_data with ProvenanceWriter() as db: db.insert_core( x, y, p, "Times_synaptic_weights_have_saturated", n_saturations) if n_saturations > 0: db.insert_report( f"The weights from the Poisson synapses for {label}" f" saturated {n_saturations} times. ")
@property @overrides(ProvidesProvenanceDataFromMachineImpl._n_additional_data_items) def _n_additional_data_items(self) -> int: return 1
[docs] @overrides(AbstractReceiveBuffersToHost.get_recorded_region_ids) def get_recorded_region_ids(self) -> List[int]: if self.__is_recording: return [0] return []
[docs] @overrides(AbstractReceiveBuffersToHost.get_recording_region_base_address) def get_recording_region_base_address(self, placement: Placement) -> int: return locate_memory_region_for_placement( placement, self._PoissonSpikeSourceRegions.SPIKE_HISTORY_REGION)
[docs] @overrides(AbstractHasProfileData.get_profile_data) def get_profile_data(self, placement: Placement) -> ProfileData: return profile_utils.get_profiling_data( self._PoissonSpikeSourceRegions.PROFILER_REGION, self.PROFILE_TAG_LABELS, placement)
[docs] @overrides(AbstractHasAssociatedBinary.get_binary_file_name) def get_binary_file_name(self) -> str: return "spike_source_poisson.aplx"
[docs] @overrides(AbstractHasAssociatedBinary.get_binary_start_type) def get_binary_start_type(self) -> ExecutableType: return ExecutableType.USES_SIMULATION_INTERFACE
[docs] @overrides(AbstractMaxSpikes.max_spikes_per_second) def max_spikes_per_second(self) -> float: # pylint: disable=missing-function-docstring # TODO https://github.com/SpiNNakerManchester/sPyNNaker/issues/1435 return self._pop_vertex.max_rate
[docs] @overrides(AbstractMaxSpikes.max_spikes_per_ts) def max_spikes_per_ts(self) -> float: # pylint: disable=missing-function-docstring # TODO https://github.com/SpiNNakerManchester/sPyNNaker/issues/1435 return self._pop_vertex.max_spikes_per_ts()
[docs] @overrides(AbstractRewritesDataSpecification.reload_required) def reload_required(self) -> bool: if self.__rate_changed: return True return SpynnakerDataView.get_first_machine_time_step() == 0
[docs] @overrides(AbstractRewritesDataSpecification.set_reload_required) def set_reload_required(self, new_value: bool) -> None: self.__rate_changed = new_value
[docs] @overrides(AbstractRewritesDataSpecification.regenerate_data_specification) def regenerate_data_specification(self, spec: DataSpecificationReloader, placement: Placement) -> None: # write rates self._write_poisson_rates(spec) # end spec spec.end_specification()
@staticmethod def __conn(synapse_info: SynapseInformation ) -> AbstractGenerateConnectorOnHost: return cast( 'AbstractGenerateConnectorOnHost', synapse_info.connector)
[docs] @overrides(AbstractGeneratesDataSpecification.generate_data_specification) def generate_data_specification(self, spec: DataSpecificationGenerator, placement: Placement) -> None: spec.comment("\n*** Spec for SpikeSourcePoisson Instance ***\n\n") # if we are here, the rates have changed! self.__rate_changed = True # write setup data spec.reserve_memory_region( region=self._PoissonSpikeSourceRegions.SYSTEM_REGION, size=SIMULATION_N_BYTES, label='setup') spec.switch_write_focus( self._PoissonSpikeSourceRegions.SYSTEM_REGION) spec.write_array(simulation_utilities.get_simulation_header_array( self.get_binary_file_name())) # write recording data spec.reserve_memory_region( region=self._PoissonSpikeSourceRegions.SPIKE_HISTORY_REGION, size=recording_utilities.get_recording_header_size(1), label="Recording") spec.switch_write_focus( self._PoissonSpikeSourceRegions.SPIKE_HISTORY_REGION) sdram = self._pop_vertex.get_recording_sdram_usage(self.vertex_slice) recorded_region_sizes = [sdram.get_total_sdram( SpynnakerDataView.get_max_run_time_steps())] spec.write_array(recording_utilities.get_recording_header_array( recorded_region_sizes)) # Write provenance space self.reserve_provenance_data_region(spec) # write parameters self._write_poisson_parameters(spec) # write rates self._write_poisson_rates(spec) # write profile data profile_utils.reserve_profile_region( spec, self._PoissonSpikeSourceRegions.PROFILER_REGION, self._pop_vertex.n_profile_samples) profile_utils.write_profile_region_data( spec, self._PoissonSpikeSourceRegions.PROFILER_REGION, self._pop_vertex.n_profile_samples) # write SDRAM edge parameters spec.reserve_memory_region( region=self._PoissonSpikeSourceRegions.SDRAM_EDGE_PARAMS, label="sdram edge params", size=get_sdram_edge_params_bytes(self.vertex_slice)) spec.switch_write_focus( self._PoissonSpikeSourceRegions.SDRAM_EDGE_PARAMS) if self.__sdram_partition is None: spec.write_array([0, 0, 0]) else: size = self.__sdram_partition.get_sdram_size_of_region_for(self) proj = self._pop_vertex.outgoing_projections[0] # pylint: disable=protected-access synapse_info = proj._synapse_information spec.write_value( self.__sdram_partition.get_sdram_base_address_for(self)) spec.write_value(size) # Work out the offset into the data to write from, based on the # synapse type in use synapse_type = synapse_info.synapse_type offset = synapse_type * self.vertex_slice.n_atoms spec.write_value(offset) # If we are here, the connector must be one-to-one so create # the synapses and then store the scaled weights connections = self.__conn(synapse_info).create_synaptic_block( (), self.vertex_slice, synapse_type, synapse_info) weight_scales = ( next(iter(cast(AbstractEdgePartition, self.__sdram_partition).edges)) .post_vertex.weight_scales) weights = connections["weight"] * weight_scales[synapse_type] weights = numpy.rint(numpy.abs(weights)).astype(uint16) if len(weights) % 2 != 0: padding = numpy.array([0], dtype=uint16) weights = numpy.concatenate((weights, padding)) spec.write_array(weights.view(uint32)) # End-of-Spec: spec.end_specification()
def _write_poisson_rates(self, spec: DataSpecificationBase) -> None: """ Generate Rate data for Poisson spike sources. :param spec: the data specification writer """ spec.comment( f"\nWriting Rates for {self.vertex_slice.n_atoms} " "poisson sources:\n") n_atoms = self.vertex_slice.n_atoms n_rates = n_atoms * self._pop_vertex.max_n_rates spec.reserve_memory_region( region=self._PoissonSpikeSourceRegions.RATES_REGION, size=get_rates_bytes(n_atoms, n_rates), label='PoissonRates') # List starts with n_items, so start with 0. Use arrays to allow # numpy concatenation to work. data_items: List[Union[Sequence[int], numpy.ndarray]] = list() data_items.append([int(self.__rate_changed)]) data_items.append([0]) n_items = 0 data = self._pop_vertex.data ids = self.vertex_slice.get_raster_ids() for (start, stop, item) in data.iter_ranges_by_ids(ids): count = stop - start items = numpy.dstack( (_u3232_to_uint64(item['rates']), _u3232_to_uint64(item['starts']), _u3232_to_uint64(item['durations'])) )[0] data_items.extend([[count], [len(items)], [0], numpy.ravel(items).view(uint32)]) n_items += 1 data_items[1] = [n_items] data_to_write = numpy.concatenate(data_items) spec.reserve_memory_region( region=self._PoissonSpikeSourceRegions.EXPANDER_REGION, size=get_expander_rates_bytes(n_atoms, n_rates), label='Expander') spec.switch_write_focus( self._PoissonSpikeSourceRegions.EXPANDER_REGION) spec.write_array(data_to_write) self.__rate_changed = False def _write_poisson_parameters(self, spec: DataSpecificationBase) -> None: """ Generate Parameter data for Poisson spike sources. :param spec: the data specification writer """ spec.comment( f"\nWriting parameters for {self.vertex_slice.n_atoms} " "Poisson sources:\n") spec.reserve_memory_region( region=self._PoissonSpikeSourceRegions.POISSON_PARAMS_REGION, size=get_params_bytes(self.vertex_slice.n_atoms), label="PoissonParams") spec.switch_write_focus( self._PoissonSpikeSourceRegions.POISSON_PARAMS_REGION) # Write Key info for this core: routing_info = SpynnakerDataView.get_routing_infos() key = routing_info.get_single_key_from(self) keys: Union[Sequence[int], numpy.ndarray] if key is None: spec.write_value(0) keys = [0] * self.vertex_slice.n_atoms else: spec.write_value(1) keys = get_keys( key, self.vertex_slice, self._pop_vertex.n_colour_bits) # Write the incoming mask if there is one incoming_mask = 0 if self._pop_vertex.incoming_control_edge is not None: routing_info = SpynnakerDataView.get_routing_infos() r_info = routing_info.get_info_from( self._pop_vertex.incoming_control_edge.pre_vertex, LIVE_POISSON_CONTROL_PARTITION_ID) incoming_mask = ~r_info.mask & 0xFFFFFFFF spec.write_value(incoming_mask) # Write the number of seconds per timestep (unsigned long fract) spec.write_value( data=SpynnakerDataView.get_simulation_time_step_s(), data_type=DataType.U032) # Write the number of timesteps per ms (accum) spec.write_value( data=SpynnakerDataView.get_simulation_time_step_per_ms(), data_type=DataType.U1616) # Write the slow rate per tick cut-off (accum) spec.write_value( data=self.SLOW_RATE_PER_TICK_CUTOFF, data_type=DataType.U1616) # Write the fast rate per tick cut-off (accum) spec.write_value( data=self.FAST_RATE_PER_TICK_CUTOFF, data_type=DataType.U1616) # Write the lo_atom ID spec.write_value(data=self.vertex_slice.lo_atom) # Write the number of sources spec.write_value(data=self.vertex_slice.n_atoms) # Write the maximum spikes per tick spec.write_value(data=self.max_spikes_per_ts()) # Write the number of colour bits spec.write_value(data=self._pop_vertex.n_colour_bits) # Write the random seed (4 words), generated randomly! spec.write_array(self._pop_vertex.kiss_seed(self.vertex_slice)) spec.write_array(keys)
[docs] def set_rate_changed(self) -> None: """ Records that the rates have changed. """ self.__rate_changed = True
def __poisson_rate_region_address(self, placement: Placement) -> int: return helpful_functions.locate_memory_region_for_placement( placement, self._PoissonSpikeSourceRegions.RATES_REGION)
[docs] def read_parameters_from_machine(self, placement: Placement) -> None: """ Reads the poisson rates of the machine if they could have changed. :param placement: """ # It is only worth updating the rates when there is a control edge # that can change them if self._pop_vertex.incoming_control_edge is not None: # locate SDRAM address where the rates are stored poisson_rate_region_sdram_address = ( self.__poisson_rate_region_address(placement)) # get size of poisson params n_atoms = self.vertex_slice.n_atoms n_rates = n_atoms * self._pop_vertex.max_n_rates size_of_region = get_rates_bytes(n_atoms, n_rates) # get data from the machine byte_array = SpynnakerDataView.read_memory( placement.x, placement.y, poisson_rate_region_sdram_address, size_of_region) # For each atom, read the number of rates and the rate parameters offset = 0 for i in self.vertex_slice.get_raster_ids(): n_rates, = _ONE_WORD.unpack_from(byte_array, offset) # Skip the count and index offset += PARAMS_WORDS_PER_NEURON * BYTES_PER_WORD rates: List[float] = list() for _ in range(n_rates): rate_int = _ONE_WORD.unpack_from(byte_array, offset)[0] rates.append(rate_int / DataType.S1615.scale) # Skip the start and duration as they can't change offset += PARAMS_WORDS_PER_RATE * BYTES_PER_WORD self._pop_vertex.rates.set_value_by_id(i, numpy.array(rates))
[docs] def read_connections( self, synapse_info: SynapseInformation) -> ConnectionsArray: """ Read the connections from the machine. :param synapse_info: The synapse information being read :return: The connections read back """ size = self.vertex_slice.n_atoms * SDRAM_EDGE_PARAMS_BYTES_PER_WEIGHT placement = SpynnakerDataView().get_placement_of_vertex(self) addr = locate_memory_region_for_placement( placement, self._PoissonSpikeSourceRegions.SDRAM_EDGE_PARAMS) addr += SDRAM_EDGE_PARAMS_BASE_BYTES data = SpynnakerDataView().read_memory( placement.x, placement.y, addr, size) weights_encoded = numpy.frombuffer(data, dtype=uint16).astype( numpy.float64) weight_scales = ( next(iter(cast(AbstractEdgePartition, self.__sdram_partition).edges)) .post_vertex.weight_scales) weights = weights_encoded / weight_scales[synapse_info.synapse_type] connections = numpy.zeros( self.vertex_slice.n_atoms, dtype=NUMPY_CONNECTORS_DTYPE) connections["source"] = numpy.arange( self.vertex_slice.lo_atom, self.vertex_slice.hi_atom + 1) connections["target"] = numpy.arange( self.vertex_slice.lo_atom, self.vertex_slice.hi_atom + 1) connections["weight"] = weights connections["delay"] = \ SpynnakerDataView().get_simulation_time_step_ms() return connections
[docs] @overrides(SendsSynapticInputsOverSDRAM.sdram_requirement) def sdram_requirement(self, sdram_machine_edge: SDRAMMachineEdge) -> int: if isinstance(sdram_machine_edge.post_vertex, ReceivesSynapticInputsOverSDRAM): return sdram_machine_edge.post_vertex.n_bytes_for_transfer raise SynapticConfigurationException( f"Unknown post vertex type in edge {sdram_machine_edge}")
[docs] @overrides(MachineVertex.get_n_keys_for_partition) def get_n_keys_for_partition(self, partition_id: str) -> int: return self.vertex_slice.n_atoms << self._pop_vertex.n_colour_bits