Skip to content

Commit d97b394

Browse files
committed
Add image data representation
Introduce a new `data_representation/images/` module that lets detectors be represented as multi-channel 3D image tensors rather than graphs, as required by CNN backbones. Components: - `ImageRepresentation` base class — analogue of `GraphDefinition` for image-shaped inputs. Builds per-channel image tensors from raw pulse data using a `GridDefinition` that maps each DOM to a voxel. - `GridDefinition` — abstract mapping from DOM string/dom indices to a 3D grid. `IC86GridDefinition` covers the IceCube IC86 array (main array + upper/lower DeepCore), `ExamplePrometheusGridDefinition` covers the Prometheus example geometry. Mapping tables live in `cnn_mapping_tables.py` as Python literals (no parquet files). - `IC86Image` and `ExamplePrometheusImage` — concrete image representations bundling a detector and its grid definition. Also adds: - `TEST_IMAGE_DIR` and the three `TEST_IC86*_IMAGE` constants in `graphnet.constants` for the new test fixtures. - `.npy` test fixtures for the IC86 main array and the two DeepCore sub-arrays. - Unit tests for `GridDefinition` and `ImageRepresentation`. No model code yet — that lands in follow-up PRs (`IceCubeDNN`, `LCSC`). Split from graphnet-team#813.
1 parent 368927d commit d97b394

14 files changed

Lines changed: 1731 additions & 0 deletions
3.25 KB
Binary file not shown.
47 KB
Binary file not shown.
768 Bytes
Binary file not shown.

src/graphnet/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
TEST_PARQUET_DATA = os.path.join(
2222
TEST_DATA_DIR, "parquet", _test_dataset_name, "merged"
2323
)
24+
TEST_IMAGE_DIR = os.path.join(TEST_DATA_DIR, "images")
25+
TEST_IC86MAIN_IMAGE = os.path.join(TEST_IMAGE_DIR, "IC86main_array_test.npy")
26+
TEST_IC86LOWERDC_IMAGE = os.path.join(
27+
TEST_IMAGE_DIR, "IC86lower_deepcore_test.npy"
28+
)
29+
TEST_IC86UPPERDC_IMAGE = os.path.join(
30+
TEST_IMAGE_DIR, "IC86upper_deepcore_test.npy"
31+
)
2432

2533
# Example data
2634
EXAMPLE_DATA_DIR = os.path.join(DATA_DIR, "examples")

src/graphnet/models/data_representation/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@
1818
NodeAsDOMTimeSeries,
1919
IceMixNodes,
2020
)
21+
from .images import (
22+
ExamplePrometheusGridDefinition,
23+
ExamplePrometheusImage,
24+
GridDefinition,
25+
IC86GridDefinition,
26+
IC86Image,
27+
ImageRepresentation,
28+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""CNN images: ``ImageRepresentation`` + ``GridDefinition``."""
2+
3+
from .image_representation import ImageRepresentation
4+
from .images import IC86Image, ExamplePrometheusImage
5+
from .mappings import (
6+
ExamplePrometheusGridDefinition,
7+
GridDefinition,
8+
IC86GridDefinition,
9+
)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Image CNN input pipeline: pulses → pixels → grids / tensors."""
2+
3+
from typing import List, Optional, Dict, Tuple, Union, Any, Callable
4+
import torch
5+
import numpy as np
6+
from numpy.random import Generator
7+
8+
from graphnet.models.data_representation import DataRepresentation
9+
from graphnet.models.data_representation.graphs import NodeDefinition
10+
from torch_geometric.data import Data
11+
from .mappings import GridDefinition
12+
13+
14+
class ImageRepresentation(DataRepresentation):
15+
"""Compose a pixel definition with a detector grid for CNN inputs.
16+
17+
A :class:`~graphnet.models.data_representation.graphs.nodes.NodeDefinition`
18+
acts as **pixel definition**: pulses ``X`` are aggregated into unordered
19+
pixel rows ``P`` (the same abstraction as graph nodes, without requiring
20+
graph terminology for CNN users).
21+
22+
A :class:`GridDefinition` defines detector-bound orthonormal grid shape(s)
23+
and lookup table(s); its :meth:`~GridDefinition.forward` scatters ``P``
24+
into image tensor(s).
25+
26+
The :class:`~graphnet.models.detector.detector.Detector` is taken from
27+
``grid_definition.detector`` so the grid matches the preprocessing geometry.
28+
"""
29+
30+
def __init__(
31+
self,
32+
pixel_definition: NodeDefinition,
33+
grid_definition: GridDefinition,
34+
input_feature_names: Optional[List[str]] = None,
35+
dtype: Optional[torch.dtype] = torch.float,
36+
perturbation_dict: Optional[Dict[str, float]] = None,
37+
seed: Optional[Union[int, Generator]] = None,
38+
add_inactive_sensors: bool = False,
39+
sensor_mask: Optional[List[int]] = None,
40+
string_mask: Optional[List[int]] = None,
41+
):
42+
"""Construct `ImageRepresentation`.
43+
44+
Args:
45+
pixel_definition: Pulse-level features → one row per pixel/DOM.
46+
grid_definition: Pixel keys + voxel indices + scatter into images.
47+
input_feature_names: Column names in raw pulse tables. If omitted,
48+
the detector's feature list is used.
49+
dtype: Feature dtype (e.g. ``torch.float``).
50+
perturbation_dict: Optional feature noise (see ``DataRepresentation``).
51+
seed: RNG for perturbations.
52+
add_inactive_sensors: Pad inactive sensors when True.
53+
sensor_mask: Drop these sensor IDs.
54+
string_mask: Drop these string IDs.
55+
56+
Note:
57+
``pixel_definition`` output columns must match what
58+
``grid_definition`` expects (including key fields in
59+
:attr:`GridDefinition.map_pixels_by`).
60+
"""
61+
super().__init__(
62+
detector=grid_definition.detector,
63+
input_feature_names=input_feature_names,
64+
dtype=dtype,
65+
perturbation_dict=perturbation_dict,
66+
seed=seed,
67+
add_inactive_sensors=add_inactive_sensors,
68+
sensor_mask=sensor_mask,
69+
string_mask=string_mask,
70+
repeat_labels=False,
71+
)
72+
self._pixel_definition = pixel_definition
73+
self._grid_definition = grid_definition
74+
75+
@property
76+
def shape(self) -> List[List[int]]:
77+
"""Channel-spatial layout per image tensor (see ``GridDefinition``)."""
78+
return self._grid_definition.shape
79+
80+
def single_image_spatial_shape(self) -> Tuple[int, int, int]:
81+
"""Return spatial size as ``(height, width, depth)`` for one 3D image.
82+
83+
Raises:
84+
ValueError: If ``shape`` does not describe exactly one four-axis
85+
layout (channels plus three spatial axes).
86+
"""
87+
layouts = self.shape
88+
if len(layouts) != 1:
89+
raise ValueError(
90+
"Expected a single-image data representation (one shape "
91+
f"entry), got {len(layouts)}. For multi-image inputs, build "
92+
"the backbone explicitly for each tensor."
93+
)
94+
layout = layouts[0]
95+
if len(layout) != 4:
96+
raise ValueError(
97+
"Expected each image layout as "
98+
"[num_channels, height, width, depth]; "
99+
f"got {layout!r}."
100+
)
101+
return (layout[1], layout[2], layout[3])
102+
103+
def _set_output_feature_names(
104+
self, input_feature_names: List[str]
105+
) -> List[str]:
106+
"""Sync pixel columns and grid output names."""
107+
self._pixel_definition.set_output_feature_names(input_feature_names)
108+
self._grid_definition._set_image_feature_names(
109+
self._pixel_definition._output_feature_names
110+
)
111+
return self._grid_definition.image_feature_names
112+
113+
def forward( # type: ignore
114+
self,
115+
input_features: np.ndarray,
116+
input_feature_names: List[str],
117+
truth_dicts: Optional[List[Dict[str, Any]]] = None,
118+
custom_label_functions: Optional[Dict[str, Callable[..., Any]]] = None,
119+
loss_weight_column: Optional[str] = None,
120+
loss_weight: Optional[float] = None,
121+
loss_weight_default_value: Optional[float] = None,
122+
data_path: Optional[str] = None,
123+
) -> Data:
124+
"""Build a ``Data`` object with image tensor(s) on ``x``."""
125+
data = super().forward(
126+
input_features=input_features,
127+
input_feature_names=input_feature_names,
128+
truth_dicts=truth_dicts,
129+
custom_label_functions=custom_label_functions,
130+
loss_weight_column=loss_weight_column,
131+
loss_weight=loss_weight,
132+
loss_weight_default_value=loss_weight_default_value,
133+
data_path=data_path,
134+
)
135+
data.x = self._pixel_definition(data.x)
136+
data.x = data.x.type(self.dtype)
137+
data = self._grid_definition(data, self.output_feature_names)
138+
if not isinstance(data.x, list):
139+
data.x = [data.x]
140+
nb_nodes = []
141+
for i, x in enumerate(data.x):
142+
data.x[i] = x.type(self.dtype)
143+
nb_nodes.append(np.prod(list(data.x[i].size()[2:])))
144+
data.num_nodes = torch.tensor(np.sum(nb_nodes))
145+
return data
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Concrete :class:`ImageRepresentation` subclasses for common detectors."""
2+
3+
from typing import List, Optional, Any
4+
import torch
5+
6+
from graphnet.models.data_representation.graphs import NodeDefinition
7+
from graphnet.models.detector import Detector, IceCube86, ORCA150
8+
9+
from .image_representation import ImageRepresentation
10+
from .mappings import IC86GridDefinition, ExamplePrometheusGridDefinition
11+
12+
13+
class IC86Image(ImageRepresentation):
14+
"""IceCube-86 images (main array + optional DeepCore tensors)."""
15+
16+
def __init__(
17+
self,
18+
pixel_definition: NodeDefinition,
19+
input_feature_names: List[str],
20+
include_lower_dc: bool = True,
21+
include_upper_dc: bool = True,
22+
string_label: str = "string",
23+
dom_number_label: str = "dom_number",
24+
dtype: Optional[torch.dtype] = torch.float,
25+
detector: Optional[Detector] = None,
26+
**kwargs: Any,
27+
) -> None:
28+
"""Construct `IC86Image`.
29+
30+
Args:
31+
pixel_definition: Pulse → DOM row features (:class:`NodeDefinition`).
32+
input_feature_names: Raw input column names.
33+
include_lower_dc: Include lower DeepCore grid.
34+
include_upper_dc: Include upper DeepCore grid.
35+
string_label: DOM string column in pixel rows.
36+
dom_number_label: DOM index column in pixel rows.
37+
dtype: Tensor dtype for images.
38+
detector: ``IceCube86``; default standardizes all but coordinates.
39+
"""
40+
if detector is None:
41+
detector = IceCube86(
42+
replace_with_identity=input_feature_names,
43+
)
44+
else:
45+
assert isinstance(detector, IceCube86)
46+
pixel_definition.set_output_feature_names(input_feature_names)
47+
assert (
48+
string_label in input_feature_names
49+
), f"String label '{string_label}' not in input feature names"
50+
assert (
51+
dom_number_label in input_feature_names
52+
), f"DOM number label '{dom_number_label}' not in input feature names"
53+
54+
grid_definition = IC86GridDefinition(
55+
detector=detector,
56+
dtype=dtype,
57+
string_label=string_label,
58+
dom_number_label=dom_number_label,
59+
pixel_feature_names=pixel_definition._output_feature_names,
60+
include_lower_dc=include_lower_dc,
61+
include_upper_dc=include_upper_dc,
62+
)
63+
64+
super().__init__(
65+
pixel_definition=pixel_definition,
66+
grid_definition=grid_definition,
67+
input_feature_names=input_feature_names,
68+
add_inactive_sensors=False,
69+
**kwargs,
70+
)
71+
72+
73+
class ExamplePrometheusImage(ImageRepresentation):
74+
"""Example Prometheus-style single-image layout (tutorial scripts only)."""
75+
76+
def __init__(
77+
self,
78+
pixel_definition: NodeDefinition,
79+
input_feature_names: List[str],
80+
string_label: str = "sensor_string_id",
81+
dom_number_label: str = "sensor_id",
82+
dtype: Optional[torch.dtype] = torch.float,
83+
detector: Optional[Detector] = None,
84+
**kwargs: Any,
85+
) -> None:
86+
"""Construct `ExamplePrometheusImage`.
87+
88+
Args:
89+
pixel_definition: Pulse → sensor row features (:class:`NodeDefinition`).
90+
input_feature_names: Raw input column names.
91+
string_label: String id column in pixel rows.
92+
dom_number_label: Sensor id column (internal grid key name).
93+
dtype: Tensor dtype for images.
94+
detector: ``ORCA150`` by default.
95+
"""
96+
if detector is None:
97+
detector = ORCA150(
98+
replace_with_identity=input_feature_names,
99+
)
100+
101+
pixel_definition.set_output_feature_names(input_feature_names)
102+
assert (
103+
string_label in input_feature_names
104+
), f"String label '{string_label}' not in input feature names"
105+
assert (
106+
dom_number_label in input_feature_names
107+
), f"DOM number label '{dom_number_label}' not in input feature names"
108+
109+
grid_definition = ExamplePrometheusGridDefinition(
110+
detector=detector,
111+
dtype=dtype,
112+
string_label=string_label,
113+
sensor_number_label=dom_number_label,
114+
pixel_feature_names=pixel_definition._output_feature_names,
115+
)
116+
117+
super().__init__(
118+
pixel_definition=pixel_definition,
119+
grid_definition=grid_definition,
120+
input_feature_names=input_feature_names,
121+
add_inactive_sensors=False,
122+
**kwargs,
123+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Detector grids: shapes, lookup tables, scatter into image tensors."""
2+
3+
from .grid_definition import (
4+
ExamplePrometheusGridDefinition,
5+
GridDefinition,
6+
IC86GridDefinition,
7+
)

0 commit comments

Comments
 (0)