Skip to content
Open
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
Empty file added Geometry]:
Empty file.
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,22 @@ ctest --test-dir build
# Build the complete SHiP geometry
./build/apps/build_geometry [output_file.db]

# Build a single subsystem on its own
./build/apps/build_geometry <Name> [output_file.db]

# List the available subsystem names
./build/apps/build_geometry --list

# View in gmex
gmex output_file.db
```

With no subsystem named, the complete detector is built. Naming a subsystem
(as spelled by `--list`, e.g. `Calorimeter`) builds just that subsystem in its
own local frame. A token ending in `.db` is taken as the output file; the
default is `ship_geometry.db` for the full build, or `<Name>.db` for a single
subsystem.

### Installing

```bash
Expand All @@ -96,20 +108,39 @@ GeoModelTools automatically.

### Factory Pattern

Each subsystem is implemented as a factory class:
Each subsystem is a self-contained factory class that builds its geometry,
describes where it belongs, and registers itself:

```cpp
class FooFactory {
public:
explicit FooFactory(SHiPMaterials& materials);
GeoPhysVol* build();

// Self-description consumed by the assembler:
// name, tree node, id, placement (x/y/z mm), and whether it is the world.
static SubsystemDescriptor descriptor() {
return {"Foo", "/SHiP/foo", 42, 0.0, 0.0, 12345.0, /*isWorld=*/false};
}
private:
SHiPMaterials& m_materials;
};
```

`SHiPGeometryBuilder::build()` orchestrates all factories, creating the world
volume (Cavern) and placing each subsystem at its global z-position.
with a single registration line in the factory's `.cpp` (inside
`namespace SHiPGeometry`):

```cpp
REGISTER_SUBSYSTEM(FooFactory)
```

The assembler names no subsystem. `assembleGeometry()` — also reachable via
the unchanged `SHiPGeometryBuilder::build()` — iterates the registry, builds
the world (the subsystem whose descriptor sets `isWorld`, i.e. the Cavern),
and places every other registered subsystem at its declared position, ordered
by `(z, id)`. A single subsystem can be built on its own, in its local frame,
with `buildSubsystem("Foo")`. The registry, descriptor type, and macro live in
`include/SHiPGeometry/SubsystemRegistry.h`.

### Materials

Expand All @@ -128,21 +159,27 @@ To add a new material, edit `src/SHiPMaterials.cpp`:
## Adding or Modifying a Subsystem

1. **Header**: `subsystems/<Name>/include/<Name>/<Name>Factory.h` — declare
the factory class with dimension constants as `static constexpr` members
the factory class with dimension constants as `static constexpr` members,
plus `static SubsystemDescriptor descriptor()` returning its name, tree
node, id, and placement
2. **Implementation**: `subsystems/<Name>/src/<Name>Factory.cpp` — implement
`build()` using GeoModel primitives (`GeoBox`, `GeoTubs`, `GeoLogVol`,
`GeoPhysVol`, `GeoTransform`, etc.)
3. **Registration**: add a `build()` + placement call in
`src/SHiPGeometry.cpp` (`SHiPGeometryBuilder::build()`)
4. **CMake**: add sources/headers to `subsystems/<Name>/CMakeLists.txt`
3. **Registration**: add one line — `REGISTER_SUBSYSTEM(<Name>Factory)` — at
the end of the factory `.cpp`, inside `namespace SHiPGeometry`. The
subsystem registers itself; **do not** edit `src/SHiPGeometry.cpp`, which
names no subsystem.
4. **CMake**: add sources/headers to `subsystems/<Name>/CMakeLists.txt`, and
add the new library to the `--whole-archive` block in `src/CMakeLists.txt`
so its self-registration initializer is not dropped by the linker
Comment thread
coderabbitai[bot] marked this conversation as resolved.
5. **Docs**: update the subsystem `README.md` with geometry tree, materials,
and status

## Structure

```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
geometry/
├── include/SHiPGeometry/ # Public headers (SHiPGeometry, SHiPMaterials)
├── include/SHiPGeometry/ # Public headers (SHiPGeometry, SHiPMaterials, SubsystemRegistry)
├── src/ # Core implementation
├── subsystems/ # Detector subsystem factories
│ ├── Cavern/
Expand Down
84 changes: 67 additions & 17 deletions apps/build_geometry.cpp
Original file line number Diff line number Diff line change
@@ -1,47 +1,97 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (C) CERN for the benefit of the SHiP Collaboration
//
// Build the SHiP GeoModel geometry and serialise it to a GeoModel SQLite (.db).
//
// Usage:
// build_geometry # full detector -> ship_geometry.db
// build_geometry out.db # full detector -> out.db
// build_geometry Calorimeter # just that subsystem -> Calorimeter.db
// build_geometry Calorimeter c.db # just that subsystem -> c.db
// build_geometry --list # list available subsystems
//
// A token ending in ".db" is the output file; any other token names a
// subsystem. With no subsystem named, the complete detector is built.

#include "SHiPGeometry/SHiPGeometry.h"
#include "SHiPGeometry/SubsystemRegistry.h"

#include <GeoModelDBManager/GMDBManager.h>
#include <GeoModelKernel/GeoPhysVol.h>
#include <GeoModelWrite/WriteGeoModel.h>

#include <filesystem>
#include <iostream>
#include <string>
#include <vector>

namespace {
bool endsWith(const std::string& s, const std::string& suffix) {
return s.size() >= suffix.size() &&
s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
}
} // namespace

int main(int argc, char* argv[]) {
std::string outputFile = "ship_geometry.db";
if (argc > 1) {
outputFile = argv[1];
std::string outputFile;
std::vector<std::string> names; // requested subsystem(s)

for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--list") {
for (const auto& n : SHiPGeometry::subsystemNames())
std::cout << n << "\n";
return 0;
}
if (endsWith(arg, ".db")) {
outputFile = arg;
} else {
names.push_back(arg);
}
}

std::cout << "Building SHiP geometry..." << std::endl;
GeoVPhysVol* geometry = nullptr;
std::string label;

SHiPGeometry::SHiPGeometryBuilder builder;
GeoPhysVol* world = builder.build();
if (names.empty()) {
// Default: the complete detector (unchanged behaviour).
SHiPGeometry::SHiPGeometryBuilder builder;
geometry = builder.build();
label = "full SHiP geometry";
if (outputFile.empty())
outputFile = "ship_geometry.db";
} else if (names.size() == 1) {
// A single subsystem, on its own (local frame).
try {
geometry = SHiPGeometry::buildSubsystem(names[0]);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\nAvailable subsystems:\n";
for (const auto& n : SHiPGeometry::subsystemNames())
std::cerr << " " << n << "\n";
return 1;
}
label = names[0];
if (outputFile.empty())
outputFile = names[0] + ".db";
} else {
std::cerr << "Error: build at most one subsystem at a time (got " << names.size() << ").\n";
return 1;
}
Comment on lines +56 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Full-build path is missing the exception handling the single-subsystem path has.

SHiPGeometry::buildSubsystem() is wrapped in try/catch, but builder.build() (→ assembleGeometry({})) is not, even though it can throw std::runtime_error (e.g. no isWorld subsystem registered). Given this PR's own whole-archive linking is fragile across toolchains, this is a realistic way for the default build path to hit an uncaught exception and terminate ungracefully instead of printing a clean error.

🐛 Proposed fix: wrap both build paths in the same try/catch
     GeoVPhysVol* geometry = nullptr;
     std::string label;
 
-    if (names.empty()) {
-        // Default: the complete detector (unchanged behaviour).
-        SHiPGeometry::SHiPGeometryBuilder builder;
-        geometry = builder.build();
-        label = "full SHiP geometry";
-        if (outputFile.empty()) outputFile = "ship_geometry.db";
-    } else if (names.size() == 1) {
-        // A single subsystem, on its own (local frame).
-        try {
-            geometry = SHiPGeometry::buildSubsystem(names[0]);
-        } catch (const std::exception& e) {
-            std::cerr << "Error: " << e.what() << "\nAvailable subsystems:\n";
-            for (const auto& n : SHiPGeometry::subsystemNames()) std::cerr << "  " << n << "\n";
-            return 1;
-        }
-        label = names[0];
-        if (outputFile.empty()) outputFile = names[0] + ".db";
-    } else {
+    if (names.size() > 1) {
         std::cerr << "Error: build at most one subsystem at a time (got " << names.size()
                   << ").\n";
         return 1;
     }
+
+    try {
+        if (names.empty()) {
+            // Default: the complete detector (unchanged behaviour).
+            SHiPGeometry::SHiPGeometryBuilder builder;
+            geometry = builder.build();
+            label = "full SHiP geometry";
+            if (outputFile.empty()) outputFile = "ship_geometry.db";
+        } else {
+            // A single subsystem, on its own (local frame).
+            geometry = SHiPGeometry::buildSubsystem(names[0]);
+            label = names[0];
+            if (outputFile.empty()) outputFile = names[0] + ".db";
+        }
+    } catch (const std::exception& e) {
+        std::cerr << "Error: " << e.what() << "\nAvailable subsystems:\n";
+        for (const auto& n : SHiPGeometry::subsystemNames()) std::cerr << "  " << n << "\n";
+        return 1;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (names.empty()) {
// Default: the complete detector (unchanged behaviour).
SHiPGeometry::SHiPGeometryBuilder builder;
geometry = builder.build();
label = "full SHiP geometry";
if (outputFile.empty()) outputFile = "ship_geometry.db";
} else if (names.size() == 1) {
// A single subsystem, on its own (local frame).
try {
geometry = SHiPGeometry::buildSubsystem(names[0]);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\nAvailable subsystems:\n";
for (const auto& n : SHiPGeometry::subsystemNames()) std::cerr << " " << n << "\n";
return 1;
}
label = names[0];
if (outputFile.empty()) outputFile = names[0] + ".db";
} else {
std::cerr << "Error: build at most one subsystem at a time (got " << names.size()
<< ").\n";
return 1;
}
if (names.size() > 1) {
std::cerr << "Error: build at most one subsystem at a time (got " << names.size()
<< ").\n";
return 1;
}
try {
if (names.empty()) {
// Default: the complete detector (unchanged behaviour).
SHiPGeometry::SHiPGeometryBuilder builder;
geometry = builder.build();
label = "full SHiP geometry";
if (outputFile.empty()) outputFile = "ship_geometry.db";
} else {
// A single subsystem, on its own (local frame).
geometry = SHiPGeometry::buildSubsystem(names[0]);
label = names[0];
if (outputFile.empty()) outputFile = names[0] + ".db";
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\nAvailable subsystems:\n";
for (const auto& n : SHiPGeometry::subsystemNames()) std::cerr << " " << n << "\n";
return 1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/build_geometry.cpp` around lines 55 - 76, The default full-build path in
build_geometry.cpp lacks the same exception handling used for
SHiPGeometry::buildSubsystem, so builder.build() can terminate on a thrown
std::runtime_error. Wrap the names.empty() branch in the same try/catch pattern
used for the single-subsystem path, catch std::exception, print a clear error
plus available subsystems if helpful, and return 1 so buildGeometry fails
gracefully instead of aborting.


if (!world) {
std::cerr << "Error: Geometry not yet implemented" << std::endl;
if (!geometry) {
std::cerr << "Error: geometry is null (not yet implemented?)." << std::endl;
return 1;
}

// Remove existing output file
if (std::filesystem::exists(outputFile)) {
std::filesystem::remove(outputFile);
}
Comment on lines 86 to 88

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

std::filesystem::remove can throw uncaught on permission/lock errors.

The throwing overload is used; a locked or permission-denied output file would crash the tool instead of producing a clean error. The preceding exists() check is also redundant since remove() returns false harmlessly when the path doesn't exist.

🛡️ Proposed fix
-    if (std::filesystem::exists(outputFile)) {
-        std::filesystem::remove(outputFile);
-    }
+    std::error_code ec;
+    std::filesystem::remove(outputFile, ec);
+    if (ec) {
+        std::cerr << "Warning: could not remove existing " << outputFile << ": " << ec.message()
+                   << std::endl;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (std::filesystem::exists(outputFile)) {
std::filesystem::remove(outputFile);
}
std::error_code ec;
std::filesystem::remove(outputFile, ec);
if (ec) {
std::cerr << "Warning: could not remove existing " << outputFile << ": " << ec.message()
<< std::endl;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/build_geometry.cpp` around lines 83 - 85, The output-file cleanup in
build_geometry.cpp uses the throwing std::filesystem::remove overload after a
redundant exists() check, which can crash on permission or lock failures. Update
the logic around the outputFile deletion to call the non-throwing remove variant
and handle its result (and any error_code) in build_geometry so missing files
are treated as a no-op while real filesystem errors are reported cleanly.


std::cout << "Writing geometry to " << outputFile << std::endl;

std::cout << "Writing " << label << " to " << outputFile << std::endl;
GMDBManager db(outputFile);
GeoModelIO::WriteGeoModel writer(db);

// Traverse the geometry tree
world->exec(&writer);

// Save to database
geometry->exec(&writer);
writer.saveToDB();

std::cout << "Done." << std::endl;
return 0;
}
108 changes: 108 additions & 0 deletions include/SHiPGeometry/SubsystemRegistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (C) CERN for the benefit of the SHiP Collaboration

#pragma once

#include <GeoModelKernel/GeoPhysVol.h> // complete GeoPhysVol/GeoVPhysVol for the macro's upcast

#include <cstdio>
#include <cstdlib>
#include <functional>
#include <map>
#include <string>
#include <utility>
#include <vector>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

namespace SHiPGeometry {

class SHiPMaterials;

/**
* @brief A subsystem's self-description: everything the assembler needs.
*
* This is the "required input to the geometry builder" that each subsystem
* yields about itself. It is a plain data type (no GeoModel dependency).
* Translations are in millimetres from the world origin.
*/
struct SubsystemDescriptor {
const char* name; ///< registry key / CLI name, e.g. "Calorimeter"
const char* node; ///< GeoNameTag, e.g. "/SHiP/calorimeter"
int id; ///< GeoIdentifierTag
double x_mm; ///< placement translation X (mm)
double y_mm; ///< placement translation Y (mm)
double z_mm; ///< placement translation Z, beam direction (mm)
bool isWorld = false; ///< true for the mother/world volume (the cavern)
};

/// A registered subsystem: its descriptor plus how to build it (local frame).
struct SubsystemInfo {
SubsystemDescriptor desc;
std::function<GeoVPhysVol*(SHiPMaterials&)> build;
};

/**
* @brief The global subsystem registry.
*
* A Meyers singleton (function-local static in an inline function) so it is
* a single shared instance across all translation units and is guaranteed to
* exist before any static registration runs. No file names any subsystem;
* subsystems add themselves via REGISTER_SUBSYSTEM.
*/
inline std::map<std::string, SubsystemInfo>& registry() {
static std::map<std::string, SubsystemInfo> instance;
return instance;
}

/// Add a subsystem to the registry. Returns true (usable as a static
/// initialiser). A duplicate name is a programming error (two subsystems
/// declaring the same descriptor name): it is reported and aborts, rather
/// than being silently dropped by emplace(). Runs at static-init, so this
/// diagnoses to stderr and aborts instead of throwing.
inline bool registerSubsystem(const SubsystemDescriptor& desc,
std::function<GeoVPhysVol*(SHiPMaterials&)> build) {
const auto result = registry().emplace(desc.name, SubsystemInfo{desc, std::move(build)});
if (!result.second) {
std::fprintf(stderr,
"SHiPGeometry: duplicate subsystem name '%s' registered; "
"each subsystem's descriptor() must return a unique name.\n",
desc.name);
std::abort();
}
return true;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ── Generic consumers — these name no subsystem ─────────────────────────────

/// Assemble the world plus a selection of subsystems (empty selection = all),
/// each placed at its own declared position. Returns the world volume.
GeoPhysVol* assembleGeometry(const std::vector<std::string>& only = {});

/// Build a single subsystem on its own, in its local frame (no world, no
/// placement). Throws std::runtime_error if the name is not registered.
GeoVPhysVol* buildSubsystem(const std::string& name);

/// The names of all registered subsystems (including the world), sorted.
std::vector<std::string> subsystemNames();

} // namespace SHiPGeometry

/**
* @brief Register a subsystem factory with the global registry.
*
* Placed once in each subsystem's own .cpp (inside namespace SHiPGeometry).
* The factory must expose `static SubsystemDescriptor descriptor()` and be
* constructible from `SHiPMaterials&` with a `build()` returning a volume.
*
* NOTE: nothing references this registration, so the subsystem library would
* otherwise be dropped from a consumer's DT_NEEDED by the toolchain's default
* --as-needed and the initialiser would never run. src/CMakeLists.txt applies
* -Wl,--no-as-needed as an INTERFACE link option on SHiPGeometry so the flag
* lands on each executable's link line. Do not remove it.
*/
#define REGISTER_SUBSYSTEM(FACTORY) \
namespace { \
const bool FACTORY##_registered = ::SHiPGeometry::registerSubsystem( \
FACTORY::descriptor(), [](::SHiPGeometry::SHiPMaterials& materials) -> ::GeoVPhysVol* { \
return FACTORY(materials).build(); \
}); \
}
14 changes: 11 additions & 3 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) CERN for the benefit of the SHiP Collaboration

# Create SHiPGeometry library
add_library(SHiPGeometry SHiPMaterials.cpp SHiPGeometry.cpp)

target_include_directories(
SHiPGeometry
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
$<INSTALL_INTERFACE:include>
)

target_link_libraries(
SHiPGeometry
PUBLIC
Expand All @@ -28,3 +25,14 @@ target_link_libraries(
TimingDetector
Calorimeter
)

# The subsystems are referenced only through their REGISTER_SUBSYSTEM static
# initialisers, so the toolchain's default --as-needed drops them from the
# DT_NEEDED of every executable that links SHiPGeometry, and their
# registrations never run. INTERFACE puts --no-as-needed on the *consumer's*
# link line, which is where the entries were being dropped.
target_link_options(SHiPGeometry INTERFACE "-Wl,--no-as-needed")

if(UNIX AND NOT APPLE)
target_link_options(SHiPGeometry INTERFACE "-Wl,--no-as-needed")
endif()
Comment on lines +28 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Remove the unconditional --no-as-needed on line 34; keep only the platform-guarded block.

Line 34 unconditionally adds -Wl,--no-as-needed to INTERFACE_LINK_OPTIONS. This flag is GNU ld-specific — the Apple linker on macOS and MSVC on Windows do not recognize it, causing link failures on those platforms. The if(UNIX AND NOT APPLE) guard on lines 36–38 is the correct place for this option, but line 34 makes the guard ineffective. On Linux, the option is also duplicated (once unconditionally, once conditionally), which is harmless but redundant.

🔧 Proposed fix: remove unconditional call, keep platform guard
 # link line, which is where the entries were being dropped.
-target_link_options(SHiPGeometry INTERFACE "-Wl,--no-as-needed")
 
 if(UNIX AND NOT APPLE)
     target_link_options(SHiPGeometry INTERFACE "-Wl,--no-as-needed")
 endif()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# The subsystems are referenced only through their REGISTER_SUBSYSTEM static
# initialisers, so the toolchain's default --as-needed drops them from the
# DT_NEEDED of every executable that links SHiPGeometry, and their
# registrations never run. INTERFACE puts --no-as-needed on the *consumer's*
# link line, which is where the entries were being dropped.
target_link_options(SHiPGeometry INTERFACE "-Wl,--no-as-needed")
if(UNIX AND NOT APPLE)
target_link_options(SHiPGeometry INTERFACE "-Wl,--no-as-needed")
endif()
# The subsystems are referenced only through their REGISTER_SUBSYSTEM static
# initialisers, so the toolchain's default --as-needed drops them from the
# DT_NEEDED of every executable that links SHiPGeometry, and their
# registrations never run. INTERFACE puts --no-as-needed on the *consumer's*
# link line, which is where the entries were being dropped.
if(UNIX AND NOT APPLE)
target_link_options(SHiPGeometry INTERFACE "-Wl,--no-as-needed")
endif()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CMakeLists.txt` around lines 28 - 38, Remove the unconditional
target_link_options call for SHiPGeometry, leaving only the --no-as-needed
option inside the existing “if(UNIX AND NOT APPLE)” guard. Preserve the guarded
platform-specific linker behavior.

Loading
Loading