diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 0837e710daeb293a95758cb89fa40f356eff1ca3..1f4b7b2be9e1220e78f0cc2ff7d2f91236bc34a2 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -42,7 +42,7 @@ dd4hep_configure_output()
 
 #==========================================================================
 
-SET(DD4HEP_EXAMPLES "AlignDet CLICSiD ClientTests Conditions DDCMS DDCodex DDDigi DDG4 DDG4_MySensDet LHeD OpticalSurfaces Persistency DDCAD SimpleDetector"
+SET(DD4HEP_EXAMPLES "AlignDet CLICSiD ClientTests Conditions DDCMS DDCodex DDDigi DDG4 DDG4_MySensDet LHeD OpticalSurfaces OpticalTracker Persistency DDCAD SimpleDetector"
   CACHE STRING "List of DD4hep Examples to build")
 SEPARATE_ARGUMENTS(DD4HEP_EXAMPLES)
 
diff --git a/examples/OpticalTracker/.gitignore b/examples/OpticalTracker/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ad9a4fb9702f225a233850321259fef29eaf4a23
--- /dev/null
+++ b/examples/OpticalTracker/.gitignore
@@ -0,0 +1,2 @@
+install
+*.root
diff --git a/examples/OpticalTracker/CMakeLists.txt b/examples/OpticalTracker/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7c5b04ebe38f31554b4d90081eeb2637a2d7852c
--- /dev/null
+++ b/examples/OpticalTracker/CMakeLists.txt
@@ -0,0 +1,59 @@
+#==========================================================================
+#  AIDA Detector description implementation 
+#--------------------------------------------------------------------------
+# Copyright (C) Organisation europeenne pour la Recherche nucleaire (CERN)
+# All rights reserved.
+#
+# For the licensing terms see $DD4hepINSTALL/LICENSE.
+# For the list of contributors see $DD4hepINSTALL/doc/CREDITS.
+#
+#==========================================================================
+cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
+
+IF(NOT TARGET DD4hep::DDCore)
+  find_package ( DD4hep REQUIRED )
+  include ( ${DD4hep_DIR}/cmake/DD4hep.cmake )
+  include ( ${DD4hep_DIR}/cmake/DD4hepBuild.cmake )
+  dd4hep_configure_output()
+ENDIF()
+
+dd4hep_set_compiler_flags()
+dd4hep_use_python_executable()
+
+#==========================================================================
+dd4hep_print("|++> OpticalTracker: ROOT version: ${ROOT_VERSION}")
+
+if(NOT ${ROOT_VERSION} VERSION_GREATER_EQUAL 6.18.00)
+  dd4hep_print("|++> Not building OpticalTracker test")
+  return()
+endif()
+dd4hep_print("|++> Building OpticalTracker test")
+
+#--------------------------------------------------------------------------
+dd4hep_configure_output()
+
+set(OpticalTracker_INSTALL ${CMAKE_INSTALL_PREFIX}/examples/OpticalTracker)
+dd4hep_add_plugin(OpticalTrackerExample SOURCES src/*.cpp
+  USES DD4hep::DDCore DD4hep::DDCond ROOT::Core ROOT::Geom ROOT::GenVector ROOT::MathCore)
+install(TARGETS OpticalTrackerExample LIBRARY DESTINATION lib)
+install(DIRECTORY compact scripts DESTINATION ${OpticalTracker_INSTALL} )
+dd4hep_configure_scripts( OpticalTracker DEFAULT_SETUP WITH_TESTS)
+
+# ---Test: run simulation
+dd4hep_add_test_reg( OpticalTracker_simulation
+  COMMAND    "${CMAKE_INSTALL_PREFIX}/bin/run_test_OpticalTracker.sh"
+  EXEC_ARGS  ${Python_EXECUTABLE} ${OpticalTracker_INSTALL}/scripts/richsim.py
+             --outputFile "${OpticalTracker_INSTALL}/sim.root"
+  REGEX_PASS "TEST: passed"
+  REGEX_FAIL "Exception;EXCEPTION;ERROR;Error;FATAL"
+  )
+
+# ---Test: Number of raw photon hits
+dd4hep_add_test_reg( OpticalTracker_number_of_hits
+  COMMAND    "${CMAKE_INSTALL_PREFIX}/bin/run_test_OpticalTracker.sh"
+  EXEC_ARGS  root.exe -b -x -n -q -l
+             "${OpticalTracker_INSTALL}/scripts/test_number_of_hits.C(\"${OpticalTracker_INSTALL}/sim.root\")"
+  REGEX_PASS "TEST: passed"
+  REGEX_FAIL "TEST: failed"
+  DEPENDS OpticalTracker_simulation
+  )
diff --git a/examples/OpticalTracker/README.md b/examples/OpticalTracker/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c07b25ab5c0e4051b2d8f0e49c0e50a2f6e61bab
--- /dev/null
+++ b/examples/OpticalTracker/README.md
@@ -0,0 +1,55 @@
+Proximity Focusing RICH
+=======================
+
+Example RICH demonstrating `Geant4OpticalTracker` Sensitive Detector plugin usage.
+
+This detector design has been pulled from [EPIC](https://github.com/eic/epic), and was originally developed in
+[ATHENA](https://eicweb.phy.anl.gov/EIC/detectors/athena), for the Electron-Ion Collider.
+
+![PFRICH](doc/geometry.png)
+
+For testing, it is recommended to follow `../README.md` and use `ctest`. See below for guidance for
+running this example standalone.
+
+To use `ctest`, run:
+```bash
+cd ..  # `pwd` should now be `DD4hep/examples`
+mkdir build
+cd build
+cmake -DDD4HEP_EXAMPLES="OpticalTracker" .. && make && make install
+ctest --output-on-failure   # or use `--verbose` to see all output
+```
+
+## Local Development
+If you want to run this example standalone, without needing to run `ctest`,
+make a standalone build. The following assumes that your current working
+directory is `DD4hep/examples/OpticalTracker`.
+
+Build with `cmake`, for example:
+```bash
+cmake -B build -S . -D CMAKE_INSTALL_PREFIX=install
+cmake --build build -- install
+```
+
+Run a test simulation:
+```bash
+install/bin/run_test_OpticalTracker.sh \
+  python install/examples/OpticalTracker/scripts/richsim.py
+```
+- See default settings in `scripts/richsim.py`
+- Override these settings, or add additional settings by appending `ddsim` options
+
+Draw the hits, to show Cherenkov photon rings:
+```bash
+root sim.root
+```
+```cpp
+// In ROOT interpreter:
+EVENT->Draw("PFRICHHits.position.Y():PFRICHHits.position.X()"); // hit positions
+EVENT->Draw("@PFRICHHits.size()");                              // raw number of hits per event
+```
+
+Test for the expected number of hits:
+```bash
+root -b -q -l scripts/test_number_of_hits.C
+```
diff --git a/examples/OpticalTracker/compact/materials.xml b/examples/OpticalTracker/compact/materials.xml
new file mode 100644
index 0000000000000000000000000000000000000000..97f37c61d6df622478bdd98d318ce6b38e8c43ad
--- /dev/null
+++ b/examples/OpticalTracker/compact/materials.xml
@@ -0,0 +1,380 @@
+<lccdd>
+  <properties>
+    <matrix name="RINDEX__Vacuum" coldim="2" values="
+      1.0*eV 1.0
+      5.1*eV 1.0
+      "/>
+    <matrix name="RINDEX__Air" coldim="2" values="
+      1.0*eV 1.00029
+      5.1*eV 1.00029
+      "/>
+    <!-- PFRICH property tables from https://github.com/cisbani/dRICh/blob/main/share/source/g4dRIChOptics.hh -->
+    <matrix name="RINDEX__C4F10_PFRICH" coldim="2" values="
+      1.7712*eV   1.0013
+      1.92389*eV  1.0013
+      2.10539*eV  1.00131
+      2.3247*eV   1.00131
+      2.59502*eV  1.00132
+      2.93647*eV  1.00133
+      3.38139*eV  1.00134
+      3.98521*eV  1.00136
+      4.85156*eV  1.0014
+      6.19921*eV  1.00149
+      "/>
+    <matrix name="ABSLENGTH__C4F10_PFRICH" coldim="2" values="
+      1.7712*eV   6.0*m
+      1.92389*eV  6.0*m
+      2.10539*eV  6.0*m
+      2.3247*eV   6.0*m
+      2.59502*eV  6.0*m
+      2.93647*eV  6.0*m
+      3.38139*eV  6.0*m
+      3.98521*eV  6.0*m
+      4.85156*eV  6.0*m
+      6.19921*eV  6.0*m
+      "/>
+    <matrix name="RINDEX__Aerogel_PFRICH" coldim="2" values="
+      1.87855*eV  1.01852
+      1.96673*eV  1.01856
+      2.05490*eV  1.01861
+      2.14308*eV  1.01866
+      2.23126*eV  1.01871
+      2.31943*eV  1.01876
+      2.40761*eV  1.01881
+      2.49579*eV  1.01887
+      2.58396*eV  1.01893
+      2.67214*eV  1.01899
+      2.76032*eV  1.01905
+      2.84849*eV  1.01912
+      2.93667*eV  1.01919
+      3.02485*eV  1.01926
+      3.11302*eV  1.01933
+      3.20120*eV  1.01941
+      3.28938*eV  1.01948
+      3.37755*eV  1.01956
+      3.46573*eV  1.01965
+      3.55391*eV  1.01973
+      3.64208*eV  1.01982
+      3.73026*eV  1.01991
+      3.81844*eV  1.02001
+      3.90661*eV  1.02010
+      3.99479*eV  1.02020
+      4.08297*eV  1.02030
+      4.17114*eV  1.02041
+      4.25932*eV  1.02052
+      4.34750*eV  1.02063
+      4.43567*eV  1.02074
+      4.52385*eV  1.02086
+      4.61203*eV  1.02098
+      4.70020*eV  1.02111
+      4.78838*eV  1.02123
+      4.87656*eV  1.02136
+      4.96473*eV  1.02150
+      5.05291*eV  1.02164
+      5.14109*eV  1.02178
+      5.22927*eV  1.02193
+      5.31744*eV  1.02208
+      5.40562*eV  1.02223
+      5.49380*eV  1.02239
+      5.58197*eV  1.02255
+      5.67015*eV  1.02271
+      5.75833*eV  1.02288
+      5.84650*eV  1.02306
+      5.93468*eV  1.02324
+      6.02286*eV  1.02342
+      6.11103*eV  1.02361
+      6.19921*eV  1.02381
+      "/>
+    <matrix name="ABSLENGTH__Aerogel_PFRICH" coldim="2" values="
+      1.87855*eV  140.000*mm
+      1.96673*eV  141.973*mm
+      2.05490*eV  143.776*mm
+      2.14308*eV  145.431*mm
+      2.23126*eV  146.955*mm
+      2.31943*eV  148.364*mm
+      2.40761*eV  149.669*mm
+      2.49579*eV  150.882*mm
+      2.58396*eV  152.012*mm
+      2.67214*eV  153.067*mm
+      2.76032*eV  154.055*mm
+      2.84849*eV  154.982*mm
+      2.93667*eV  155.854*mm
+      3.02485*eV  156.674*mm
+      3.11302*eV  157.448*mm
+      3.20120*eV  158.180*mm
+      3.28938*eV  158.872*mm
+      3.37755*eV  159.528*mm
+      3.46573*eV  160.150*mm
+      3.55391*eV  160.742*mm
+      3.64208*eV  147.916*mm
+      3.73026*eV  128.139*mm
+      3.81844*eV  111.378*mm
+      3.90661*eV   97.121*mm
+      3.99479*eV   84.948*mm
+      4.08297*eV   74.518*mm
+      4.17114*eV   65.552*mm
+      4.25932*eV   57.819*mm
+      4.34750*eV   51.130*mm
+      4.43567*eV   45.327*mm
+      4.52385*eV   40.278*mm
+      4.61203*eV   35.873*mm
+      4.70020*eV   32.019*mm
+      4.78838*eV   28.641*mm
+      4.87656*eV   25.670*mm
+      4.96473*eV   23.054*mm
+      5.05291*eV   20.742*mm
+      5.14109*eV   18.698*mm
+      5.22927*eV   16.884*mm
+      5.31744*eV   15.272*mm
+      5.40562*eV   13.837*mm
+      5.49380*eV   12.557*mm
+      5.58197*eV   11.413*mm
+      5.67015*eV   10.389*mm
+      5.75833*eV    9.470*mm
+      5.84650*eV    8.645*mm
+      5.93468*eV    7.902*mm
+      6.02286*eV    7.233*mm
+      6.11103*eV    6.629*mm
+      6.19921*eV    6.082*mm
+      "/>
+    <matrix name="RAYLEIGH__Aerogel_PFRICH" coldim="2" values="
+      1.87855*eV  281.107*mm
+      1.96673*eV  233.984*mm
+      2.05490*eV  196.334*mm
+      2.14308*eV  165.962*mm
+      2.23126*eV  141.242*mm
+      2.31943*eV  120.958*mm
+      2.40761*eV  104.188*mm
+      2.49579*eV   90.226*mm
+      2.58396*eV   78.527*mm
+      2.67214*eV   68.663*mm
+      2.76032*eV   60.301*mm
+      2.84849*eV   53.174*mm
+      2.93667*eV   47.070*mm
+      3.02485*eV   41.816*mm
+      3.11302*eV   37.277*mm
+      3.20120*eV   33.336*mm
+      3.28938*eV   29.903*mm
+      3.37755*eV   26.900*mm
+      3.46573*eV   24.265*mm
+      3.55391*eV   21.946*mm
+      3.64208*eV   19.896*mm
+      3.73026*eV   18.080*mm
+      3.81844*eV   16.468*mm
+      3.90661*eV   15.030*mm
+      3.99479*eV   13.746*mm
+      4.08297*eV   12.596*mm
+      4.17114*eV   11.564*mm
+      4.25932*eV   10.637*mm
+      4.34750*eV    9.799*mm
+      4.43567*eV    9.043*mm
+      4.52385*eV    8.358*mm
+      4.61203*eV    7.738*mm
+      4.70020*eV    7.172*mm
+      4.78838*eV    6.659*mm
+      4.87656*eV    6.191*mm
+      4.96473*eV    5.762*mm
+      5.05291*eV    5.370*mm
+      5.14109*eV    5.011*mm
+      5.22927*eV    4.681*mm
+      5.31744*eV    4.379*mm
+      5.40562*eV    4.100*mm
+      5.49380*eV    3.844*mm
+      5.58197*eV    3.606*mm
+      5.67015*eV    3.386*mm
+      5.75833*eV    3.184*mm
+      5.84650*eV    2.996*mm
+      5.93468*eV    2.822*mm
+      6.02286*eV    2.660*mm
+      6.11103*eV    2.510*mm
+      6.19921*eV    2.370*mm
+      "/>
+    <matrix name="RINDEX__Acrylic_PFRICH" coldim="2" values="
+      4.13281*eV  1.5017
+      4.22099*eV  1.5017
+      4.30916*eV  1.5017
+      4.39734*eV  1.5017
+      4.48552*eV  1.5017
+      4.57369*eV  1.5017
+      4.66187*eV  1.5017
+      4.75005*eV  1.5017
+      4.83822*eV  1.5017
+      4.9264*eV   1.5017
+      5.01458*eV  1.5017
+      5.10275*eV  1.5017
+      5.19093*eV  1.5017
+      5.27911*eV  1.5017
+      5.36728*eV  1.5017
+      5.45546*eV  1.5017
+      5.54364*eV  1.5017
+      5.63181*eV  1.5017
+      5.71999*eV  1.5017
+      5.80817*eV  1.5017
+      5.89634*eV  1.5017
+      5.98452*eV  1.5017
+      6.0727*eV   1.5017
+      6.16087*eV  1.5017
+      6.24905*eV  1.5017
+      6.33723*eV  1.5017
+      6.4254*eV   1.5017
+      6.51358*eV  1.5017
+      6.60176*eV  1.5017
+      6.68993*eV  1.5017
+      6.77811*eV  1.5017
+      6.86629*eV  1.5017
+      6.95446*eV  1.5017
+      7.04264*eV  1.5017
+      7.13082*eV  1.5017
+      7.21899*eV  1.5017
+      7.30717*eV  1.5017
+      7.39535*eV  1.5017
+      7.48353*eV  1.5017
+      7.5717*eV   1.5017
+      7.65988*eV  1.5017
+      7.74806*eV  1.5017
+      7.83623*eV  1.5017
+      7.92441*eV  1.5017
+      8.01259*eV  1.5017
+      8.10076*eV  1.5017
+      8.18894*eV  1.5017
+      8.27712*eV  1.5017
+      8.36529*eV  1.5017
+      8.45347*eV  1.5017
+      "/>
+    <matrix name="ABSLENGTH__Acrylic_PFRICH" coldim="2" values="
+      4.13281*eV  82.0704*mm
+      4.22099*eV  36.9138*mm
+      4.30916*eV  13.3325*mm
+      4.39734*eV  5.03627*mm
+      4.48552*eV  2.3393*mm
+      4.57369*eV  1.36177*mm
+      4.66187*eV  0.933192*mm
+      4.75005*eV  0.708268*mm
+      4.83822*eV  0.573082*mm
+      4.9264*eV   0.483641*mm
+      5.01458*eV  0.420282*mm
+      5.10275*eV  0.373102*mm
+      5.19093*eV  0.33662*mm
+      5.27911*eV  0.307572*mm
+      5.36728*eV  0.283902*mm
+      5.45546*eV  0.264235*mm
+      5.54364*eV  0.247641*mm
+      5.63181*eV  0.233453*mm
+      5.71999*eV  0.221177*mm
+      5.80817*eV  0.210456*mm
+      5.89634*eV  0.201012*mm
+      5.98452*eV  0.192627*mm
+      6.0727*eV   0.185134*mm
+      6.16087*eV  0.178399*mm
+      6.24905*eV  0.172309*mm
+      6.33723*eV  0.166779*mm
+      6.4254*eV   0.166779*mm
+      6.51358*eV  0.166779*mm
+      6.60176*eV  0.166779*mm
+      6.68993*eV  0.166779*mm
+      6.77811*eV  0.166779*mm
+      6.86629*eV  0.166779*mm
+      6.95446*eV  0.166779*mm
+      7.04264*eV  0.166779*mm
+      7.13082*eV  0.166779*mm
+      7.21899*eV  0.166779*mm
+      7.30717*eV  0.166779*mm
+      7.39535*eV  0.166779*mm
+      7.48353*eV  0.166779*mm
+      7.5717*eV   0.166779*mm
+      7.65988*eV  0.166779*mm
+      7.74806*eV  0.166779*mm
+      7.83623*eV  0.166779*mm
+      7.92441*eV  0.166779*mm
+      8.01259*eV  0.166779*mm
+      8.10076*eV  0.166779*mm
+      8.18894*eV  0.166779*mm
+      8.27712*eV  0.166779*mm
+      8.36529*eV  0.166779*mm
+      8.45347*eV  0.166779*mm
+      "/>
+  </properties>
+
+  <materials>
+    <material name="Air">
+      <D type="density" unit="g/cm3" value="0.0012"/>
+      <fraction n="0.754" ref="N"/>
+      <fraction n="0.234" ref="O"/>
+      <fraction n="0.012" ref="Ar"/>
+    </material>
+    <material name="AirOptical">
+      <D type="density" unit="g/cm3" value="0.0012"/>
+      <fraction n="0.754" ref="N"/>
+      <fraction n="0.234" ref="O"/>
+      <fraction n="0.012" ref="Ar"/>
+      <property name="RINDEX" ref="RINDEX__Air"/>
+      <property name="ABSLENGTH" coldim="2" values="1*eV  200*m  5*eV  200*m"/>
+    </material>
+    <material name="Vacuum">
+      <D type="density" unit="g/cm3" value="0.0000000001"/>
+      <fraction n="0.754" ref="N"/>
+      <fraction n="0.234" ref="O"/>
+      <fraction n="0.012" ref="Ar"/>
+    </material>
+    <material name="VacuumOptical">
+      <D type="density" unit="g/cm3" value="0.0000000001"/>
+      <fraction n="0.754" ref="N"/>
+      <fraction n="0.234" ref="O"/>
+      <fraction n="0.012" ref="Ar"/>
+      <property name="RINDEX" ref="RINDEX__Vacuum"/>
+      <property name="ABSLENGTH" coldim="2" values="1*eV  2000*m  5*eV  2000*m"/>
+    </material>
+    <material name="SiliconDioxide"> <!-- density from `G4_SILICON_DIOXIDE` (NIST DB) -->
+      <D type="density" value="2.32" unit="g/cm3"/>
+      <composite n="1" ref="Si"/>
+      <composite n="2" ref="O"/>
+    </material>
+    <material name="Plexiglass">
+      <D type="density" value="1.19" unit="g/cm3"/>
+      <composite n="5" ref="C"/>
+      <composite n="8" ref="H"/>
+      <composite n="2" ref="O"/>
+    </material>
+    <material name="PolyvinylAcetate">
+      <D type="density" value="1.19" unit="g/cm3"/>
+      <composite n="4" ref="C"/>
+      <composite n="6" ref="H"/>
+      <composite n="2" ref="O"/>
+    </material>
+    <material name="C4F10_PFRICH">
+      <D type="density" value="0.009935" unit="g/cm3"/>
+      <composite n="4"  ref="C"/>
+      <composite n="10" ref="F"/>
+      <property name="RINDEX"    ref="RINDEX__C4F10_PFRICH"/>
+      <property name="ABSLENGTH" ref="ABSLENGTH__C4F10_PFRICH"/>
+    </material>
+    <material name="Aerogel_PFRICH">
+      <D type="density" value="0.110" unit="g/cm3"/>
+      <comment> n_air = [dens(Si02)-dens(aerogel)] / [dens(Si02)-dens(Air) ] </comment>
+      <fraction n="    (2.32-0.11) / (2.32-0.0012)" ref="Air"/>
+      <fraction n="1 - (2.32-0.11) / (2.32-0.0012)" ref="SiliconDioxide"/>
+      <property name="RINDEX"    ref="RINDEX__Aerogel_PFRICH"/>
+      <property name="ABSLENGTH" ref="ABSLENGTH__Aerogel_PFRICH"/>
+      <property name="RAYLEIGH"  ref="RAYLEIGH__Aerogel_PFRICH"/>
+    </material>
+    <material name="Acrylic_PFRICH">
+      <D type="density" value="1.19" unit="g/cm3"/>
+      <comment> TO BE IMPROVED </comment>
+      <fraction n="0.99" ref="Plexiglass"/>
+      <fraction n="0.01" ref="PolyvinylAcetate"/>
+      <property name="RINDEX"    ref="RINDEX__Acrylic_PFRICH"/>
+      <property name="ABSLENGTH" ref="ABSLENGTH__Acrylic_PFRICH"/>
+    </material>
+  </materials>
+
+  <surfaces>
+    <opticalsurface name="SensorSurface_PFRICH" model="glisur" finish="polished" type="dielectric_dielectric">
+      <property name="EFFICIENCY" coldim="2" values="
+        1*eV  1
+        4*eV  1
+        7*eV  1
+        "/>
+    </opticalsurface>
+  </surfaces>
+
+</lccdd>
diff --git a/examples/OpticalTracker/compact/pfrich.xml b/examples/OpticalTracker/compact/pfrich.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9f3581575a79cb14371afcf46604b010385ac8fa
--- /dev/null
+++ b/examples/OpticalTracker/compact/pfrich.xml
@@ -0,0 +1,209 @@
+<lccdd 
+  xmlns:compact="http://www.lcsim.org/schemas/compact/1.0"
+  xmlns:xs="http://www.w3.org/2001/XMLSchema"
+  xs:noNamespaceSchemaLocation="http://www.lcsim.org/schemas/compact/1.0/compact.xsd"
+  >
+
+<info
+  name="PFRICH"
+  title="Proximity Focusing Ring Imaging Cherenkov Detector"
+  author="Christopher Dilks"
+  url="https://github.com/eic/epic"
+  status="development"
+  version="1.0"
+  >
+  <comment>
+  Example RICH Detector
+  </comment>
+</info>
+
+<debug>
+  <type name="surface"       value="0"/>
+  <type name="material"      value="0"/>
+  <type name="readout"       value="0"/>
+  <type name="segmentation"  value="0"/>
+  <type name="limits"        value="0"/>
+  <type name="region"        value="0"/>
+  <type name="includes"      value="0"/>
+</debug>
+
+<includes>
+  <gdmlFile ref="${DD4hepINSTALL}/DDDetectors/compact/elements.xml"/>
+  <file     ref="materials.xml"/>
+</includes>
+
+<display>
+  <vis name="vessel_vis"  r="102/256" g="102/256" b="102/256" alpha="1.0" showDaughters="true" visible="true" />
+  <vis name="gas_vis"     r="100/256" g="200/256" b="0/256"   alpha="0.5" showDaughters="true" visible="true" />
+  <vis name="aerogel_vis" r="0/256"   g="161/256" b="156/256" alpha="1.0" showDaughters="true" visible="true" />
+  <vis name="filter_vis"  r="248/256" g="188/256" b="0/256"   alpha="1.0" showDaughters="true" visible="true" />
+  <vis name="sensor_vis"  r="0/256"   g="96/256"  b="156/256" alpha="1.0" showDaughters="true" visible="true" />
+  <vis name="service_vis" r="102/256" g="102/256" b="102/256" alpha="1.0" showDaughters="true" visible="true" />
+  <vis name="no_vis" showDaughters="false" visible="false" />
+</display>
+
+<define>
+  <!-- global constants -->
+  <constant name="world_side" value="30*m"/>
+  <constant name="world_x"    value="world_side"/>
+  <constant name="world_y"    value="world_side"/>
+  <constant name="world_z"    value="100*m"/>
+  <constant name="Pi"         value="3.14159265359"/>
+  <constant name="mil"        value="0.0254*mm"/>
+  <constant name="inch"       value="2.54*cm"/>
+  <!-- RICH constants -->
+  <constant name="PFRICH_ID"                 value="1"/>                           <!-- unique ID for this detector -->
+  <constant name="PFRICH_Length"             value="58.0*cm"/>                     <!-- vessel z-length -->
+  <constant name="PFRICH_zmin"               value="-150*cm"/>                     <!-- vessel front -->
+  <constant name="PFRICH_zmax"               value="PFRICH_zmin - PFRICH_Length"/> <!-- vessel back -->
+  <constant name="PFRICH_rmin0"              value="5*cm"/>                        <!-- bore radius at vessel frontplane -->
+  <constant name="PFRICH_rmin1"              value="7*cm"/>                        <!-- bore radius at vessel backplane -->
+  <constant name="PFRICH_rmax"               value="93*cm"/>                       <!-- vessel backplane radius -->
+  <constant name="PFRICH_wall_thickness"     value="0.5*cm"/>                      <!-- thickness of radial walls -->
+  <constant name="PFRICH_window_thickness"   value="0.1*cm"/>                      <!-- thickness of entrance and exit walls -->
+  <constant name="PFRICH_aerogel_thickness"  value="3.0*cm"/>                      <!-- aerogel thickness -->
+  <constant name="PFRICH_filter_thickness"   value="0.3*mm"/>                      <!-- filter thickness (between aerogel and gas) -->
+  <constant name="PFRICH_sensor_active_size" value="24.0*mm"/>                     <!-- sensor side length (effective area) -->
+  <constant name="PFRICH_sensor_full_size"   value="25.8*mm"/>                     <!-- sensor side length (full size, with enclosure) -->
+  <constant name="PFRICH_sensor_thickness"   value="0.5*mm"/>                      <!-- sensor thickness -->
+  <constant name="PFRICH_sensor_dist"        value="40*cm"/>                       <!-- distance between aerogel exit plane and sensor entrance plane -->
+  <constant name="PFRICH_num_px"             value="8"/>                           <!-- number of pixels along one side of the sensor -->
+  <constant name="PFRICH_pixel_pitch"        value="PFRICH_sensor_active_size / PFRICH_num_px"/> <!-- center-to-center distance between sensor pixels -->
+</define>
+
+<detectors>
+
+  <!-- /detectors/detector -->
+  <comment>
+    ### PFRICH: Proximity Focusing RICH
+  </comment>
+  <detector
+    id="PFRICH_ID"
+    name="PFRICH"
+    type="PFRICH"
+    readout="PFRICHHits"
+    gas="C4F10_PFRICH"
+    material="Aluminum"
+    vis_vessel="vessel_vis"
+    vis_gas="gas_vis"
+    >
+
+    <!-- /detectors/detector/dimensions -->
+    <comment>
+      #### Vessel
+      - dimensions:
+      - `zmin`:              z-position of vessel front plane
+      - `length`:            overall z-length of the full vessel
+      - `rmin0` and `rmin1`: bore radius at front plane and back plane, respectively
+      - `rmax0` and `rmax1`: outer radius of vessel, at front plane and back plane, respectively
+      - `wall_thickness`:    thickness of radial walls
+      - `window_thickness`:  thickness of entrance and exit disks
+    </comment>
+    <dimensions
+      zmin="PFRICH_zmin"
+      zmax="PFRICH_zmax"
+      length="PFRICH_Length"
+      rmin0="PFRICH_rmin0"
+      rmin1="PFRICH_rmin1"
+      rmax0="PFRICH_rmax"
+      rmax1="PFRICH_rmax"
+      wall_thickness="PFRICH_wall_thickness"
+      window_thickness="PFRICH_window_thickness"
+      />
+
+    <!-- /detectors/detector/radiator -->
+    <comment>
+      #### Radiator
+      - `radiator` includes aerogel and a filter; the filter is applied to the back of the aerogel, so that it
+        separates the aerogel and gas radiators
+      - dimensions:
+        - `frontplane`:      front of the aerogel, w.r.t. front plane of the vessel envelope
+        - `rmin` and `rmax`: inner and outer radius (at the front plane; radial bounds are conical)
+        - `thickness`:       radiator thickness, defined separately for aerogel and filter
+    </comment>
+    <radiator
+      frontplane="-PFRICH_window_thickness"
+      rmin="PFRICH_rmin0 + PFRICH_wall_thickness + 0.2*cm"
+      rmax="(PFRICH_rmax/PFRICH_zmax)*PFRICH_zmin + 8.0*cm"
+      >
+      <aerogel material="Aerogel_PFRICH" vis="aerogel_vis" thickness="PFRICH_aerogel_thickness" />
+      <filter  material="Acrylic_PFRICH" vis="filter_vis"  thickness="PFRICH_filter_thickness"  />
+  </radiator>
+
+  <!-- /detectors/detector/sensors -->
+  <comment>
+    #### Sensors
+  </comment>
+  <sensors>
+
+    <!-- /detectors/detector/sensors/module -->
+    <comment>
+      ##### Sensor module
+      - dimensions:
+        - `side`:      side length of the square module
+        - `thickness`: thickness of the sensor module
+        - `gap`:       provides room between the squares, to help prevent them from overlapping
+      - notes:
+        - the values of `side` and `gap` will determine how many sensors there are, since the
+          sensor placement algorithm will try to place as many as it can in the specified region
+        - the material is `AirOptical`, to resolve a technical issue with the refractive boundary
+    </comment>
+    <module
+      material="AirOptical"
+      surface="SensorSurface_PFRICH"
+      vis="sensor_vis"
+      side="PFRICH_sensor_active_size"
+      thickness="PFRICH_sensor_thickness"
+      gap="0.5*(PFRICH_sensor_full_size-PFRICH_sensor_active_size) + 0.5*mm"
+      />
+
+    <!-- /detectors/detector/sensors/plane -->
+    <comment>
+      ##### Sensor plane
+      - sensors will be placed on a plane
+      - plane dimensions:
+        - `sensordist`: distance between sensor plane active surface (e.g., photocathode) and aerogel backplane
+        - `rmin`:       minimum radial position of a sensor's centroid
+        - `rmax`:       maximum radial position of a sensor's centroid
+    </comment>
+    <plane
+      sensordist="PFRICH_sensor_dist"
+      rmin="PFRICH_rmin1 + 2*cm"
+      rmax="PFRICH_rmax  - 4*cm"
+      />
+
+    <services>
+      <comment>
+        Material should be equivalent with 3x0.5cm Al, spread over the entire available distance.
+      </comment>
+      <component name="aluminum" thickness="5*mm"  vis="service_vis" material="Aluminum"/>
+      <component name="air"      thickness="40*mm" vis="no_vis"      material="Air"/>
+      <component name="aluminum" thickness="5*mm"  vis="service_vis" material="Aluminum"/>
+      <component name="air"      thickness="40*mm" vis="no_vis"      material="Air"/>
+      <component name="aluminum" thickness="5*mm"  vis="service_vis" material="Aluminum"/>
+    </services>
+
+  </sensors>
+  </detector>
+</detectors>
+
+<comment>
+  #### Readout
+  - segmentation: square matrix of pixels
+  - `grid_size_x,y`: size of each sensor pixel
+  - `offset_x,y`: specified such that the `x` and `y` field values are unsigned
+</comment>
+<readouts>
+  <readout name="PFRICHHits">
+    <segmentation
+      type="CartesianGridXY"
+      grid_size_x="PFRICH_pixel_pitch"
+      grid_size_y="PFRICH_pixel_pitch"
+      offset_x="-0.5*(PFRICH_num_px-1)*PFRICH_pixel_pitch"
+      offset_y="-0.5*(PFRICH_num_px-1)*PFRICH_pixel_pitch"
+      />
+    <id>system:8,module:12,x:32:-16,y:-16</id>
+  </readout>
+</readouts>
+
+</lccdd>
diff --git a/examples/OpticalTracker/doc/geometry.png b/examples/OpticalTracker/doc/geometry.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b85d7c9a47118c0e33ae795e06bc6ca11162773
Binary files /dev/null and b/examples/OpticalTracker/doc/geometry.png differ
diff --git a/examples/OpticalTracker/scripts/richsim.py b/examples/OpticalTracker/scripts/richsim.py
new file mode 100755
index 0000000000000000000000000000000000000000..b371ce3a964c74fe58306fa30d01a92e4416e57e
--- /dev/null
+++ b/examples/OpticalTracker/scripts/richsim.py
@@ -0,0 +1,92 @@
+"""
+DD4hep simulation with some argument parsing
+Based on M. Frank and F. Gaede runSim.py
+   @author  A.Sailer
+   @version 0.1
+
+Modified with settings for RICH simulation
+"""
+from __future__ import absolute_import, unicode_literals
+import logging
+import sys
+import os
+
+from DDSim.DD4hepSimulation import DD4hepSimulation
+
+
+if __name__ == "__main__":
+    logging.basicConfig(
+        format="%(name)-16s %(levelname)s %(message)s",
+        level=logging.INFO,
+        stream=sys.stdout,
+        )
+    logger = logging.getLogger("DDSim")
+
+    SIM = DD4hepSimulation()
+
+    # Ensure that Cerenkov and optical physics are always loaded
+    def setupCerenkov(kernel):
+        from DDG4 import PhysicsList
+
+        seq = kernel.physicsList()
+        cerenkov = PhysicsList(kernel, "Geant4CerenkovPhysics/CerenkovPhys")
+        cerenkov.MaxNumPhotonsPerStep = 10
+        cerenkov.MaxBetaChangePerStep = 10.0
+        cerenkov.TrackSecondariesFirst = False
+        cerenkov.VerboseLevel = 0
+        cerenkov.enableUI()
+        seq.adopt(cerenkov)
+        ph = PhysicsList(kernel, "Geant4OpticalPhotonPhysics/OpticalGammaPhys")
+        ph.addParticleConstructor("G4OpticalPhoton")
+        ph.VerboseLevel = 0
+        ph.enableUI()
+        seq.adopt(ph)
+        return None
+
+    SIM.physics.setupUserPhysics(setupCerenkov)
+
+    # Allow energy depositions to 0 energy in trackers (which include optical detectors)
+    SIM.filter.tracker = "edep0"
+
+    # Some detectors are only sensitive to optical photons
+    SIM.filter.filters["opticalphotons"] = dict(
+        name="ParticleSelectFilter/OpticalPhotonSelector",
+        parameter={"particle": "opticalphoton"},
+        )
+    SIM.filter.mapDetFilter["PFRICH"] = "opticalphotons"
+
+    # Use the optical tracker for the PFRICH
+    SIM.action.mapActions["PFRICH"] = "Geant4OpticalTrackerAction"
+
+    # Disable user tracker particle handler, so hits can be associated to photons
+    SIM.part.userParticleHandler = ""
+
+    # Particle gun settings: pions with fixed energy and theta, varying phi
+    SIM.numberOfEvents = 500
+    SIM.enableGun = True
+    SIM.gun.energy = "40*GeV"
+    SIM.gun.particle = "pi+"
+    SIM.gun.thetaMin = "195.0*deg"
+    SIM.gun.thetaMax = "195.1*deg"
+    SIM.gun.distribution = "cos(theta)"
+
+    # Installed compact file, otherwise assume the user passed `--compactFile`
+    install_prefix = os.environ.get("DD4hepExamplesINSTALL")
+    if install_prefix:
+        SIM.compactFile = install_prefix + "/examples/OpticalTracker/compact/pfrich.xml"
+
+    # Output file (assuming CWD)
+    SIM.outputFile = "sim.root"
+
+    # Override with user options
+    SIM.parseOptions()
+
+    # Run the simulation
+    try:
+        SIM.run()
+        logger.info("TEST: passed")
+    except NameError as e:
+        logger.fatal("TEST: failed")
+        if "global name" in str(e):
+            globalToSet = str(e).split("'")[1]
+            logger.fatal("Unknown global variable, please add\nglobal %s\nto your steeringFile" % globalToSet)
diff --git a/examples/OpticalTracker/scripts/test_number_of_hits.C b/examples/OpticalTracker/scripts/test_number_of_hits.C
new file mode 100644
index 0000000000000000000000000000000000000000..f5bef5bd5034410f91c72868f68044c96670a1c2
--- /dev/null
+++ b/examples/OpticalTracker/scripts/test_number_of_hits.C
@@ -0,0 +1,21 @@
+void test_number_of_hits(TString sim_file_name="sim.root") {
+
+  // test requirements
+  const Double_t expected_number_of_hits = 230.0;
+  const Double_t allowed_deviation       = 15.0;
+
+  // get average number of hits
+  auto sim_file = new TFile(sim_file_name);
+  auto t = (TTree*) sim_file->Get("EVENT");
+  auto h = new TH1D("h","<hits>",500,0,1000);
+  t->Project("h","@PFRICHHits.size()");
+  auto ave_hits = h->GetMean();
+
+  // check if this is the expected number of hits
+  bool pass_test = abs(ave_hits - expected_number_of_hits) < allowed_deviation;
+  std::cout << "TEST: " << (pass_test ? "passed" : "failed")
+    << " with average number of hits = " << ave_hits
+    << " (expected " << expected_number_of_hits
+    << "+/-" << allowed_deviation << ")"
+    << std::endl;
+}
diff --git a/examples/OpticalTracker/src/PFRICH_geo.cpp b/examples/OpticalTracker/src/PFRICH_geo.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..779e2243dcad32da70a95a2c6b05c727d296093c
--- /dev/null
+++ b/examples/OpticalTracker/src/PFRICH_geo.cpp
@@ -0,0 +1,270 @@
+//----------------------------------
+//  pfRICH: Proximity Focusing RICH
+//  Author: C. Dilks
+//----------------------------------
+
+#include "DD4hep/DetFactoryHelper.h"
+#include "DD4hep/OpticalSurfaces.h"
+#include "DD4hep/Printout.h"
+#include "DDRec/DetectorData.h"
+#include <XML/Helper.h>
+
+using namespace dd4hep;
+using namespace dd4hep::rec;
+
+// create the detector
+static Ref_t createDetector(Detector& desc, xml::Handle_t handle, SensitiveDetector sens)
+{
+  xml::DetElement       detElem = handle;
+  std::string           detName = detElem.nameStr();
+  int                   detID   = detElem.id();
+  xml::Component        dims    = detElem.dimensions();
+  OpticalSurfaceManager surfMgr = desc.surfaceManager();
+  DetElement            det(detName, detID);
+
+  // constant attributes -----------------------------------------------------------
+  // - vessel
+  double vesselLength       = dims.attr<double>(_Unicode(length));
+  double vesselZmin         = dims.attr<double>(_Unicode(zmin));
+  double vesselRmin0        = dims.attr<double>(_Unicode(rmin0));
+  double vesselRmin1        = dims.attr<double>(_Unicode(rmin1));
+  double vesselRmax0        = dims.attr<double>(_Unicode(rmax0));
+  double vesselRmax1        = dims.attr<double>(_Unicode(rmax1));
+  double wallThickness      = dims.attr<double>(_Unicode(wall_thickness));
+  double windowThickness    = dims.attr<double>(_Unicode(window_thickness));
+  auto   vesselMat          = desc.material(detElem.attr<std::string>(_Unicode(material)));
+  auto   gasvolMat          = desc.material(detElem.attr<std::string>(_Unicode(gas)));
+  auto   vesselVis          = desc.visAttributes(detElem.attr<std::string>(_Unicode(vis_vessel)));
+  auto   gasvolVis          = desc.visAttributes(detElem.attr<std::string>(_Unicode(vis_gas)));
+  // - radiator (applies to aerogel and filter)
+  auto   radiatorElem       = detElem.child(_Unicode(radiator));
+  double radiatorRmin       = radiatorElem.attr<double>(_Unicode(rmin));
+  double radiatorRmax       = radiatorElem.attr<double>(_Unicode(rmax));
+  double radiatorFrontplane = radiatorElem.attr<double>(_Unicode(frontplane));
+  // - aerogel
+  auto   aerogelElem        = radiatorElem.child(_Unicode(aerogel));
+  auto   aerogelMat         = desc.material(aerogelElem.attr<std::string>(_Unicode(material)));
+  auto   aerogelVis         = desc.visAttributes(aerogelElem.attr<std::string>(_Unicode(vis)));
+  double aerogelThickness   = aerogelElem.attr<double>(_Unicode(thickness));
+  // - filter
+  auto   filterElem         = radiatorElem.child(_Unicode(filter));
+  auto   filterMat          = desc.material(filterElem.attr<std::string>(_Unicode(material)));
+  auto   filterVis          = desc.visAttributes(filterElem.attr<std::string>(_Unicode(vis)));
+  double filterThickness    = filterElem.attr<double>(_Unicode(thickness));
+  // - sensor module
+  auto   sensorElem         = detElem.child(_Unicode(sensors)).child(_Unicode(module));
+  auto   sensorMat          = desc.material(sensorElem.attr<std::string>(_Unicode(material)));
+  auto   sensorVis          = desc.visAttributes(sensorElem.attr<std::string>(_Unicode(vis)));
+  auto   sensorSurf         = surfMgr.opticalSurface(sensorElem.attr<std::string>(_Unicode(surface)));
+  double sensorSide         = sensorElem.attr<double>(_Unicode(side));
+  double sensorGap          = sensorElem.attr<double>(_Unicode(gap));
+  double sensorThickness    = sensorElem.attr<double>(_Unicode(thickness));
+  // - sensor plane
+  auto   sensorPlaneElem    = detElem.child(_Unicode(sensors)).child(_Unicode(plane));
+  double sensorPlaneDist    = sensorPlaneElem.attr<double>(_Unicode(sensordist));
+  double sensorPlaneRmin    = sensorPlaneElem.attr<double>(_Unicode(rmin));
+  double sensorPlaneRmax    = sensorPlaneElem.attr<double>(_Unicode(rmax));
+
+  // BUILD VESSEL //////////////////////////////////////
+  /* - `vessel`: aluminum enclosure, the mother volume of the pfRICH
+   * - `gasvol`: gas volume, which fills `vessel`; all other volumes defined below
+   *   are children of `gasvol`
+   */
+
+  // tank solids
+  double boreDelta = vesselRmin1 - vesselRmin0;
+  Cone vesselSolid(
+      vesselLength / 2.0,
+      vesselRmin1,
+      vesselRmax1,
+      vesselRmin0,
+      vesselRmax0
+      );
+  Cone gasvolSolid(
+      vesselLength / 2.0 - windowThickness,
+      vesselRmin1 + wallThickness,
+      vesselRmax1 - wallThickness,
+      vesselRmin0 + wallThickness,
+      vesselRmax0 - wallThickness
+      );
+
+  // volumes
+  Volume vesselVol(detName,        vesselSolid, vesselMat);
+  Volume gasvolVol(detName+"_gas", gasvolSolid, gasvolMat);
+  vesselVol.setVisAttributes(vesselVis);
+  gasvolVol.setVisAttributes(gasvolVis);
+
+  // reference positions
+  /* - the vessel is created such that the center of the cylindrical tank volume
+   *   coincides with the origin; this is called the "origin position" of the vessel
+   * - when the vessel (and its children volumes) is placed, it is translated in
+   *   the z-direction to be in the proper full-detector integration location
+   * - these reference positions are for the frontplane and backplane of the vessel,
+   *   with respect to the vessel origin position
+   */
+  auto originFront = Position(0., 0., vesselLength / 2.0);
+
+  // sensitive detector type
+  sens.setType("tracker");
+
+  // BUILD RADIATOR //////////////////////////////////////
+
+  // attributes
+  double airGap = 0.01 * mm; // air gap between aerogel and filter (FIXME? actually it's currently a gas gap)
+
+  // solid and volume: create aerogel and filter
+  Cone aerogelSolid(
+      aerogelThickness / 2,
+      radiatorRmin + boreDelta * aerogelThickness / vesselLength, // at backplane
+      radiatorRmax,
+      radiatorRmin, // at frontplane
+      radiatorRmax
+      );
+  Cone filterSolid(
+      filterThickness / 2,
+      radiatorRmin + boreDelta * (aerogelThickness + airGap + filterThickness) / vesselLength, // at backplane
+      radiatorRmax,
+      radiatorRmin + boreDelta * (aerogelThickness + airGap) / vesselLength, // at frontplane
+      radiatorRmax
+      );
+  Volume aerogelVol(detName + "_aerogel", aerogelSolid, aerogelMat);
+  Volume filterVol(detName  + "_filter",  filterSolid,  filterMat);
+  aerogelVol.setVisAttributes(aerogelVis);
+  filterVol.setVisAttributes(filterVis);
+
+  // aerogel placement and surface properties
+  // FIXME: define skin properties for aerogel and filter
+  auto radiatorPos = Position(0., 0., radiatorFrontplane - 0.5 * aerogelThickness) + originFront;
+  auto aerogelPV = gasvolVol.placeVolume(
+      aerogelVol,
+      Transform3D(Translation3D(radiatorPos.x(), radiatorPos.y(), radiatorPos.z())) // re-center to originFront
+      );
+  DetElement aerogelDE(det, "aerogel_de", 0);
+  aerogelDE.setPlacement(aerogelPV);
+
+  // filter placement and surface properties
+  auto filterPV = gasvolVol.placeVolume(
+      filterVol,
+      Transform3D(
+        Translation3D(0., 0., -airGap) // add an airgap (FIXME: actually a gas gap)
+        *
+        Translation3D(radiatorPos.x(), radiatorPos.y(), radiatorPos.z())  // re-center to originFront
+        *
+        Translation3D(0., 0., -(aerogelThickness + filterThickness) / 2.) // move to aerogel backplane
+        )
+      );
+  DetElement filterDE(det, "filter_de", 0);
+  filterDE.setPlacement(filterPV);
+
+  // BUILD SENSORS ///////////////////////
+
+  // solid and volume: single sensor module
+  Box    sensorSolid(sensorSide / 2., sensorSide / 2., sensorThickness / 2.);
+  Volume sensorVol(detName + "_sensor", sensorSolid, sensorMat);
+  sensorVol.setVisAttributes(sensorVis);
+
+  // sensitivity
+  sensorVol.setSensitiveDetector(sens);
+
+  // sensor plane positioning: we want `sensorPlaneDist` to be the distance between the
+  // aerogel backplane (i.e., aerogel/filter boundary) and the sensor active surface (e.g, photocathode)
+  double sensorZpos     = radiatorFrontplane - aerogelThickness - sensorPlaneDist - 0.5 * sensorThickness;
+  auto   sensorPlanePos = Position(0., 0., sensorZpos) + originFront; // reference position
+  // miscellaneous
+  int    imod    = 0;           // module number
+  double tBoxMax = vesselRmax1; // sensors will be tiled in tBox, within annular limits
+
+  // SENSOR MODULE LOOP ------------------------
+  /* cartesian tiling loop
+   * - start at (x=0,y=0), to center the grid
+   * - loop over positive-x positions; for each, place the corresponding negative-x sensor too
+   * - nested similar loop over y positions
+   */
+  double sx, sy;
+  for (double usx = 0; usx <= tBoxMax; usx += sensorSide + sensorGap) {
+    for (int sgnx = 1; sgnx >= (usx > 0 ? -1 : 1); sgnx -= 2) {
+      for (double usy = 0; usy <= tBoxMax; usy += sensorSide + sensorGap) {
+        for (int sgny = 1; sgny >= (usy > 0 ? -1 : 1); sgny -= 2) {
+
+          // sensor (x,y) center
+          sx = sgnx * usx;
+          sy = sgny * usy;
+
+          // annular cut
+          if (std::hypot(sx, sy) < sensorPlaneRmin || std::hypot(sx, sy) > sensorPlaneRmax)
+            continue;
+
+          // placement (note: transformations are in reverse order)
+          auto sensorPV = gasvolVol.placeVolume(
+              sensorVol,
+              Transform3D(
+                Translation3D(sensorPlanePos.x(), sensorPlanePos.y(), sensorPlanePos.z()) // move to reference position
+                *
+                Translation3D(sx, sy, 0.) // move to grid position
+                )
+              );
+
+          // generate LUT for module number -> sensor position, for readout mapping tests
+          // printf("%d %f %f\n",imod,sensorPV.position().x(),sensorPV.position().y());
+
+          // properties
+          sensorPV.addPhysVolID("module", imod);
+          DetElement sensorDE(det, Form("sensor_de_%d", imod), imod);
+          sensorDE.setPlacement(sensorPV);
+          SkinSurface sensorSkin(desc, sensorDE, "sensor_optical_surface", sensorSurf, sensorVol); // FIXME: 3rd arg needs `imod`?
+          sensorSkin.isValid();
+
+          // increment sensor module number
+          imod++;
+        }
+      }
+    }
+  }
+  // END SENSOR MODULE LOOP ------------------------
+
+  // Add service material if desired (added by Sylvester Joosten) ////////////////
+  if (detElem.child("sensors").hasChild(_Unicode(services))) {
+    xml_comp_t x_service = detElem.child("sensors").child(_Unicode(services));
+    Assembly   service_vol("services");
+    service_vol.setVisAttributes(desc, x_service.visStr());
+
+    // Compute service total thickness from components
+    double total_thickness = 0;
+    for (xml_coll_t ci(x_service, _Unicode(component)); ci; ++ci) {
+      total_thickness += xml_comp_t(ci).thickness();
+    }
+
+    int    ncomponents   = 0;
+    double thickness_sum = -total_thickness / 2.0;
+    for (xml_coll_t ci(x_service, _Unicode(component)); ci; ++ci, ncomponents++) {
+      xml_comp_t x_comp    = ci;
+      double     thickness = x_comp.thickness();
+      Tube       c_tube{sensorPlaneRmin, sensorPlaneRmax, thickness / 2};
+      Volume     c_vol{_toString(ncomponents, "component%d"), c_tube, desc.material(x_comp.materialStr())};
+      c_vol.setVisAttributes(desc, x_comp.visStr());
+      service_vol.placeVolume(c_vol, Position(0, 0, thickness_sum + thickness / 2.0));
+      thickness_sum += thickness;
+    }
+    gasvolVol.placeVolume(service_vol,
+        Transform3D(Translation3D(sensorPlanePos.x(), sensorPlanePos.y(),
+            sensorPlanePos.z() - sensorThickness - total_thickness)));
+  }
+
+  // VESSEL PLACEMENT /////////////////////////////////////////////////////////////
+
+  // place gas volume
+  PlacedVolume gasvolPV = vesselVol.placeVolume(gasvolVol, Position(0, 0, 0));
+  DetElement   gasvolDE(det, "gasvol_de", 0);
+  gasvolDE.setPlacement(gasvolPV);
+
+  // place mother volume (vessel)
+  Volume       motherVol = desc.pickMotherVolume(det);
+  PlacedVolume vesselPV  = motherVol.placeVolume(vesselVol, Position(0, 0, vesselZmin) - originFront);
+  vesselPV.addPhysVolID("system", detID);
+  det.setPlacement(vesselPV);
+
+  return det;
+}
+
+// clang-format off
+DECLARE_DETELEMENT(PFRICH, createDetector)