Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement distance to surface and add the surface types to HPGe object #51

Merged
merged 31 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
85e241d
implement distance to surface and add the surface types to HPGe object
tdixon97 Nov 1, 2024
4fc6513
remove duplicate code by switching p+ and groove construction to a se…
tdixon97 Nov 2, 2024
ca4fa2d
surface types for all detectors
tdixon97 Nov 2, 2024
653a39e
adding tests
tdixon97 Nov 2, 2024
9ea214e
some more tests
tdixon97 Nov 2, 2024
84624a3
split utils in two
tdixon97 Nov 2, 2024
ac8079a
add tests on the surface type lists
tdixon97 Nov 2, 2024
9c9119b
add a surface area calculation
tdixon97 Nov 2, 2024
51bf42a
small fix
tdixon97 Nov 2, 2024
6c36aff
allow to compute distnce to just some surface
tdixon97 Nov 2, 2024
e86e980
add method for distance from points to a surface + tests
tdixon97 Nov 3, 2024
031803f
option to plot profile splitting by surface type
tdixon97 Nov 4, 2024
da33c0d
update commit hashh to correct the fixed bege groove
tdixon97 Nov 4, 2024
f889fa8
remove unused function
tdixon97 Nov 4, 2024
dc17884
add a method too check if points are in the hpge
tdixon97 Nov 4, 2024
9f33649
Implement requests from review
tdixon97 Nov 4, 2024
2cbc99b
fix docstrings
tdixon97 Nov 4, 2024
593d9a2
fix typing
tdixon97 Nov 4, 2024
ccf870b
avoid recomputing r,z
tdixon97 Nov 4, 2024
6d49fe3
switch to jit compiled distance calculations
tdixon97 Nov 5, 2024
9533476
add a check on obj1 for non-polycones
tdixon97 Nov 5, 2024
e02a8cf
add numba to dependencies
tdixon97 Nov 5, 2024
27ba6ea
switch surface_area to return a list not the sum
tdixon97 Nov 11, 2024
e82ec1b
small fix to surface_indexing
tdixon97 Nov 11, 2024
3f95c8a
fix size of r0
tdixon97 Nov 11, 2024
1381e7c
remove undeed indexing of z
tdixon97 Nov 11, 2024
b609551
remove some very simple functions
tdixon97 Nov 13, 2024
34b6764
Some style fixes
gipert Nov 14, 2024
76cf8a1
chore: update pre-commit hooks (#52)
pre-commit-ci[bot] Nov 4, 2024
6a1037d
Docs: do not show argument types in function signature
gipert Nov 7, 2024
ad7b135
Improve type hints rendering in API docs
gipert Nov 14, 2024
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
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ci:

repos:
- repo: https://github.com/adamchainz/blacken-docs
rev: "1.18.0"
rev: "1.19.1"
hooks:
- id: blacken-docs
additional_dependencies: [black==23.*]
Expand Down Expand Up @@ -42,14 +42,14 @@ repos:
args: [--prose-wrap=always]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.6.9"
rev: "v0.7.2"
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.11.2"
rev: "v1.13.0"
hooks:
- id: mypy
files: src|tests
Expand All @@ -76,12 +76,12 @@ repos:
exclude: .pre-commit-config.yaml

- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.20.2
rev: v0.22
hooks:
- id: validate-pyproject

- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.3
rev: 0.29.4
hooks:
- id: check-dependabot
- id: check-github-workflows
Expand Down
7 changes: 6 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
# sphinx-autodoc
# Include __init__() docstring in class docstring
autoclass_content = "both"
autodoc_typehints = "both"
autodoc_typehints = "description"
autodoc_typehints_description_target = "documented_params"
autodoc_typehints_format = "short"

autodoc_type_aliases = {
"ArrayLike": "ArrayLike",
"NDArray": "NDArray",
}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"numpy",
"numba",
"pint != 0.24",
"pyg4ometry",
"pylegendmeta>=v0.9.0a2",
Expand Down
1 change: 1 addition & 0 deletions src/legendhpges/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"PPC",
"SemiCoax",
"make_hpge",
"utils",
"V07646A",
"P00664B",
"V02160A",
Expand Down
175 changes: 171 additions & 4 deletions src/legendhpges/base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from __future__ import annotations

import json
import logging
import math
from abc import ABC, abstractmethod
from pathlib import Path

import numpy as np
from legendmeta import AttrsDict
from numpy.typing import ArrayLike, NDArray
from pint import Quantity
from pyg4ometry import geant4

from . import utils
from .materials import make_natural_germanium
from .registry import default_g4_registry
from .registry import default_units_registry as u
Expand Down Expand Up @@ -63,23 +67,27 @@ def __init__(

self.registry = registry

self.surfaces = []

# build logical volume, default [mm]
super().__init__(self._g4_solid(), material, self.name, self.registry)

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.metadata})"

def _g4_solid(self) -> geant4.solid.SolidBase:
"""Build (by default) a :class:`pyg4ometry.solid.GenericPolycone` instance from the (r, z) information.
"""Build a :class:`pyg4ometry.solid.GenericPolycone` instance from the ``(r, z)`` information.

Returns
-------
g4_solid
A derived class of :class:`pyg4ometry.solid.SolidBase` to be used to construct the logical volume.
A derived class of :class:`pyg4ometry.solid.SolidBase` to be used
to construct the logical volume.

Note
----
Detectors with a special geometry can have this method overridden in their class definition.
Detectors with a special geometry can have this method overridden
in their class definition.
"""
# return ordered r,z lists, default unit [mm]
r, z = self._decode_polycone_coord()
Expand All @@ -98,13 +106,128 @@ def _decode_polycone_coord(self) -> tuple[list[float], list[float]]:
Returns
-------
(r, z)
two lists of r and z coordinates, respectively.
two lists of `r` and `z` coordinates, respectively.

Note
----
Must be overloaded by derived classes.
"""

def get_profile(self) -> tuple[list[float], list[float]]:
"""Get the profile of the HPGe detector.

Returns
-------
(r.z)
two lists of `r` and `z` coordinates, respectively.

Note
-----
For V02160A and P00664A the detector profile is that of the solid without cut.
"""
if not isinstance(self.solid, geant4.solid.GenericPolycone) and not isinstance(
self.solid.obj1, geant4.solid.GenericPolycone
):
msg = "solid is not a polycone and neither is its primary consistient (obj1), thus no profile can be computed."
raise ValueError(msg)
if not isinstance(self.solid, geant4.solid.GenericPolycone):
r = self.solid.obj1.pR
z = self.solid.obj1.pZ
else:
r = self.solid.pR
z = self.solid.pZ

return r, z

def is_inside(self, coords: ArrayLike, tol: float = 1e-11) -> NDArray[bool]:
"""Compute whether each point is inside the volume.

Parameters
----------
coords
2D array of shape `(n,3)` of `(x,y,z)` coordinates for each of `n`
points, second index corresponds to `(x,y,z)`.
tol
distance outside the surface which is considered inside. Should be
on the order of numerical precision of the floating point representation.
"""

if not isinstance(self.solid, geant4.solid.GenericPolycone):
msg = f"distance_to_surface is not implemented for {type(self.solid)} yet"
raise NotImplementedError(msg)

if not isinstance(coords, np.ndarray):
coords = np.array(coords)

if np.shape(coords)[1] != 3:
msg = "coords must be provided as a 2D array with x,y,z coordinates for each point."
raise ValueError(msg)

coords_rz = utils.convert_coords(coords)

# get the profile
r, z = self.get_profile()
s1, s2 = utils.get_line_segments(r, z, surface_indices=None)

# convert coords
coords_rz = utils.convert_coords(coords)

# compute shortest distances
dists = utils.shortest_distance(s1, s2, coords_rz, tol=tol)

# fnd the sign of the id with the lowest distance
ids = np.argmin(abs(dists), axis=1)
return np.where(dists[np.arange(dists.shape[0]), ids] > 0, True, False)

def distance_to_surface(
self,
coords: ArrayLike,
surface_indices: ArrayLike | None = None,
tol: float = 1e-11,
) -> NDArray:
"""Compute the distance of a set of points to the nearest detector surface.

Parameters
----------
coords
2D array of shape `(n,3)` of `(x,y,z)` coordinates for each of `n`
points, second index corresponds to `(x,y,z)`.
surface_indices
list of indices of surfaces to consider. If ``None`` (the default)
all surfaces used.
tol
distance outside the surface which is considered inside. Should be
on the order of numerical precision of the floating point representation.

Note
----
- Only implemented for solids based on :class:`geant4.solid.GenericPolycone`
- Coordinates should be relative to the origin of the polycone.
"""
# check type of the solid
if not isinstance(self.solid, geant4.solid.GenericPolycone):
msg = f"distance_to_surface is not implemented for {type(self.solid)} yet"
raise NotImplementedError(msg)

if not isinstance(coords, np.ndarray):
coords = np.array(coords)

if np.shape(coords)[1] != 3:
msg = "coords must be provided as a 2D array with x,y,z coordinates for each point."
raise ValueError(msg)

# get the coordinates
r, z = self.get_profile()
s1, s2 = utils.get_line_segments(r, z, surface_indices=surface_indices)

# convert coords
coords_rz = utils.convert_coords(coords)

# compute shortest distances to every surface
dists = utils.shortest_distance(s1, s2, coords_rz, tol=tol, signed=False)

return np.min(abs(dists), axis=1)

@property
def volume(self) -> Quantity:
"""Volume of the HPGe."""
Expand All @@ -124,3 +247,47 @@ def volume(self) -> Quantity:
def mass(self) -> Quantity:
"""Mass of the HPGe."""
return (self.volume * (self.material.density * u.g / u.cm**3)).to(u.g)

def surface_area(
self, surface_indices: ArrayLike | None = None
) -> NDArray[Quantity]:
"""Surface area of the HPGe.

If a list of surface_indices is provided the area is computed only
considering these surfaces, from :math:`r_i` to :math:`r_{i+1}` and
:math:`z_i` to :math:`z_{i+1}`. Else the full area is computed.

Parameters
----------
surface_indices
list of indices or ``None``.

Note
----
Calculation is based on a polycone geometry so is incorrect for
asymmetric detectors.
"""
if not isinstance(self.solid, geant4.solid.GenericPolycone):
logging.warning("The area is that of the solid without cut")

r, z = self.get_profile()

r = np.array(r)
z = np.array(z)

dr = np.array([r2 - r1 for r1, r2 in zip(r[:-1], r[1:])])
dz = np.array([z2 - z1 for z1, z2 in zip(z[:-1], z[1:])])
dl = np.sqrt(np.power(dr, 2) + np.power(dz, 2))
r0 = r[:-1]

if surface_indices is not None:
dr = dr[surface_indices]
dz = dz[surface_indices]
dl = dl[surface_indices]
r0 = r0[surface_indices]

return np.where(
dr == 0,
abs(dz) * r0 * 2 * np.pi * u.mm**2,
abs(dr) * dl * np.pi * u.mm**2,
)
28 changes: 13 additions & 15 deletions src/legendhpges/bege.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import math

from .base import HPGe
from .build_utils import make_pplus


class BEGe(HPGe):
Expand All @@ -16,22 +17,12 @@ def _tan(a):

r = []
z = []
surfaces = []

if c.pp_contact.depth_in_mm > 0:
r += [0, c.pp_contact.radius_in_mm, c.pp_contact.radius_in_mm]
z += [c.pp_contact.depth_in_mm, c.pp_contact.depth_in_mm, 0]
else:
r += [0]
z += [0]

r += [
c.groove.radius_in_mm.inner,
c.groove.radius_in_mm.inner,
c.groove.radius_in_mm.outer,
c.groove.radius_in_mm.outer,
]

z += [0, c.groove.depth_in_mm, c.groove.depth_in_mm, 0]
r_p, z_p, surface_p = make_pplus(c)
r += r_p
z += z_p
surfaces += surface_p

if c.taper.bottom.height_in_mm > 0:
r += [
Expand All @@ -40,9 +31,11 @@ def _tan(a):
c.radius_in_mm,
]
z += [0, c.taper.bottom.height_in_mm]
surfaces += ["nplus", "nplus"]
else:
r += [c.radius_in_mm]
z += [0]
surfaces += ["nplus"]

if c.taper.top.height_in_mm > 0:
r += [
Expand All @@ -51,11 +44,16 @@ def _tan(a):
- c.taper.top.height_in_mm * _tan(c.taper.top.angle_in_deg),
]
z += [c.height_in_mm - c.taper.top.height_in_mm, c.height_in_mm]
surfaces += ["nplus", "nplus"]
else:
r += [c.radius_in_mm]
z += [c.height_in_mm]
surfaces += ["nplus"]

r += [0]
z += [c.height_in_mm]
surfaces += ["nplus"]

self.surfaces = surfaces

return r, z
Loading
Loading