This documentation is structured according to the Diátaxis architecture:

StudyWork
PracticalTutorialsHow-To Guides
TheoreticalExplanationReference

Introduction

nain4 has 3 main goals:

  1. Enabling writing Geant4 application code much more concisely: increasing the signal to noise ratio of Geant4 code.

  2. Making it easy to write automated tests for Geant4 application code.

  3. Promoting Geant4 errors from run-time to compile-time.

Why is it called nain4?

Géant and nain are French words:

  • Géant: giant (noun); giant, huge (adjectives)
  • Nain: dwarf, midget (nouns); dwarf, miniature (adjectives)

With nain4, you can express in a tiny amount of code, that which would take huge amounts of code to express in plain Geant4; turn your gigantic source files into miniature ones, without loss of meaning.

How should I pronounce nain4?

Rendered in the International Phonetic Alphabet, the pronunciation of the French word nain, is \nɛ̃\.

In English this is, very roughly, something like nuh.

This sound tends to get drowned out and sound odd, in the middle of a sentence spoken in English, and many other languages. Therefore feel free to pronounce it nano. In Spanish, enano is especially appropriate.

Tutorials

How-To Guides

How to install nix

nain4 is distributed and its dependencies are managed using nix. The reasons for this are explained in why nix?.

On your personal computer

In order to install nix on your personal computer, simply use1

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

The installer will guide you through the installation steps. This installer requires super-user permissions (sudo).

On a HPC cluster

The situation on HPC clusters is different, as we usually we don't have super-user permissions. At the moment the installation of nix in this kind of environments is not fully supported. In the meantime, you may want to check How to make nain4 available in a Geant4/cmake project

direnv

We strongly recommend using direnv to automatically activate and deactivate development environments when entering or leaving a project directory.

To use direnv:

  1. Make sure that it is installed on your system. To install direnv with nix, run

    nix profile install nixpkgs#direnv
    
  2. Don't forget to hook it into your shell. Depending on which shell you are using, this will involve adding one of the following lines to the end of your shell configuration file:

    eval "$(direnv hook bash)"  # in ~/.bashrc
    eval "$(direnv hook zsh)"   # in ~/.zshrc
    eval `direnv hook tcsh`     # in ~/.cshrc
    

The first time direnv wants to perform an automatic switch in a new context (combination of directory + .envrc contents), it asks you for permission to do so. You can give it permission by typing direnv allow in the shell. The message that direnv gives you at this stage is pretty clear, but it's usually written in red, thus you might get the mistaken impression that there is an error.

Now, every time you enter the directory you will see a message like:

direnv: loading .envrc
direnv export: +FOO +BAR +BAZ ...

and when you exit

direnv: unloading

How to make nain4 available in a Geant4/cmake project

There are 3 options to include nain4 in your project:

  1. Adding it as an external project (recommended)
  2. Adding it as a subdirectory in your project
  3. Creating an independent installation

Adding Nain4 as a external project

Add this snippet to your CMakeLists.txt:

include(FetchContent)

set(FETCHCONTENT_UPDATES_DISCONNECTED ON CACHE BOOL "Cache package contents to avoid unnecessary downloads")

FetchContent_Declare(
  nain4
  GIT_REPOSITORY https://github.com/${USERNAME}/nain4.git
  GIT_TAG        ${COMMIT_HASH}
  # make sure that no other nain4 installation is used
  OVERRIDE_FIND_PACKAGE
  SOURCE_SUBDIR nain4/src
)

FetchContent_MakeAvailable(nain4)

The value given to GIT_TAG may be a replaced with a branch name (origin/<branch-name>) or a commit hash. When opting for the 'branch name' option, it's important to be aware of the potential risk of losing reproducibility. With each execution of CMake, the library may be updated to a different state, introducing changes that can impact the reproducibility of your project. This can lead to compilation or runtime errors, making it challenging to recreate specific build or runtime environments. Thus, we advise to use a commit hash or a tag instead.

Adding Nain4 as a subdirectory to your project

Clone the Nain4 repository into your own project or create a symbolic link to the clone. Then simply add

add_subdirectory(nain4/nain4/src)

to your CMakeLists.txt.

Install Nain4 independently

git clone https://github.com/jacg/nain4.git
cd nain4
cmake [-DCMAKE_INSTALL_PREFIX=/path/to/install/] -S nain4 -B build
cmake --build build --target install

Unless the option CMAKE_INSTALL_PREFIX is speficied, the files will be installed in /path/to/nain4/install. Then add to your CMakeLists.txt:

set(nain4_DIR "$ENV{NAIN4_INSTALL}/lib/cmake/nain4/")

find_package(nain4 REQUIRED)

Make sure you have NAIN4_INSTALL set in your shell environment before running cmake or replace it by /path/to/install/ in the CMakeLists.txt file.

Linking to Nain4

Regardless of the chosen option, you will also need to link the library to each target like:

target_link_libraries(
client_exe
PRIVATE
nain4
${Geant4_LIBRARIES}
)

How to start a nain4-based project

The following instructions assume that you have nix installed. If not, please see Install nix.

Setting up a new nain4 project

In order to create a nain4-based project use bootstrap-client-project like this:

nix run github:jacg/nain4#bootstrap-client-project path/to/your/project your-chosen-name "one line project description"

where

  • path/to/your/project should not exist before you run the command.
  • your-chosen-name should be replaced with a name of your choice: this should be a valid identifier in both bash and the Nix language, so, roughly speaking, it should not contain any spaces, slashes or other dodgy characters. It will be used to name various things within your project.
  • "one line project description" should be replaced with some text of your choice. Don't forget the quotes.

Basic usage

  • just run -n 100: run the application in batch mode with /run/beamOn 100
  • just run -g: run the application with the interactive GUI WARNING: unless you are running on
    • NixOS
    • macOS this will probably take a long time the first time you try it, as it will download and compile nixGL which is used to detect graphics hardware automatically and guarantee the presence of the required graphics drivers. On subsequent runs, this overhead will disappear.
  • TODO: describe the early/late CLI options
  • TODO: describe the macro path CLI options

Project contents

Your bootstrapped project will contain the following files

path/to/your/project
├── execute-with-nixgl-if-needed.sh
├── flake
│  └── outputs.nix
├── flake.lock
├── flake.nix
├── justfile
├── macs
│  ├── early-cli.mac
│  ├── early-hard-wired.mac
│  ├── late-cli.mac
│  ├── late-hard-wired.mac
│  ├── run.mac
│  ├── vis.mac
│  └── vis2.mac
├── run-each-test-in-separate-process.sh.in
├── src
│  ├── LXe.cc
│  ├── LXe.hh
│  ├── meson.build
│  └── n4app.cc
└── test
   ├── catch2-main-test.cc
   ├── meson.build
   ├── test-catch2-demo.cc
   └── test-LXe.cc
  • TODO: discuss the contents of src and test

  • The macs directory contains Geant4 macro files, used to provide some app configuration parameters at runtime.

    • macs/run.mac controls aspects of the simulation
    • macs/vis.mac controls aspects of visulisation (only used in interactive mode)
  • The justfile file defines some recipes for easier usage of the application.

  • The flake directory and flake.lock, flake.nix files describe the dependency tree for your project. In most cases, you will not need to read or modify them.

Make it your project

Because this is a template, there are a number of variables that take a generic name. We encourage you to change them to represent something meaningfull to you. You can find these variables by running

grep -Rn CHANGEME .

on the root folder of your project. You will see an output with several lines like

[file]:[line-number]:#CHANGEME: [description]

Open those files with your favourite text editor and rename those variables.

How to create a minimal nain4 app

Building the app

One of the most basic examples of a nain4 app can be found in templates/basic/src/n4app.cc. Here is the full file:

#include <n4-all.hh>

#include <G4GenericMessenger.hh>

#include <G4PrimaryParticle.hh>
#include <G4String.hh>
#include <G4SystemOfUnits.hh>   // physical units such as `m` for metre
#include <G4Event.hh>           // needed to inject primary particles into an event
#include <G4Box.hh>             // for creating shapes in the geometry
#include <G4Sphere.hh>          // for creating shapes in the geometry
#include <FTFP_BERT.hh>         // our choice of physics list
#include <G4RandomDirection.hh> // for launching particles in random directions


#include <G4ThreeVector.hh>
#include <cstdlib>

struct my {
  G4double       straw_radius{0.1 * m};
  G4double      bubble_radius{0.2 * m};
  G4double      socket_rot   {-90 * deg};
  G4String      particle_name{"geantino"};
  G4double      particle_energy{511 * keV};
  G4ThreeVector particle_dir {};
};

auto my_generator(const my& my) {
  return [&](G4Event* event) {
    auto particle_type = n4::find_particle(my.particle_name);
    auto vertex = new G4PrimaryVertex();
    auto r = my.particle_dir.mag2() > 0 ? my.particle_dir : G4RandomDirection();
    vertex -> SetPrimary(new G4PrimaryParticle(
                           particle_type,
                           r.x(), r.y(), r.z(),
                           my.particle_energy
                         ));
    event  -> AddPrimaryVertex(vertex);
  };
}

n4::actions* create_actions(my& my, unsigned& n_event) {
  auto my_stepping_action = [&] (const G4Step* step) {
    auto pt = step -> GetPreStepPoint();
    auto volume_name = pt -> GetTouchable() -> GetVolume() -> GetName();
    if (volume_name == "straw" || volume_name == "bubble") {
      auto pos = pt -> GetPosition();
      std::cout << volume_name << " " << pos << std::endl;
    }
  };

  auto my_event_action = [&] (const G4Event*) {
     n_event++;
     std::cout << "end of event " << n_event << std::endl;
  };

  return (new n4::        actions{my_generator(my)  })
 -> set( (new n4::   event_action{                  }) -> end(my_event_action) )
 -> set(  new n4::stepping_action{my_stepping_action});
}

auto my_geometry(const my& my) {
  auto r_bub = my.bubble_radius;
  auto r_str = my.straw_radius;
  auto water  = n4::material("G4_WATER");
  auto air    = n4::material("G4_AIR");
  auto steel  = n4::material("G4_STAINLESS-STEEL");
  auto world  = n4::box("world").cube(2*m).x(3*m).volume(water);

  n4::sphere("bubble").r(r_bub)         .place(air).in(world).at  (1.3*m, 0.8*m, 0.3*m).now();
  n4::tubs  ("straw" ).r(r_str).z(1.9*m).place(air).in(world).at_x(0.2*m              ).now();

  n4       ::sphere("socket-cap" ).r(0.3*m).phi_delta(180*deg)
    .sub(n4::box   ("socket-hole").cube(0.4*m))
    .name("socket")
    .place(steel).in(world).rotate_x(my.socket_rot).at(1*m, 0, 0.7*m).now();

  return n4::place(world).now();
}

int main(int argc, char* argv[]) {
  unsigned n_event = 0;

  my my;

  G4int physics_verbosity = 0;

  // The trailing slash after '/my_geometry' is CRUCIAL: without it, the
  // messenger violates the principle of least surprise.
  auto messenger = new G4GenericMessenger{nullptr, "/my/", "docs: bla bla bla"};
  messenger -> DeclarePropertyWithUnit("straw_radius"      , "m"  , my. straw_radius  );
  messenger -> DeclarePropertyWithUnit("bubble_radius"     , "m"  , my.bubble_radius  );
  messenger -> DeclarePropertyWithUnit("socket_rot"        , "deg", my.socket_rot     );
  messenger -> DeclarePropertyWithUnit("particle_energy"   , "keV", my.particle_energy);
  messenger -> DeclareProperty        ("particle"          ,        my.particle_name  );
  messenger -> DeclareProperty        ("particle_direction",        my.particle_dir   );
  messenger -> DeclareProperty        ("physics_verbosity" ,        physics_verbosity );

  n4::run_manager::create()
    .ui("CHANGEME-EXE", argc, argv)
    .macro_path("macs")
    .apply_command("/my/straw_radius 0.5 m")
    .apply_early_macro("early-hard-wired.mac")
    .apply_cli_early() // CLI --early executed at this point
    // .apply_command(...) // also possible after apply_early_macro

    .physics<FTFP_BERT>(physics_verbosity)
    .geometry([&] { return my_geometry(my); })
    .actions(create_actions(my, n_event))

    .apply_command("/my/particle e-")
    .apply_late_macro("late-hard-wired.mac")
    .apply_cli_late() // CLI --late executed at this point
    // .apply_command(...) // also possible after apply_late_macro

    .run();



  // Important! physics list has to be set before the generator!

}

Now, let's split it into bite-sized portions. First we create a simple program that reads the number of events from CLI:

int main(int argc, char* argv[]) {
}

In order to run a simulation we need to create a run manager:

  n4::run_manager::create()
    .ui("CHANGEME-EXE", argc, argv)
    .macro_path("macs")
    .apply_command("/my/straw_radius 0.5 m")
    .apply_early_macro("early-hard-wired.mac")
    .apply_cli_early() // CLI --early executed at this point
    // .apply_command(...) // also possible after apply_early_macro

    .physics<FTFP_BERT>(physics_verbosity)
    .geometry([&] { return my_geometry(my); })
    .actions(create_actions(my, n_event))

    .apply_command("/my/particle e-")
    .apply_late_macro("late-hard-wired.mac")
    .apply_cli_late() // CLI --late executed at this point
    // .apply_command(...) // also possible after apply_late_macro

    .run();


The run manager needs to be initialized with 3 attributes:

  • A physics list
  • A geometry
  • A set of actions

To build the action set we need to provide a primary generator action. Other actions are optional. Note that the physics list must be instantiated and given to the run manager before the primary generator is instantiated. We provide the run manager with these attributes:

  // Important! physics list has to be set before the generator!

here, my_geometry, and my_generator are the two functions we need to provide to run our simulation:

auto my_geometry(const my& my) {
  auto r_bub = my.bubble_radius;
  auto r_str = my.straw_radius;
  auto water  = n4::material("G4_WATER");
  auto air    = n4::material("G4_AIR");
  auto steel  = n4::material("G4_STAINLESS-STEEL");
  auto world  = n4::box("world").cube(2*m).x(3*m).volume(water);

  n4::sphere("bubble").r(r_bub)         .place(air).in(world).at  (1.3*m, 0.8*m, 0.3*m).now();
  n4::tubs  ("straw" ).r(r_str).z(1.9*m).place(air).in(world).at_x(0.2*m              ).now();

  n4       ::sphere("socket-cap" ).r(0.3*m).phi_delta(180*deg)
    .sub(n4::box   ("socket-hole").cube(0.4*m))
    .name("socket")
    .place(steel).in(world).rotate_x(my.socket_rot).at(1*m, 0, 0.7*m).now();

  return n4::place(world).now();
}

auto my_generator(const my& my) {
  return [&](G4Event* event) {
    auto particle_type = n4::find_particle(my.particle_name);
    auto vertex = new G4PrimaryVertex();
    auto r = my.particle_dir.mag2() > 0 ? my.particle_dir : G4RandomDirection();
    vertex -> SetPrimary(new G4PrimaryParticle(
                           particle_type,
                           r.x(), r.y(), r.z(),
                           my.particle_energy
                         ));
    event  -> AddPrimaryVertex(vertex);
  };
}

and create_actions is defined as:

n4::actions* create_actions(my& my, unsigned& n_event) {
  auto my_stepping_action = [&] (const G4Step* step) {
    auto pt = step -> GetPreStepPoint();
    auto volume_name = pt -> GetTouchable() -> GetVolume() -> GetName();
    if (volume_name == "straw" || volume_name == "bubble") {
      auto pos = pt -> GetPosition();
      std::cout << volume_name << " " << pos << std::endl;
    }
  };

  auto my_event_action = [&] (const G4Event*) {
     n_event++;
     std::cout << "end of event " << n_event << std::endl;
  };

  return (new n4::        actions{my_generator(my)  })
 -> set( (new n4::   event_action{                  }) -> end(my_event_action) )
 -> set(  new n4::stepping_action{my_stepping_action});
}

With this, our setup is ready, and we can start the simulation with


Don't forget to add the relevant includes

#include <n4-all.hh>

#include <G4GenericMessenger.hh>

#include <G4PrimaryParticle.hh>
#include <G4String.hh>
#include <G4SystemOfUnits.hh>   // physical units such as `m` for metre
#include <G4Event.hh>           // needed to inject primary particles into an event
#include <G4Box.hh>             // for creating shapes in the geometry
#include <G4Sphere.hh>          // for creating shapes in the geometry
#include <FTFP_BERT.hh>         // our choice of physics list
#include <G4RandomDirection.hh> // for launching particles in random directions


#include <G4ThreeVector.hh>
#include <cstdlib>

Compiling the app

The app comes with its own CMakeLists.txt:

{{#include ../../../examples/00-basic/CMakeLists.txt:full_file}}

This contains the following bits. First, we have the project initialization:

{{#include ../../../examples/00-basic/CMakeLists.txt:project_setup}}

It specifies the minimum cmake version, defines the project name and some lines necessary for code analysis in IDEs. Next, we include nain4 and Geant4 in our project:

{{#include ../../../examples/00-basic/CMakeLists.txt:include_nain4_geant4}}

Note that we have chosen to use an existing installation of nain4 so this example runs out of the box when run within nain4. If you were to copy this example and run it elsewhere, make sure to check How to make nain4 available in a Geant4/cmake project.

Finally, we create an executable file and link it to the relevant libraries.

{{#include ../../../examples/00-basic/CMakeLists.txt:create_exe_and_link}}

To compile the app we cd into 00-basic and run just compile-app or

cmake -S . -B build && cmake --build build

Running the app

To run this app type just run-app <number of events> (after compilation) from the 00-basic directory. You can also achieve the same typing ./build/n4-00-basic <number of events>.

As this simulation is extremelly basic, it doesn't produce anything. You will only see a Geant4 header.

How to upgrade your nain4 dependency

To upgrade nain4 to the lastest version available on master:

  1. nix flake lock --update-input nain4

    This operation modifies flake.lock. Commit these modifications, either right now, or after step 3.

  2. just clean

    This ensures that there are no old build products which still use the old version. (Hopefully this will no longer be necessary once nain4 migrates its build infrastructure from cmake to meson.)

  3. Build, run and test the behaviour of your project, and fix any problems caused by incompatibility of your code with the new version of nain4.

  4. Commit these changes to your repository. Don't forget to commit the modified flake.lock; this is important in order to ensure that every checkout of a given commit in your repository uses exactly the same version of dependencies and that the versions of the dependencies are compatible with the version of your code.

Using other versions of nain4

It is possible to use versions of nain4 other than those available on the official master branch. See here for details.

How to generate random numbers

nain4 offers a range of utilities to generate random numbers. This includes scalars, tuples and 3-vectors. All these methods are declared in the header <n4-random.hh> which is transitively included by <n4-utils.hh>. They are gathered in the namespace nain4::random.

Scalars

The methods described in this section generate a single number.

Continuous distributions

  • uniform(): generates a random floating point number homogeneously distributed in the range [0, 1).
  • uniform(low, high): generates a random floating point number homogeneously distributed in the range [low, high).
  • uniform_half_width(hw): generates a random floating point number homogeneously distributed in the range [-hw, hw).
  • uniform_width(w): generates a random floating point number homogeneously distributed in the range [-w/2, w/2).

Discrete distributions

  • biased_coin(p_true): generates a random boolean with probability p_true of being true.
  • fair_die(n_sides): generates a random unsigned integer in the range [0, n_sides - 1] with equal probability.
  • gen = biased_choice(weights): provides a generator of unsigned integers in the range [0, weights.size() - 1] with given weights. The generator must be called to obtain a number: random_number = gen().

Tuples

  • random_in_disc(r): generates a pair of floating point numbers {x, y} that satisfy x^2 + y^2 <= r^2.

3-vectors

The methods described in this section generate G4ThreeVectors.

  • random_in_sphere(r): generates a vector of floating point numbers {x, y, z} that satisfy x^2 + y^2 + z^2 <= r^2.

Directions

nain4 provides the direction builder to help generate random directions. By default this tool generates unit vectors distributed isotropically in 4π, but it can be configured to restrict the generation to certain directions.

The general usage pattern is

auto generator = n4::random::direction{}.<optional extra specifications>;
auto one_random_unit_vector = generator.get();

The <optional extra specifications> are described below. θ (theta) is the polar angle measured with respect to the positive z-axis and φ (phi) is the azimuthal angle with φ=0 in the plane of the positive x-axis and increasing towards the positive y-axis, unless the axes are re-oriented with .rotate_{x,y,z}.

The following specifiers (with hopefully self-explanatory names) restrict the angles of the generated directions

  • {min,max}_theta(<angle in radians>) or (<angle in degrees> * deg)

    • range: [0, π] or [0, 180°]
  • {min,max}_cos_theta(<ratio>) range: [-1, 1]

  • {min,max}_phi(<angle in radians>) or (<angle in degrees> * deg)

    • range: [0, 2π] or [0, 360°]
  • rotate_{x,y,z}(<angle in radians>) or (<angle in degrees> * deg)

    rotation around the specified axis. range: [0, 2π] or [0, 360°]

  • rotate(<G4RotationMatrix>)

  • bidirectional(): accept both the current selection and its reflection in the origin.

  • exclude(): invert the selection. Generates the complement of the selected criteria. Bear in mind that this method is more computationally expensive as it needs to generate directions until the result is not rejected. Very restricted directionalities are discouraged.

Examples

TODO: add pictures

  • Isotropic emission: n4::direction{}

  • Opening angle around the positive z axis: n4::direction().max_theta(theta)

  • Opening angle around the positive x axis: n4::direction().max_theta(theta).rotate_y(90 * deg)

  • Only one octant: n4::direction().min_cos_theta(0).min_phi(CLHEP::halfpi).max_phi(CLHEP::pi)

  • Back-to-back beams around the positive z axis with some opening angle: n4::direction().max_theta(theta).bidirectional()

  • Isotropic except for some small angle around the negative z-axis: n4::direction().min_theta(7*CLHEP::pi/8).exclude()

Explanation

  • Functions better than classes
  • Closures
  • Always prefer std::vector over C-arrays.

Placement: Laziness and Accumulation

The auxiliary placement methods belong to an object of type n4::place. Instances of this type can accumulate state.

This can be useful for reducing repetition: compare the following two code samples, which produce identical results

// Store complicated configuration
auto place_box = n4::box("box").cube(1*m).place(gold).in(world).at_x(1*m);
// Reuse it many times, emphasizing what differs: rot_z(angle) and copy_no(N)
place_box.clone().rot_z(23*deg).copy_no(1).now();
place_box.clone().rot_z(54*deg).copy_no(2).now();
place_box.clone().rot_z(91*deg).copy_no(3).now();
// Repeat complicated configuration explicitly: signal lost in the noise 
n4::box("box").cube(1*m).place(gold).in(world).at_x(1*m).copy_no(1).rot_z(23*deg).now();
n4::box("box").cube(1*m).place(gold).in(world).at_x(1*m).copy_no(2).rot_z(54*deg).now();
n4::box("box").cube(1*m).place(gold).in(world).at_x(1*m).copy_no(3).rot_z(91*deg).now();

However, note the presence of .clone() on each line of the first sample. Without it, the effect of each call to rot_z would be accumulated in place_box

place_box = n4::box("box").cube(1*m).place(copper).in(world);

// Place boxes at 1, 2 and 3 metres
for (auto k : {1,2,3}) { place_box.clone().at_x(k*m).now(); }
// Place boxes at 1, 3 and 6 metres
for (auto k : {1,2,3}) { place_box        .at_x(k*m).now(); }

Displacements and rotations are not commutative

TODO

Input types for boolean solids' methods

nain4 provides a collection of methods to create boolean solids described in n4 boolean solids. These functions accept either a n4::shape or G4VSolid*. However, when building a geometry, it's fairly easy to get confused with the different stages of the creation of components (solid, (logical)volume, place(ment)). This can produce errors that might be difficult to understand. nain4 attempts to clarify them by providing explicit compile-time messages for the most likely cases. Here we describe all these errors and try to give a through explanation of their causes and how to handle them.

For this explanation we will use the addition of two boxes as an example, but the rationale does not depend on the shapes or the methods used. Here is the definition of the boxes:

n4::box box1 = n4::box("box1").cube(1*m);
n4::box box2 = n4::box("box2").cube(2*m);
G4Box*  box3 = new G4Box{"box3", 3*m, 3*m, 3*m};

The correct syntax

For reference, here are three possible options to create a union solid from these boxes:

box1.add(box2        ); // OK
box1.add(box2.solid()); // OK
box1.add(box3        ); // OK

Here, add could be replaced with its alias join to produce the same result. Also, notice that the union solid is not created until .solid(), volume(...) or .place(...) is called on the result of add(...).

Possible errors

All the errors share the same message, with a hint of what the type of the incorrect input is. The error message looks like this:

[n4::boolean_shape::<METHOD>]
Attempted to create a boolean shape using <TYPE_HINT>.
Only n4::shape and G4VSolid* are accepted.
For more details, please check https://jacg.github.io/nain4/explanation/boolean_solid_input_types.md

where <METHOD> indicates the method which generated the error (this might help in the case of chained operations) and <TYPE_HINT> indicates the input type that gave rise to the error.

<TYPE_HINT> = a G4LogicalVolume

The variable passed to the boolean-creation method is of type G4LogicalVolume*. The most likely causes are:

  1. [G4-style] this variable was bound to a new G4LogicalVolume(...), e.g.

    G4Box*           box2_solid = new G4Box(...);
    G4LogicalVolume* box2_logic = new G4LogicalVolume(box2_solid, ...);
    box1.add(box2_logic); // WRONG
    

    The solution is to pass box2_solid instead of box2_logic.

  2. [n4-style] you called .volume(...) on the input solid, e.g.

    box1.add(box2.volume(...)); // WRONG
    

    The solution is to remove the call to volume(...) or call .solid() instead.

<TYPE_HINT> = a G4PVPlacement

The variable passed to the boolean-creation method is of type G4PVPlacement*. The most likely causes are:

  1. [G4-style] this variable was assigned to new G4PVPlacement(...), e.g.

    G4Box*           box2_solid = new G4Box(...);
    G4LogicalVolume* box2_logic = new G4LogicalVolume(box2_solid, ...);
    G4PVPlacement*   box2_place = new G4PVPlacement(box2_logic, ...);
    box1.add(box2_place); // WRONG
    

    The solution is to pass box2_solid instead of box2_place.

  2. [n4-style] you called .place(...).other().methods().now() on the input solid, e.g.

    box1.add(box2.place(...).other().methods().now()); // WRONG
    

    The solution is to remove the call to place(...).other().methods().now() or call .solid() instead.

<TYPE_HINT> = a n4::place

The variable passed to the boolean-creation method is of type n4::place. The most likely cause is that you called .place(...) (without calling .now()) on the input solid, e.g.

box1.add(box2.place(...).other().methods()); // WRONG

The solution is to remove the call to place(...).other().methods() or call .solid() instead.

<TYPE_HINT> = an unknown type

The type of the variable passed to the boolean-creation method is neither a valid one (n4::shape, G4VSolid*) nor one of the specific cases described above. In this case nain4 cannot provide any help. You must find the latest assignment of this variable and understand what its type is. A LSP plugin in you text editor might help.

Requesting the handling of other cases

If you find that there is another case that nain4 should handle, please open an issue at https://github.com/jacg/nain4/issues!

Return value of G4VSensitiveDetector::ProcessHits

G4VSensitiveDetector's method ProcessHits must be implemented by the user. nain4 provides n4::sensitive_detector to ease implementing G4VSensitiveDetector subclasses. Because ProcessHits is mandatory, nain4 obliges the user to provide an implementation as a construction argument to n4::sensitive_detector.

The aforementioned method returns bool for historical reasons. (This seems to be undocumented, but see this Geant4 forum comment.)

The summary is:

  • The actual value returned doesn't matter: it is ignored by G4.
  • One day the Geant4 developers may decide to change the return type to void at which point old code will give compilation errors.

Why nix?

TODO

Reference

Library headers

nain4 provides headers of different granularity, for convenience in different situations.

<n4-all.hh>

Gives access to all nain4 components. This header is meant to be used for exploration:

  • when you are just getting started with nain4 and don't want to be distracted by the need to find specific headers for specific situations

  • when you want LSP to help you discover available features

  • When your whole nain4 application still lives in a single source file

As your program grows and is split into multiple files, and you gain more experience with nain4, it would be more appropriate to use the more fine-grained headers, described below.

  • <n4-main.hh>

    Groups together utilities that are required to write the main function of a nain4 application. These are also available via the separate headers:

    • <n4-run-manager.hh>: nain4 interface for configuring the Geant4 run manager
    • <n4-mandatory.hh>: nain4 utilities for concise implementation of the user-defined classes that must be registered with the run manager
  • <n4-geometry.hh>

    Groups together functionality that is typically used in defining detector geometries. These are also available via the separate headers:

    • <n4-material>: finding existing materials; constructing new materials
    • <n4-shape>: construction of G4VSolids
    • <n4-boolean-shape>: construction of boolean solids
    • <n4-volume> creation of logical volumes
    • <n4-place> creation and placement of physical volumes
    • <n4-sensitive> nain4 utilities for concise implementation of G4VSensitiveDetector
  • <n4-utils.hh>

    Various ready-made conveniences to alleviate the tedium and verbosity of oft-encountered tasks. These are also available via the separate headers:

    • <n4-constants.hh>: physical constants not provided by CLHEP
    • <n4-inspect.hh>: finding existing geometry components, materials, etc.
    • <n4-random.hh>: random number generation
    • <n4-sequences.hh>: convenient creation of sequences of numerical data
    • <n4-stream.hh>: redirecting or silencing C++ output streams
  • <n4-testing.hh>

    Utilities to help with writing automated tests for nain4 applications. These are also available via the separate headers:

    • <n4-defaults.hh>: ready-made dummy application components
    • <n4-geometry-iterators.hh>: iterating over all sub-elements of a geometry
  • Other headers

    • <n4-exceptions.hh>: exceptions relating to situations arising in nain4 code

Constructing solids and logical volumes

Header: <n4-shapes.hh>

Summary

Constructing a G4VSolid

auto ball = n4::sphere("ball").r(1.2*m).solid();
auto radius = 1.2*m;
auto ball   = new G4Sphere("ball", 0, radius, 0, CLHEP::twopi, 0, CLHEP::pi);

(In this specific example, n4::sphere notices that it would be more efficient to create a G4Orb instead of a G4Sphere and does that for you automatically.)

Constructing a G4LogicalVolume

Frequently, after having made a G4VSolid you immediately use it to make a G4LogicalVolume with the same name. nain4 allows you to do this in a single step:

auto copper = n4::material("G4_Cu");
auto ball   = n4::sphere("ball").r(1.2*m).volume(copper);
auto copper = G4NistManager::Instance() -> FindOrBuildMaterial("G4_Cu");
auto radius = 1.2*m;
auto ball_solid = new G4Sphere("ball", 0, radius, 0, CLHEP::twopi, 0, CLHEP::pi);
auto ball = new G4VLogicalVolume(ball_solid, copper, "ball");

If you need to do this as two separate steps

auto copper     = n4::material("G4_Cu");
auto ball_solid = n4::sphere("ball").r(1.2*m).solid();
auto ball       = n4::volume(ball_solid, copper);

Not all G4VSolids are supported by the nain4::shape interface, yet. In such cases n4::volume lets you combine the construction of a solid and corresponding logical volume in a single step. Here is how you could do it if n4::sphere did not exist:

auto copper = n4::material("G4_Cu");
auto radius = 1.2*m;
auto ball   = n4::volume<G4Sphere>("ball", copper, 0, radius, 0, CLHEP::twopi, 0, CLHEP::pi);

The arguments passed after the name and material, are forwarded to the specified G4VSolid's constructor.

Placing a volume

Should you want to place your G4LogicalVolume immediately, nain4 allows you to include this in a single step, too:

auto safe = ...
auto gold = n4::material("G4_Au");
n4::box("nugget").cube(2*cm).place(gold).in(safe).now();
auto safe_solid = ...
auto safe = ...
auto gold = G4NistManager::Instance() -> FindOrBuildMaterial("G4_Au");
auto nugget_solid = new G4Box("nugget", 2*cm/2, 2*cm/2, 2*cm/2);
auto nugget_logical = new G4VLogicalVolume(nugget_solid, gold, "nugget");
new G4PVPlacement(nullptr, {}, nugget_logical, "nugget", safe, false, 0);

However, frequently you need to keep a handle to the logical volume, in order to be able to place things into it later. In such cases you would break this into two separate steps:

auto safe   = ...
auto gold   = n4::material("G4_Au");
auto nugget = n4::box("nugget").cube(2*cm).volume(gold);
n4::place(nugget).in(safe).now();

nain4's placement utilities have a rich interface, which is described in Placement of physical volumes.

Summary

G4VSolid        * s = n4::SOLID("name", ...).solid();
G4VLogicalVolume* v = n4::SOLID("name", ...).volume(material);
G4PVPlacement   * p = n4::SOLID("name", ...).place (material).in(volume) ... .now();

Specifying Dimensions

The G4VSolids, and hence also the n4::shapes, are parameterized by combinations of four principal types of coordinate

  1. Cartesian lengths, x, y and z
  2. Radial length, r
  3. Azimuthal angle, φ
  4. Polar angle, θ

nain4 provides a consistent set of methods for setting these, in any n4::shape that uses them. These methods are described here, the n4::shapes are described in the next section. All the methods provided by nain4 have short, but explicit names. This removes the need for the user to remember the order of the arguments while highlighting their meaning.

Cartesian lengths

The principal methods for setting Cartesian lengths are

  • x
  • half_x

and their equivalents for y and z.

Explicit names

Unlike Geant4, nain4 favours the use of full lengths instead of half-lengths. Using half-lengths creates noise by either introducing /2 operations continuously or by defining variables with long names to indicate the subtle but important distinction between half- and full-lengths. For instance, in order to create a cube of side 1 m in pure G4:

auto box_half_length = 0.5*m;
G4Box* box1 = new G4Box("box", box_half_length,  box_half_length,  box_half_length);
// or
auto box_length = 1*m;
G4Box* box2 = new G4Box("box", box_length/2,  box_length/2,  box_length/2);

In contrast, in nain4 one can simply write

auto box = n4::box("box").cube(1*m);

Overriding

If you set a Cartesian length more than once in the same shape, the last setting overrides previous ones. For example:

.x(1*m).half_x(3*m)  // `x` set to 6 m
.x(1*m).     x(3*m)  // `x` set to 3 m

Convenient alternatives

n4::shapes which depend on more than one Cartesian length, typically provide extra methods for setting various combinations, for example n4::box offers extra methods cube, xyz, xy, xz and yz along with their half_ variants.

Radial length: r

Three methods are provided for specifying the two degrees of freedom in radial lengths:

  • r_inner
  • r_delta
  • r

with the constraint r = r_inner + r_delta. Thus valid combinations of these methods are

Methods used Implied value
r_innerr_deltar
r 0 r
r_inner r r - r_inner
r_deltar r - r_delta
r_innerr_delta 0 r_inner + r_delta

Providing too few or too many values results in a run-time error.

Some shapes, such as n4::cons (G4Cons), have multiple radii. In such cases the method names acquire a number, to distinguish between them r* -> r1*, r2*.

Azimuthal angle: φ

Three methods are provided for specifying the two degrees of freedom in azimuthal angles:

  • phi_start
  • phi_delta
  • phi_end

with the constraint phi_end = phi_start + phi_delta. Thus valid combinations of these methods are

Methods used Implied value
phi_startphi_deltaphi_end
0
phi_start 2π - start
phi_delta 0 δ
phi_end 0 end
phi_deltaphi_end end - δ
phi_start phi_end end - start
phi_startphi_delta start + δ

Providing too few or too many values results in a run-time error.

Polar angle: θ

Three methods are provided for specifying the two degrees of freedom in polar angles:

  • theta_start
  • theta_delta
  • theta_end

with the constraint theta_end = theta_start + theta_delta. Thus valid combinations of these methods are

Methods used Implied value
theta_starttheta_deltatheta_end
0 π π
theta_start π - start π
theta_delta 0 δ
theta_end 0 end
theta_deltatheta_end end - δ
theta_start theta_end end - start
theta_starttheta_delta start + δ

Providing too few or too many values results in a run-time error.

Common methods

All n4::SOLIDs share the following methods:

Available Shapes

n4::box

Constructs G4Box: cuboid with side lengths x, y and z. Within its frame of reference it is centred on the origin with sides parallel to the x/y/z axes. Displacements and rotations can be applied with .place(material).

Example

G4Box* box = n4::box("box").xz(10*cm).y(50*cm).solid();
auto cross_section_length = 10*cm, y_length = 50*cm;
G4Box* box = new G4Box("box", cross_section_length/2, y_length/2, cross_section_length/2);

Methods

Full-length methods

All these methods take full (as opposed to half-) lengths:

  • x(lx), y(ly), z(ly): set one dimension.
  • xy(l), xz(l), yz(l): set two dimensions to the same value.
  • xyz(lx, ly, lz), xyz(g4-three-vector): set three dimensions independently
  • cube(l): set all dimensions to the same value.

Note the, perhaps surprising, difference between .xyz() and the .xy()-.xz()-.yz() triumvirate: The latter assign a single value to multiple coordinates; the former accepts a separate value for each coordinate it sets.

Half-length methods

All the aforementioned full-length methods have alternatives which accept half-lengths: half_x(lx/2), half_cube(l/2), half_xy(lxy), etc.

Overriding

If any value is specified more than once, the last setting overrides any earlier ones. Thus, the following three lines are equivalent.

.cube(1*m          ).z(2*m)
.xyz (1*m, 1*m, 1*m).z(2*m)
.xy  (1*m          ).z(2*m)

While the first two work, the last one states the intent most clearly.

See the sections about setting Cartesian lengths for more details.

n4::sphere

Constructs G4Sphere or G4Orb, depending on values provided.

  • G4Sphere: section of a spherical shell, between specified azimuthal (φ) and polar (θ) angles. Within its frame of reference, φ is measured counterclockwise WRT the x-axis when viewed from positive z; θ is measured WRT positive z. Displacements and rotations can be applied with .place(material).
  • G4Orb: special case of G4Sphere.

Examples

A solid sphere

G4Orb* ball = n4::sphere("ball").r(1*m).solid();
G4Orb* ball = new G4Orb("ball", 1*m);

thus nain4 helps you avoid the common mistake of creating an equivalent (but less efficient) G4Sphere instead

G4Sphere* ball = new G4Sphere("ball", 0, 1*m, 0, CLHEP::twopi, 0, CLHEP::pi);

A hollow sphere

G4Sphere* hollow = n4::sphere("hollow").r(2*m).r_delta(10*cm).solid();
auto outer = 2*m, thickness = 10*cm;
G4Sphere* hollow = new G4Sphere("hollow", outer - thickness, outer, 0, CLHEP::twopi, 0, CLHEP::pi);

A spherical wedge

G4Sphere* wedge = n4::sphere("wedge").r(1*m).phi_start(20*deg).phi_end(30*deg).solid();
auto start_phi = 20*deg, end_phi = 30*deg;
G4Sphere* wedge = new G4Sphere("wedge", 0, 1*m, start_phi, end_phi - start_phi, 0, CLHEP::pi);

Methods

  • r_inner(l)
  • r_delta(l)
  • r(l)
  • phi_start(a)
  • phi_delta(a)
  • phi_end(a)
  • theta_start(a)
  • theta_delta(a)
  • theta_end(a)

See the sections about setting radial lengths, azimuthal angles and polar angles for more details.

n4::tubs

Constructs G4Tubs: tube or tube segment. Within its frame of reference, it is parallel to and centred on the z-axis; φ is measured counterclockwise WRT the x-axis when viewed from positive z. Displacements and rotations can be applied with .place(material).

Examples

A solid cylinder

G4Tubs* cylinder = n4::tubs("cylinder").r(1*m).z(2*m).solid();
auto radius   = 1*m;
auto z_length = 2*m;
G4Tubs* cylinder = new G4Tubs("cylinder", 0, radius, z_length/2, 0, CLHEP::twopi);

A tube

G4Tubs* tube = n4::tubs("tube").r(1*m).r_delta(10*cm).z(2*m).solid();
auto z_length = 2*m, outer_radius = 1*m, thickness = 10*cm;
G4Tubs* tube = new G4Tubs("tube", outer_radius - thickness, 1*m, z_length/2, 0, CLHEP::twopi);

A cylindrical wedge

G4Tubs* wedge = n4::tubs("wedge").r(1*m).z(2*m).phi_start(20*deg).phi_end(30*deg).solid();
auto z_length = 2*m, radius = 1*m, start_phi = 20*deg, end_phi = 30*deg;
G4Tubs* wedge = new G4Tubs("wedge", 0, radius, z_length/2, start_phi, end_phi - start_phi);

Methods

  • z()
  • half_z()
  • r_inner(l)
  • r_delta(l)
  • r(l)
  • phi_start(a)
  • phi_delta(a)
  • phi_end(a)

See the sections about setting Cartesian lengths, radial lengths and azimuthal angles for more details.

n4::trd

Constructs G4Trd: frustrum (a.k.a. truncated pyramid) with two parallel rectangular faces and four trapezoidal faces. The rectangular faces are perpendicular to the z axis and their sides are parallel to the x and y axes. The bottom face (placed at negative z) has side lengths x1, y1, and the top face (placed at positive z) has side lengths x2, y2. Both faces are separated by a distance z. Within its frame of reference it is centred on the origin. Displacements and rotations can be applied with .place(material).

Example

G4Trd* trd = n4::trd("trd").xy1(10*cm).xy2(5*cm).z(50*cm).solid();
auto cross_section_bottom = 10*cm, cross_section_top = 5*cm, z_length = 50*cm;
G4Trd* trd = new G4Trd("trd", cross_section_bottom/2, cross_section_top/2, cross_section_bottom/2, cross_section_top/2, z_length/2);

Methods

Full-length methods

All these methods take full (as opposed to half-) lengths:

  • x1(lx), x2(ly), y1(lx), y2(ly): set one dimension of one rectangular face. 1 Refers to the bottom face, while 2 refers to the top face.
  • xy1(l), xy2(l): set both dimensions of one face. 1 Refers to the bottom face, while 2 refers to the top face.
  • z(l): set the distance between the two parallel faces.
Half-length methods

All the aforementioned full-length methods have alternatives which accept half-lengths: half_x1(lx/2), half_xy2(lxy/2), half_z(lz/2), etc.

See the sections about setting Cartesian lengths for more details.

n4::cons

Constructs G4Cons: truncated cone or cone section. Within its frame of reference, it is parallel to and centred on the z-axis; φ is measured counterclockwise WRT the x-axis when viewed from positive z. Displacements and rotations can be applied with .place(material).

Examples

A solid truncated cone

G4Tubs* solid_cone = n4::tubs("solid_cone").r1(1*m).r2(2*m).z(3*m).solid();
auto radius_1 = 1*m, radius_2 = 2*m, z_length = 3*m;
G4Cons* solid_cone = new G4Cons("solid_cone", 0, radius_1, 0, radius_2, z_length/2, 0, CLHEP::twopi);

A hollow cone

G4Cone* hollow_cone = n4::cone("hollow_cone").r1(1*m).r2(2*cm).r_delta(10*cm).z(3*m).solid();
auto radius_1 = 1*m, radius_2 = 2*m, thickness = 10*cm, z_length = 3*m;
G4Cons* hollow_cone = new G4Cons("hollow_cone", radius_1 - thickness, radius_1, radius_2 - thickness, radius_2, z_length/2, 0, CLHEP::twopi);

A conical wedge

G4Cons* wedge = n4::cons("wedge").r1(1*m).r2(2*m).z(3*m).phi_start(20*deg).phi_end(30*deg).solid();
auto radius_1 = 1*m, radius_2 = 2*m, z_length = 3*m, start_phi = 20*deg, end_phi = 30*deg;
G4Cons* wedge = new G4Cons("wedge", 0, radius_1, 0, radius_2, z_length/2, start_phi, end_phi - start_phi);

Methods

  • z()
  • half_z()
  • r1_inner(l), r1_delta(l), r1(l): Sets the radial limits of the bottom (negative z) side
  • r2_inner(l), r2_delta(l), r2(l): Sets the radial limits of the top (positive z) side
  • r_delta: Sets both r1_delta and r2_delta
  • phi_start(a)
  • phi_delta(a)
  • phi_end(a)

See the sections about setting Cartesian lengths, radial lengths and azimuthal angles for more details.

Obviously missing shapes

n4::torus

n4::trap

n4::para

n4::polycone

n4::elliptical_tube

n4::ellipse

n4::ellipsoid

n4::elliptical_cone

n4::tet (...rahedron)

n4::hype (...rbolic cone)

n4::twisted_*

n4::breps Boundary Represented Solids

n4::tesselated_solids

TODO: document envelope_of

Placement of physical volumes

Summary

World volumes

nain4's placement API can be used to place a logical volume as it is being created

auto air = n4::material("G4_AIR");
n4::box("world").cube("1*m").place(air).now();
auto air = G4NistManager::Instance() -> FindOrBuildMaterial("G4_AIR");
auto world_size = 1*m;
auto world_solid = new G4Box("world", world_size/2, world_size/2, world_size/2);
auto world_logical = new G4LogicalVolume(world_solid, air, "world")
new G4PVPlacement(nullptr, {}, world_logical, "world", nullptr, false, 0);

or to place an existing logical volume

auto air = n4::material("G4_AIR");
G4LogicalVolume* existing_logical_volume = n4::box("world").cube(1*m).volume(air);
n4::place(existing_logical_volume).now();

(The Geant4 equivalent of this sample is essentially identical to that of the previous sample.)

In the above examples, no mother volume was specified in the placement phase, therefore these volumes are world volumes: all other volumes must be placed within the world volume—either directly, or in its daughters—and they must not protrude beyond the bounds of the world volume.

NOTE: the volume is not placed until the `.now()`

method is called. See Placement: Laziness and Accumulation for more details.

Daughter volumes

  • Daughter volumes are placed inside a mother volume with the .in(mother) method.
  • The position and orientation, as well as other information about the placement, can be specified with the placement API's auxiliary methods.

Just like in the case of world volumes, a logical volume can be placed as it is being created

auto copper = n4::material("G4_Cu");
n4::box("box").cube(1*cm).place(copper).in(world).at_y(3*cm).rot_z(30*deg).now();
auto copper = G4NistManager::Instance() -> FindOrBuildMaterial("G4_Cu");
auto box_size = 1*cm;
auto box_solid = new G4Box("box", box_size/2, box_size/2, box_size/2);
auto box_logical = new G4LogicalVolume(box_solid, copper, "box")
auto rot_z_30 = new G4RotationMatrix();
rot_z_30 -> rotateZ(30*deg);
new G4PVPlacement(rot_z_30, {0, 3*cm, 0}, box_logical, "box", world, false, 0);

or in two separate steps

auto copper = n4::material("G4_Cu");
auto box = n4::box("box").cube(1*cm).volume(copper);
n4::place(box).in(world).at_y(3*cm).rot_z(30*deg).now();

Methods

  • Mother volume

    • in(logical-volume)
    • in(physical-volume) // not implemented yet
    • in(n4::place) // not implemented yet

    If no mother volume is specified, the placed volume becomes the world volume. There can be only one world volume in a Geant4 application. nain4 issues a run-time error if more than one world volume is created.

  • Translations

    • at(G4ThreeVector)
    • at(x,y,z)
    • at_x(x), at_y(y), at_z(z)

    The above methods specify the displacement of the daughter volume's origin with respect to the mother volume's origin.

    By default there is no displacement between the two.

    The effect of these methods is cumulative. Thus the following are equivalent

    .at  (1*m,      2*m,      3*m)
    .at_x(1*m).at_y(2*m).at_z(3*m)
    

    as are these

    .at_x(101*m)
    .at_x(1*m).at_x(100*m)
    

    NOTE: Displacements and rotations are not commutative.

  • Rotations

    • rotate(G4RotationMatrix), rot(G4RotationMatrix) (identical meanings)
    • rotate_x(angle), rotate_y(angle), rotate_z(angle)
    • rot_x(angle), rot_y(angle), rot_z(angle) (shorter-named equivalents of the above)

    The above methods specify the rotation of the daughter volume with respect to its mother volume.

    By default there is no rotation between the two.

    The effect of these methods is cumulative. Thus, the following two lines are equivalent

    .rot_x(60*deg).rot_x(30*deg)
    .rot_x(90*deg)
    

    NOTE: Rotations around distinct axes are not commutative. Thus the following two lines are NOT equivalent

    .rot_x(90*deg).rot_y(90*deg)
    .rot_y(90*deg).rot_x(90*deg)
    

    NOTE: Displacements and rotations are not commutative.

  • Transformations

    • transform(G4Transform3D*), trans(G4Transform3D*) (identical meanings)

    The above methods specify the overall transformation of the daughter volume with respect to its mother volume.

  • name(string) By default the physical volume inherits the name of the logical volume being placed. This method allows overriding the inherited name.

  • copy_no(N) Useful to distinguish between multiple placements of a single logical volume. Implicitly appends "-N" to the physical volume's name, in addition to setting an copy-number attribute.

  • Overlap checking

    Besides daughter volumes being placed entirely within their mothers, there should be no overlap between volumes. If this happens, the behaviour of tracking becomes inconsistent.

    The following methods control overlap checking.

    When overlap checking is enabled, Geant4 will throw a runtime error if overlaps are detected, as opposed to allowing your program to run with an ill-defined geometry. However, this process may consume significant resources and slow down the construction of the geometry. Therefore, you may not want to have overlap checking enabled permanently.

    • check_overlaps() Activates checking for this placement only.

    • n4::place::check_overlaps_switch_on() Activate checking for all placements until further notice.

    • n4::place::check_overlaps_switch_off() Deactivate checking until further notice, for placements which do not have it explicitly enabled with .check_overlaps().

  • Controlling state accumulation

    • clone() Create an independent copy of the current placement state.

    Observe the difference in the effect of these two loops, whose source differs only in the presence/absence of .clone()

    place_box = n4::box("box").cube(1*m).place(copper).in(world);
    
    // Place boxes at 1, 2 and 3 metres
    for (auto k : {1,2,3}) { place_box.clone().at_x(k*m).now(); }
    // Place boxes at 1, 3 and 6 metres
    for (auto k : {1,2,3}) { place_box        .at_x(k*m).now(); }
    

    See Placement: Laziness and Accumulation.

Constructing boolean solids

Managing nain4 versions

The version of nain4 used by your project is determined by two files in its top-level directory:

  1. flake.nix
  2. flake.lock

By default, flake.nix contains the line

nain4.url = "github:jacg/nain4";

which states that nain4 should be taken from the default branch (master) of the nain4 repository owned by user jacg on GitHub.

However

  • the official master branch receives frequent updates, and
  • you don't want the version of nain4 that your project uses to change without your knowledge and consent.

This is the purpose of flake.lock: it pins the precise version (the commit id) of all of your project's flake.nix-specified dependencies.

Upgrading to the lastest version of nain4 available of master

If you want to upgrade to the most recent version of nain4 available on its master branch, see here.

Using other versions of nain4

By default, flake.nix instructs your system to use the latest master and flake.lock pins it to a specific version of master. But you can specify other versions of any dependency, including ones which are not located in the official repository. Here a few examples of how you might modify the corresponding line in your project's flake.nix:

  • Use a preview of a feature being developed on the some-experimental-feature branch in the nain4 repo.

    nain4.url = "github:jacg/nain4?branch=some-experimental-feature"

  • Use a specific tagged version of nain4.

    nain4.url = "github:jacg/nain4?ref=v1.2.3"

  • Use a specific commit available in the nain4 repo.

    nain4.url = "github:jacg/nain4?ref=9b4699ca25539d41bd1f5965a341be5e8ff862f1"

  • Use a version of nain4 made available in a repo other than the official nain4 one.

    nain4.url = "github:gonzaponte/nain4?branch=cool-idea"

  • Use a version of nain4 being developed on your own machine.

    nain4.url = "/home/me/src/nain4?branch=cool-idea"

For the full details, see the Nix manual.

Don't forget that, after switching to a different version of nain4, you may need to just clean and recompile your code, to avoid mysterious and annoying errors caused by stale build artefacts. (Hopefully this will be done for you automatically once nain4 migrates its build infrastructure from cmake to meson.)