Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
550 changes: 550 additions & 0 deletions Examples/Scripts/Python/display_tracking_geometry.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm maybe this is too early, but I would find it nice to split geometry-specific and generic parts.
One could e.g. have a function/class in a non-executable script takes the subsystem-volume-id dicts and then separate the ODD / generic detector specific scripts in one or two dedicated scripts

E.g.,
Python/Utilities/display_tracking_geometry.py
Examples/Scripts/Python/display_odd.py
Examples/Scripts/Python/display_generic.py

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions Python/Core/src/Surfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

#include "Acts/Geometry/GeometryContext.hpp"
#include "Acts/Geometry/GeometryHierarchyMap.hpp"
#include "Acts/Geometry/Polyhedron.hpp"
#include "Acts/Material/ISurfaceMaterial.hpp"
#include "Acts/Surfaces/AnnulusBounds.hpp"
#include "Acts/Surfaces/BoundaryTolerance.hpp"
Expand Down Expand Up @@ -42,6 +44,14 @@ namespace ActsPython {
// This adds the definitions from Core/Surfaces to the python module
/// @param m is the pybind11 core module
void addSurfaces(py::module_& m) {
{
py::class_<Polyhedron>(m, "Polyhedron")
.def_readonly("vertices", &Polyhedron::vertices)
.def_readonly("faces", &Polyhedron::faces)
.def_readonly("triangularMesh", &Polyhedron::triangularMesh)
.def_readonly("exact", &Polyhedron::exact);
}

{
py::class_<BoundaryTolerance>(m, "BoundaryTolerance")
.def_static("infinite", &BoundaryTolerance::Infinite)
Expand Down Expand Up @@ -328,6 +338,13 @@ void addSurfaces(py::module_& m) {
.def_property_readonly("thickness", &Surface::thickness)
.def_property_readonly("isSensitive", &Surface::isSensitive)
.def_property_readonly("isAlignable", &Surface::isAlignable)
.def(
"polyhedronRepresentation",
[](const Surface& self, const GeometryContext& gctx,
unsigned int quarterSegments) {
return self.polyhedronRepresentation(gctx, quarterSegments);
},
py::arg("geometryContext"), py::arg("quarterSegments") = 2)
.def("visualize", &Surface::visualize)
.def_property_readonly("surfaceMaterial",
&Surface::surfaceMaterialSharedPtr)
Expand Down
26 changes: 26 additions & 0 deletions Python/Utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
set(_python_dir "${CMAKE_BINARY_DIR}/python")
set(_utilities_python_install_dir "${_acts_python_install_dir}/utilities")

set(_utilities_py_files
__init__.py
geometry_identifier.py
projection2d.py
tracking_surfaces.py
)

foreach(f ${_utilities_py_files})
set(_target ${_python_dir}/acts/utilities/${f})
get_filename_component(_dir ${_target} DIRECTORY)
file(MAKE_DIRECTORY ${_dir})

file(
CREATE_LINK ${CMAKE_CURRENT_SOURCE_DIR}/python/${f} ${_target}
SYMBOLIC
)

install(
FILES ${CMAKE_CURRENT_SOURCE_DIR}/python/${f}
DESTINATION ${_utilities_python_install_dir}
)
endforeach()

acts_add_library(PythonUtilities src/WhiteBoardRegistry.cpp)

target_include_directories(
Expand Down
35 changes: 35 additions & 0 deletions Python/Utilities/python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Pure-Python helpers shipped with Acts (geometry views, utilities)."""

from .geometry_identifier import GeometryIdentifierMasks
from .projection2d import (
auto_axis_limits,
project_polyhedron_vertices,
surface_to_2d_patches,
)
from .tracking_surfaces import (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More a style question, do we want to import all these functions into the acts.utilities module, or wouldn't it be clearer to have acts.utilities.projection2d.function?

collect_material_surfaces,
collect_sensitive_surfaces,
filter_surfaces_by_layer_ids,
filter_surfaces_by_volume_ids,
group_surfaces_by_volume_layer,
parse_layer_selection_spec,
parse_subsystem_region_spec,
sort_surfaces_by_geometry_id,
volume_ids_for_selection,
)

__all__ = [
"GeometryIdentifierMasks",
"auto_axis_limits",
"collect_material_surfaces",
"collect_sensitive_surfaces",
"filter_surfaces_by_layer_ids",
"filter_surfaces_by_volume_ids",
"group_surfaces_by_volume_layer",
"parse_layer_selection_spec",
"parse_subsystem_region_spec",
"project_polyhedron_vertices",
"sort_surfaces_by_geometry_id",
"surface_to_2d_patches",
"volume_ids_for_selection",
]
44 changes: 44 additions & 0 deletions Python/Utilities/python/geometry_identifier.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm not sure, don't we have this in C++, and can bind the geometry identifier? Do we need a reimplementation in Python?

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""GeometryIdentifier bit layout (see ``Acts::GeometryIdentifier`` in C++)."""

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class GeometryIdentifierMasks:
"""Masks and shifts match ``Core/include/Acts/Geometry/GeometryIdentifier.hpp``."""

volume_mask: int = 0xFF00000000000000
boundary_mask: int = 0x00FF000000000000
layer_mask: int = 0x0000FFF000000000
approach_mask: int = 0x0000000FF0000000
sensitive_mask: int = 0x000000000FFFFF00
extra_mask: int = 0x00000000000000FF

@property
def volume_shift(self) -> int:
return (self.volume_mask & -self.volume_mask).bit_length() - 1

@property
def boundary_shift(self) -> int:
return (self.boundary_mask & -self.boundary_mask).bit_length() - 1

@property
def layer_shift(self) -> int:
return (self.layer_mask & -self.layer_mask).bit_length() - 1

@property
def approach_shift(self) -> int:
return (self.approach_mask & -self.approach_mask).bit_length() - 1

@property
def sensitive_shift(self) -> int:
return (self.sensitive_mask & -self.sensitive_mask).bit_length() - 1

@property
def extra_shift(self) -> int:
return (self.extra_mask & -self.extra_mask).bit_length() - 1


DEFAULT_MASKS = GeometryIdentifierMasks()
103 changes: 103 additions & 0 deletions Python/Utilities/python/projection2d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""2D projections of surface polyhedra (xy, zr, zphi)."""

from __future__ import annotations

import math
from typing import Iterable, List, Literal, Sequence, Tuple

import numpy as np

ViewName = Literal["xy", "zr", "zphi"]


def project_point(
view: ViewName,
x: float,
y: float,
z: float,
) -> Tuple[float, float]:
if view == "xy":
return x, y
if view == "zr":
r = math.hypot(x, y)
return z, r
if view == "zphi":
phi = math.atan2(y, x)
return z, phi
raise ValueError(view)


def project_polyhedron_vertices(
view: ViewName,
vertices,
) -> np.ndarray:
"""Project ACTS ``Polyhedron.vertices`` (Vector3 sequence) to Nx2 float array."""

pts = np.zeros((len(vertices), 2))
for i, v in enumerate(vertices):
pts[i] = project_point(view, float(v[0]), float(v[1]), float(v[2]))
return pts


def _dedupe_loop(idxs: Sequence[int]) -> List[int]:
if not idxs:
return []
out = list(idxs)
if out[0] == out[-1]:
out = out[:-1]
return out


def surface_to_2d_patches(
view: ViewName,
polyhedron,
*,
prefer_triangles: bool = False,
) -> List[np.ndarray]:
"""Return closed 2D polygons (each Kx2) for matplotlib.

By default uses the native ``Polyhedron.faces`` (polygon mesh). With
``prefer_triangles=True``, uses ``triangularMesh`` when non-empty.
"""

if prefer_triangles and polyhedron.triangularMesh:
faces_source = polyhedron.triangularMesh
else:
faces_source = polyhedron.faces
patches: List[np.ndarray] = []
verts3 = polyhedron.vertices

for face in faces_source:
idxs = _dedupe_loop(face)
ring = np.zeros((len(idxs), 2))
for j, k in enumerate(idxs):
v = verts3[k]
ring[j] = project_point(view, float(v[0]), float(v[1]), float(v[2]))
if len(ring) >= 3:
patches.append(ring)
return patches


def auto_axis_limits(
polygons: Iterable[np.ndarray],
*,
margin_ratio: float = 0.05,
) -> Tuple[float, float, float, float]:
"""Return xmin, ymin, xmax, ymax over all polygon vertices."""

xs: List[float] = []
ys: List[float] = []
for poly in polygons:
if poly.size == 0:
continue
xs.extend(poly[:, 0].tolist())
ys.extend(poly[:, 1].tolist())
if not xs:
return -1.0, -1.0, 1.0, 1.0
xmin, xmax = min(xs), max(xs)
ymin, ymax = min(ys), max(ys)
dx = xmax - xmin if xmax > xmin else 1.0
dy = ymax - ymin if ymax > ymin else 1.0
mx = margin_ratio * dx
my = margin_ratio * dy
return xmin - mx, ymin - my, xmax + mx, ymax + my
Loading
Loading