Quickstart ========== Installation ------------ Before using the package, install it via pip: .. code-block:: bash pip install pelican-lite Alternatively, if you're developing locally: .. code-block:: bash git clone https://github.com/heidelberg-hepml/pelican-lite.git cd pelican pip install -e ".[dev]" pre-commit install 0. Generate particle data ------------------------- We start by generating toy particle data, for instance for an amplitude regression task. We describe particles by a four-momentum and one scalar feature, for instance the particle type. Using random numbers, we generate a batch of 128 events with 10 particles each. .. code-block:: python import torch num_scalars = 1 B, N = 128, 10 mass = 1 p3 = torch.randn(B, N, 3) E = (mass**2 + (p3**2).sum(dim=-1, keepdims=True)).sqrt() fourmomenta = torch.cat([E, p3], dim=-1) # (128, 10, 4) scalars = torch.randn(B, N, num_scalars) # (128, 10, 1) 1. Represent particle data as a sparse tensor --------------------------------------------- At the core of this PELICAN implementation is the representation of sparse tensors. Instead of relying on zero-padding, this approach flattens the batch and node dimensions into a common node-across-batch dimension. The ``ptr`` or ``batch`` objects carry the information of which batch a node belongs to. In addition the ``edge_index`` allows us to operate on arbitrary graphs, instead of only fully connected graphs. Note that no zero-padding is required for the toy data that we are using in this demo. .. code-block:: python from pelican.utils import get_batch_from_ptr, get_edge_index_from_ptr, get_edge_index_from_shape scalars_sparse = scalars.flatten(end_dim=-2) fourmomenta_sparse = fourmomenta.flatten(end_dim=-2) print(scalars_sparse.shape, fourmomenta_sparse.shape) # (1280, 1) (1280, 4) # approach 1 (assumes dense tensors) edge_index, batch = get_edge_index_from_shape(fourmomenta.shape, remove_self_loops=False) print(edge_index.shape, batch.shape) # (2, 12800) (1280,) # approach 2 (start from generic ptr) ptr = torch.arange(B+1) * N batch_2 = get_batch_from_ptr(ptr) edge_index_2 = get_edge_index_from_ptr(ptr, fourmomenta_sparse.shape, remove_self_loops=False) # consistency checks assert torch.all(batch == batch_2) assert torch.all(edge_index == edge_index_2) 2. Organize PELICAN inputs into ranks ------------------------------------- PELICAN assumes Lorentz-invariant inputs and processes them with permutation-equivariant operations. To this end, the inputs to the architecture have to be organized by their transformation behaviour under permutations. There is graph-level information (rank 0), node-level information (rank 1), and edge-level information (rank 2). Extending this framework to higher-order representations is straight-forward, however those are expected to be less relevant in high-energy physics applications. In our case, we first have the particle-wise scalar information as rank 1 inputs. To turn the Lorentz vectors :math:`p_i^\mu` into Lorentz-invariants, we can take their inner products :math:`p_i^\mu p_{j,\mu}`, or equivalently :math:`(p_i+p_j)^\mu (p_i+p_j)_\mu`, as rank 2 or edge-level information. Note that the particle masses :math:`m_i^2 = p_i^\mu p_{i,\mu}` are node-level invariants, but emerge from the set of rank 2 objects. There are no rank 0 objects in this example, but they are also supported in the PELICAN architecture. .. code-block:: python from pelican.utils import get_edge_attr in_rank1 = scalars_sparse in_rank2 = get_edge_attr(fourmomenta_sparse, edge_index).unsqueeze(-1) print(in_rank1.shape, in_rank2.shape) # (1280, 1) (12800, 1) 3. Process inputs with PELICAN network -------------------------------------- We are now ready to process our data with a PELICAN network. It first projects all input data into rank 2 representations, processes them in that representation, and finally maps them back to outputs of rank 0, 1, or 2. .. code-block:: python from pelican.nets import PELICAN for out_rank in range(3): net = PELICAN( in_channels_rank2=1, in_channels_rank1=num_scalars, in_channels_rank0=0, out_rank=out_rank, out_channels=1, num_blocks=2, hidden_channels=16, ) out = net(edge_index, batch, in_rank2=in_rank2, in_rank1=in_rank1) print(out_rank, out.shape) # (0, (128, 1)) (1, (1280, 1)) (2, (12800, 1))