obdeect-sources 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- obdeect_sources-0.8.0/CMakeLists.txt +101 -0
- obdeect_sources-0.8.0/PKG-INFO +54 -0
- obdeect_sources-0.8.0/README.md +26 -0
- obdeect_sources-0.8.0/cpp/bindings/core.cpp +228 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/background.hpp +102 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/cli.hpp +115 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/density.hpp +330 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/emission_stream.hpp +192 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/beam-scan.hpp +71 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/flasher.hpp +235 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/laser-beam.hpp +151 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/led.hpp +384 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/muon-track.hpp +482 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/night-sky.hpp +81 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/point-source.hpp +99 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/programs/stream_output.hpp +323 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/sky_sources.hpp +189 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/source.hpp +358 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/streams.hpp +27 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/track.hpp +136 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/vec3.hpp +69 -0
- obdeect_sources-0.8.0/cpp/include/obdeect/sources/version.hpp +13 -0
- obdeect_sources-0.8.0/cpp/tests/program_stream.hpp +85 -0
- obdeect_sources-0.8.0/cpp/tests/sky_stream_check.hpp +50 -0
- obdeect_sources-0.8.0/cpp/tests/test_beam-scan.cpp +120 -0
- obdeect_sources-0.8.0/cpp/tests/test_density.cpp +175 -0
- obdeect_sources-0.8.0/cpp/tests/test_flasher.cpp +425 -0
- obdeect_sources-0.8.0/cpp/tests/test_laser-beam.cpp +212 -0
- obdeect_sources-0.8.0/cpp/tests/test_led.cpp +218 -0
- obdeect_sources-0.8.0/cpp/tests/test_muon-track.cpp +448 -0
- obdeect_sources-0.8.0/cpp/tests/test_night-sky.cpp +237 -0
- obdeect_sources-0.8.0/cpp/tests/test_point-source.cpp +210 -0
- obdeect_sources-0.8.0/cpp/tests/test_sky_args.cpp +154 -0
- obdeect_sources-0.8.0/cpp/tests/test_source.cpp +401 -0
- obdeect_sources-0.8.0/cpp/tests/test_stream_contract.cpp +50 -0
- obdeect_sources-0.8.0/cpp/tests/test_version.cpp +14 -0
- obdeect_sources-0.8.0/cpp/tools/beam-scan.cpp +52 -0
- obdeect_sources-0.8.0/cpp/tools/flasher.cpp +130 -0
- obdeect_sources-0.8.0/cpp/tools/laser-beam.cpp +95 -0
- obdeect_sources-0.8.0/cpp/tools/led.cpp +139 -0
- obdeect_sources-0.8.0/cpp/tools/muon-track.cpp +35 -0
- obdeect_sources-0.8.0/cpp/tools/night-sky.cpp +75 -0
- obdeect_sources-0.8.0/cpp/tools/point-source.cpp +98 -0
- obdeect_sources-0.8.0/cpp/tools/sky_args.hpp +181 -0
- obdeect_sources-0.8.0/cpp/tools/telpos_file.hpp +61 -0
- obdeect_sources-0.8.0/cpp/tools/tool_args.hpp +85 -0
- obdeect_sources-0.8.0/pyproject.toml +42 -0
- obdeect_sources-0.8.0/python/obdeect/sources/__init__.py +5 -0
- obdeect_sources-0.8.0/python/obdeect/sources/cli.py +21 -0
- obdeect_sources-0.8.0/python/obdeect/sources/plot.py +57 -0
- obdeect_sources-0.8.0/python/obdeect/sources/source.py +263 -0
- obdeect_sources-0.8.0/python/tests/test_core_arity.py +27 -0
- obdeect_sources-0.8.0/python/tests/test_import.py +9 -0
- obdeect_sources-0.8.0/python/tests/test_plot.py +30 -0
- obdeect_sources-0.8.0/python/tests/test_source.py +325 -0
- obdeect_sources-0.8.0/python/tests/test_stream_contract.py +34 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(obdeect_sources LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
option(OBDEECT_SOURCES_BUILD_TESTING "Build the C++ tests" ON)
|
|
5
|
+
if(DEFINED SKBUILD_PLATLIB_DIR)
|
|
6
|
+
set(OBDEECT_SOURCES_BUILD_PYTHON_API_DEFAULT ON)
|
|
7
|
+
else()
|
|
8
|
+
set(OBDEECT_SOURCES_BUILD_PYTHON_API_DEFAULT OFF)
|
|
9
|
+
endif()
|
|
10
|
+
option(OBDEECT_SOURCES_BUILD_PYTHON_API "Build the Python extension" ${OBDEECT_SOURCES_BUILD_PYTHON_API_DEFAULT})
|
|
11
|
+
|
|
12
|
+
# The stream component: the target of the combined build, else the stream sources next to
|
|
13
|
+
# this component (builds from the repository), else an installed package (sdist builds).
|
|
14
|
+
if(NOT TARGET obdeect::stream)
|
|
15
|
+
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/../stream/CMakeLists.txt)
|
|
16
|
+
set(OBDEECT_STREAM_BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
|
17
|
+
set(OBDEECT_STREAM_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
|
|
18
|
+
set(OBDEECT_STREAM_BUILD_CAPI OFF CACHE BOOL "" FORCE)
|
|
19
|
+
set(OBDEECT_STREAM_BUILD_PYTHON_API OFF CACHE BOOL "" FORCE)
|
|
20
|
+
set(OBDEECT_STREAM_INSTALL OFF CACHE BOOL "" FORCE)
|
|
21
|
+
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../stream ${CMAKE_CURRENT_BINARY_DIR}/stream)
|
|
22
|
+
message(STATUS "obdeect sources: using the stream sources in ../stream")
|
|
23
|
+
else()
|
|
24
|
+
find_package(Python 3.14 COMPONENTS Interpreter)
|
|
25
|
+
if(Python_Interpreter_FOUND AND NOT obdeect_stream_DIR)
|
|
26
|
+
execute_process(COMMAND "${Python_EXECUTABLE}" -m obdeect.stream --cmake-dir
|
|
27
|
+
OUTPUT_VARIABLE obdeect_stream_DIR OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
|
28
|
+
endif()
|
|
29
|
+
find_package(obdeect_stream CONFIG REQUIRED)
|
|
30
|
+
message(STATUS "obdeect sources: using the installed stream package ${obdeect_stream_VERSION} (${obdeect_stream_DIR})")
|
|
31
|
+
endif()
|
|
32
|
+
endif()
|
|
33
|
+
|
|
34
|
+
# Header-only, standard-library-only core. Photon chunks use the obdeect stream headers.
|
|
35
|
+
add_library(obdeect_sources_core INTERFACE)
|
|
36
|
+
target_include_directories(obdeect_sources_core INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/cpp/include)
|
|
37
|
+
target_compile_features(obdeect_sources_core INTERFACE cxx_std_20)
|
|
38
|
+
target_link_libraries(obdeect_sources_core INTERFACE obdeect::stream)
|
|
39
|
+
# Identical floating-point results for any thread count and chunk size on one
|
|
40
|
+
# platform: no implicit fused multiply-add contraction.
|
|
41
|
+
target_compile_options(obdeect_sources_core INTERFACE -ffp-contract=off)
|
|
42
|
+
|
|
43
|
+
function(obdeect_sources_enable_warnings target)
|
|
44
|
+
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
|
45
|
+
# GCC 13 reports false maybe-uninitialized warnings inside std::variant at -O3.
|
|
46
|
+
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
|
47
|
+
target_compile_options(${target} PRIVATE -Wno-error=maybe-uninitialized)
|
|
48
|
+
endif()
|
|
49
|
+
endfunction()
|
|
50
|
+
|
|
51
|
+
if(OBDEECT_SOURCES_BUILD_PYTHON_API)
|
|
52
|
+
find_package(Python 3.14 COMPONENTS Interpreter Development.Module REQUIRED)
|
|
53
|
+
execute_process(COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
|
|
54
|
+
OUTPUT_VARIABLE nanobind_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
55
|
+
COMMAND_ERROR_IS_FATAL ANY)
|
|
56
|
+
find_package(nanobind CONFIG REQUIRED)
|
|
57
|
+
nanobind_add_module(_core NB_STATIC cpp/bindings/core.cpp)
|
|
58
|
+
target_link_libraries(_core PRIVATE obdeect_sources_core)
|
|
59
|
+
obdeect_sources_enable_warnings(_core)
|
|
60
|
+
install(TARGETS _core LIBRARY DESTINATION obdeect/sources)
|
|
61
|
+
endif()
|
|
62
|
+
|
|
63
|
+
if(OBDEECT_SOURCES_BUILD_TESTING)
|
|
64
|
+
enable_testing()
|
|
65
|
+
foreach(test_name version density source stream_contract)
|
|
66
|
+
add_executable(obdeect_sources_test_${test_name} cpp/tests/test_${test_name}.cpp)
|
|
67
|
+
target_link_libraries(obdeect_sources_test_${test_name} PRIVATE obdeect_sources_core)
|
|
68
|
+
# check.hpp lives with the stream tests; tests are built from the repository only, never from an sdist.
|
|
69
|
+
target_include_directories(obdeect_sources_test_${test_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../stream/cpp/tests)
|
|
70
|
+
obdeect_sources_enable_warnings(obdeect_sources_test_${test_name})
|
|
71
|
+
add_test(NAME ${test_name} COMMAND obdeect_sources_test_${test_name})
|
|
72
|
+
endforeach()
|
|
73
|
+
endif()
|
|
74
|
+
|
|
75
|
+
# Light-source programs: tool and test sources share the program name.
|
|
76
|
+
option(OBDEECT_SOURCES_BUILD_TOOLS "Build the light-source command-line tools" ON)
|
|
77
|
+
set(OBDEECT_SOURCES_LIGHT_SOURCE_PROGRAMS
|
|
78
|
+
flasher led point-source beam-scan laser-beam night-sky muon-track)
|
|
79
|
+
if(OBDEECT_SOURCES_BUILD_TESTING)
|
|
80
|
+
foreach(test_name ${OBDEECT_SOURCES_LIGHT_SOURCE_PROGRAMS} sky_args)
|
|
81
|
+
add_executable(obdeect_sources_test_${test_name} cpp/tests/test_${test_name}.cpp)
|
|
82
|
+
target_link_libraries(obdeect_sources_test_${test_name} PRIVATE obdeect_sources_core)
|
|
83
|
+
# check.hpp lives with the stream tests; tests are built from the repository only, never from an sdist.
|
|
84
|
+
target_include_directories(obdeect_sources_test_${test_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../stream/cpp/tests)
|
|
85
|
+
obdeect_sources_enable_warnings(obdeect_sources_test_${test_name})
|
|
86
|
+
add_test(NAME ${test_name} COMMAND obdeect_sources_test_${test_name})
|
|
87
|
+
endforeach()
|
|
88
|
+
endif()
|
|
89
|
+
if(OBDEECT_SOURCES_BUILD_TOOLS)
|
|
90
|
+
foreach(tool ${OBDEECT_SOURCES_LIGHT_SOURCE_PROGRAMS})
|
|
91
|
+
add_executable(obdeect-sources-${tool} cpp/tools/${tool}.cpp)
|
|
92
|
+
target_link_libraries(obdeect-sources-${tool} PRIVATE obdeect_sources_core)
|
|
93
|
+
obdeect_sources_enable_warnings(obdeect-sources-${tool})
|
|
94
|
+
endforeach()
|
|
95
|
+
# Wheels: inside the package, run through `obdeect sources <program>`.
|
|
96
|
+
if(DEFINED SKBUILD_PLATLIB_DIR)
|
|
97
|
+
foreach(tool ${OBDEECT_SOURCES_LIGHT_SOURCE_PROGRAMS})
|
|
98
|
+
install(TARGETS obdeect-sources-${tool} RUNTIME DESTINATION obdeect/sources/bin)
|
|
99
|
+
endforeach()
|
|
100
|
+
endif()
|
|
101
|
+
endif()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obdeect-sources
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Artificial light sources for Cherenkov telescopes
|
|
5
|
+
Author: obdeect contributors
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: C++
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
12
|
+
Requires-Python: >=3.14
|
|
13
|
+
Requires-Dist: numpy>=2.3
|
|
14
|
+
Requires-Dist: obdeect-stream==0.8.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: nanobind>=3.1; extra == "dev"
|
|
17
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff; extra == "dev"
|
|
20
|
+
Provides-Extra: docs
|
|
21
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
22
|
+
Requires-Dist: pydata-sphinx-theme; extra == "docs"
|
|
23
|
+
Requires-Dist: pyyaml; extra == "docs"
|
|
24
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
25
|
+
Provides-Extra: plot
|
|
26
|
+
Requires-Dist: matplotlib>=3.8; extra == "plot"
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# obdeect-sources
|
|
30
|
+
|
|
31
|
+
Artificial light sources for Cherenkov-telescope simulations: flashers, illuminators, lasers,
|
|
32
|
+
LEDs and stars. Sources emit photons at sky, telescope-sphere, telescope or camera level. The
|
|
33
|
+
C++20 core uses only the standard library. It has a Python API and writes photon streams.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install obdeect-sources # this component only (also installs obdeect-stream)
|
|
37
|
+
pip install obdeect # all obdeect components
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.14 or higher.
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
obdeect sources flasher --type dish --photons 1e6 # also --type secondary, --frame camera
|
|
46
|
+
obdeect sources led --camera-file cam.dat --photons 500
|
|
47
|
+
obdeect sources point-source
|
|
48
|
+
obdeect sources beam-scan
|
|
49
|
+
obdeect sources laser-beam
|
|
50
|
+
obdeect sources night-sky
|
|
51
|
+
obdeect sources muon-track
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Full documentation and the repository README: https://github.com/gammasim/obdeect
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# obdeect-sources
|
|
2
|
+
|
|
3
|
+
Artificial light sources for Cherenkov-telescope simulations: flashers, illuminators, lasers,
|
|
4
|
+
LEDs and stars. Sources emit photons at sky, telescope-sphere, telescope or camera level. The
|
|
5
|
+
C++20 core uses only the standard library. It has a Python API and writes photon streams.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install obdeect-sources # this component only (also installs obdeect-stream)
|
|
9
|
+
pip install obdeect # all obdeect components
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.14 or higher.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
obdeect sources flasher --type dish --photons 1e6 # also --type secondary, --frame camera
|
|
18
|
+
obdeect sources led --camera-file cam.dat --photons 500
|
|
19
|
+
obdeect sources point-source
|
|
20
|
+
obdeect sources beam-scan
|
|
21
|
+
obdeect sources laser-beam
|
|
22
|
+
obdeect sources night-sky
|
|
23
|
+
obdeect sources muon-track
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Full documentation and the repository README: https://github.com/gammasim/obdeect
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// nanobind extension obdeect.sources._core: the only C++ file that depends on Python.
|
|
2
|
+
|
|
3
|
+
#include <nanobind/nanobind.h>
|
|
4
|
+
#include <nanobind/ndarray.h>
|
|
5
|
+
#include <nanobind/stl/string.h>
|
|
6
|
+
#include <nanobind/stl/vector.h>
|
|
7
|
+
|
|
8
|
+
#include "obdeect/sources/density.hpp"
|
|
9
|
+
#include "obdeect/sources/source.hpp"
|
|
10
|
+
#include "obdeect/sources/streams.hpp"
|
|
11
|
+
#include "obdeect/sources/version.hpp"
|
|
12
|
+
|
|
13
|
+
#include "obdeect/stream/format.hpp"
|
|
14
|
+
#include "obdeect/stream/photons.hpp"
|
|
15
|
+
|
|
16
|
+
#include <cmath>
|
|
17
|
+
#include <cstdint>
|
|
18
|
+
#include <memory>
|
|
19
|
+
#include <stdexcept>
|
|
20
|
+
#include <string>
|
|
21
|
+
#include <vector>
|
|
22
|
+
|
|
23
|
+
namespace nb = nanobind;
|
|
24
|
+
using namespace obdeect::sources;
|
|
25
|
+
|
|
26
|
+
namespace {
|
|
27
|
+
|
|
28
|
+
Vec3 to_vec3(const std::vector<double> &v, const char *what) {
|
|
29
|
+
if (v.size() != 3)
|
|
30
|
+
throw std::invalid_argument(std::string(what) + " needs three components");
|
|
31
|
+
const Vec3 out{v[0], v[1], v[2]};
|
|
32
|
+
if (!is_finite(out))
|
|
33
|
+
throw std::invalid_argument(std::string(what) + " must be finite");
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void need(const std::vector<double> ¶ms, std::size_t n, const char *kind) {
|
|
38
|
+
if (params.size() != n)
|
|
39
|
+
throw std::invalid_argument(std::string(kind) + ": wrong number of parameters");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
LinearDensity table(const std::vector<double> &x, const std::vector<double> &y) {
|
|
43
|
+
return LinearDensity(x, y);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Spectrum make_spectrum(const std::string &kind, const std::vector<double> &p,
|
|
47
|
+
const std::vector<double> &x, const std::vector<double> &y) {
|
|
48
|
+
if (kind == "fixed") {
|
|
49
|
+
need(p, 1, "fixed spectrum");
|
|
50
|
+
return Fixed{p[0]};
|
|
51
|
+
}
|
|
52
|
+
if (kind == "uniform") {
|
|
53
|
+
need(p, 2, "uniform spectrum");
|
|
54
|
+
return UniformRange{p[0], p[1]};
|
|
55
|
+
}
|
|
56
|
+
if (kind == "inverse_square") {
|
|
57
|
+
need(p, 2, "inverse_square spectrum");
|
|
58
|
+
return InverseSquare{p[0], p[1]};
|
|
59
|
+
}
|
|
60
|
+
if (kind == "table") {
|
|
61
|
+
need(p, 0, "table spectrum");
|
|
62
|
+
return Tabulated{table(x, y)};
|
|
63
|
+
}
|
|
64
|
+
throw std::invalid_argument("unknown spectrum kind '" + kind + "'");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Pulse make_pulse(const std::string &kind, const std::vector<double> &p,
|
|
68
|
+
const std::vector<double> &x, const std::vector<double> &y) {
|
|
69
|
+
if (kind == "fixed") {
|
|
70
|
+
need(p, 1, "fixed pulse");
|
|
71
|
+
return Fixed{p[0]};
|
|
72
|
+
}
|
|
73
|
+
if (kind == "tophat") {
|
|
74
|
+
need(p, 2, "tophat pulse");
|
|
75
|
+
return UniformRange{p[1] - 0.5 * p[0], p[1] + 0.5 * p[0]};
|
|
76
|
+
}
|
|
77
|
+
if (kind == "gauss") {
|
|
78
|
+
need(p, 2, "gauss pulse");
|
|
79
|
+
return GaussianShape{p[1], p[0]};
|
|
80
|
+
}
|
|
81
|
+
if (kind == "exponential") {
|
|
82
|
+
need(p, 2, "exponential pulse");
|
|
83
|
+
return Exponential{p[1], p[0]};
|
|
84
|
+
}
|
|
85
|
+
if (kind == "complex") {
|
|
86
|
+
need(p, 4, "complex pulse");
|
|
87
|
+
return ComplexPulse{p[0], p[1], p[2], p[3]};
|
|
88
|
+
}
|
|
89
|
+
if (kind == "table") {
|
|
90
|
+
need(p, 0, "table pulse");
|
|
91
|
+
return Tabulated{table(x, y)};
|
|
92
|
+
}
|
|
93
|
+
throw std::invalid_argument("unknown pulse kind '" + kind + "'");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
double cone_u0(double angle_deg) {
|
|
97
|
+
if (!(std::isfinite(angle_deg) && angle_deg >= 0.0 && angle_deg <= 180.0))
|
|
98
|
+
throw std::invalid_argument("angle_deg must be finite and in [0, 180]");
|
|
99
|
+
return 1.0 - std::cos(angle_deg * pi / 180.0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Angular make_angular(const std::string &kind, const std::vector<double> &p,
|
|
103
|
+
const std::vector<double> &x, const std::vector<double> &y) {
|
|
104
|
+
if (kind == "isotropic" || kind == "lambertian" || kind == "rayleigh" || kind == "table")
|
|
105
|
+
need(p, 0, kind.c_str());
|
|
106
|
+
if (kind == "isotropic")
|
|
107
|
+
return Isotropic{};
|
|
108
|
+
if (kind == "lambertian")
|
|
109
|
+
return Lambertian{};
|
|
110
|
+
if (kind == "rayleigh")
|
|
111
|
+
return TabulatedAngular{rayleigh_angular_table()};
|
|
112
|
+
if (kind == "cone") {
|
|
113
|
+
need(p, 1, "cone");
|
|
114
|
+
return FixedAngle{cone_u0(p[0])};
|
|
115
|
+
}
|
|
116
|
+
if (kind == "filled_cone") {
|
|
117
|
+
need(p, 1, "filled_cone");
|
|
118
|
+
return FilledCone{cone_u0(p[0])};
|
|
119
|
+
}
|
|
120
|
+
if (kind == "gauss") {
|
|
121
|
+
need(p, 1, "gauss");
|
|
122
|
+
return TabulatedAngular{gaussian_angular_table(p[0] * pi / 180.0)};
|
|
123
|
+
}
|
|
124
|
+
if (kind == "table")
|
|
125
|
+
return TabulatedAngular{table(x, y)};
|
|
126
|
+
throw std::invalid_argument("unknown angular kind '" + kind + "'");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A float32 numpy array that owns a copy of the values.
|
|
130
|
+
nb::object to_array(const std::vector<float> &values) {
|
|
131
|
+
auto *data = new float[values.size()];
|
|
132
|
+
std::copy(values.begin(), values.end(), data);
|
|
133
|
+
nb::capsule owner(data, [](void *p) noexcept { delete[] static_cast<float *>(p); });
|
|
134
|
+
return nb::cast(nb::ndarray<nb::numpy, float>(data, {values.size()}, owner));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} // namespace
|
|
138
|
+
|
|
139
|
+
NB_MODULE(_core, m) {
|
|
140
|
+
m.doc() = "obdeect sources native core";
|
|
141
|
+
m.def("core_version", [] { return nb::make_tuple(version_major, version_minor, version_patch); });
|
|
142
|
+
m.def("stream_contract", [] {
|
|
143
|
+
nb::dict d;
|
|
144
|
+
d["format_major"] = obdeect::stream::format_major;
|
|
145
|
+
d["format_minor"] = obdeect::stream::format_minor;
|
|
146
|
+
d["max_chunk_rows"] = obdeect::stream::max_chunk_rows;
|
|
147
|
+
d["sources_format_major"] = photon_stream_format_major;
|
|
148
|
+
nb::dict frames;
|
|
149
|
+
frames["sky"] = static_cast<int>(obdeect::stream::Frame::sky);
|
|
150
|
+
frames["telescope"] = static_cast<int>(obdeect::stream::Frame::telescope);
|
|
151
|
+
frames["camera"] = static_cast<int>(obdeect::stream::Frame::camera);
|
|
152
|
+
d["frames"] = frames;
|
|
153
|
+
nb::list columns;
|
|
154
|
+
for (std::uint32_t c = 0; c < obdeect::stream::n_columns; ++c)
|
|
155
|
+
if ((source_columns >> c) & 1U)
|
|
156
|
+
columns.append(obdeect::stream::column_name(static_cast<obdeect::stream::Column>(c)));
|
|
157
|
+
d["emitted_columns"] = columns;
|
|
158
|
+
return d;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
nb::class_<Spectrum>(m, "Spectrum");
|
|
162
|
+
nb::class_<Pulse>(m, "Pulse");
|
|
163
|
+
nb::class_<Angular>(m, "Angular");
|
|
164
|
+
m.def("make_spectrum", &make_spectrum, nb::arg("kind"), nb::arg("params"), nb::arg("x"),
|
|
165
|
+
nb::arg("y"));
|
|
166
|
+
m.def("make_pulse", &make_pulse, nb::arg("kind"), nb::arg("params"), nb::arg("x"), nb::arg("y"));
|
|
167
|
+
m.def("make_angular", &make_angular, nb::arg("kind"), nb::arg("params"), nb::arg("x"),
|
|
168
|
+
nb::arg("y"));
|
|
169
|
+
|
|
170
|
+
nb::class_<Emitter>(m, "Emitter")
|
|
171
|
+
.def("__init__", [](Emitter *self, const std::vector<double> &position,
|
|
172
|
+
const std::vector<double> &pointing, const Angular &angular) {
|
|
173
|
+
new (self) Emitter{to_vec3(position, "position"), to_vec3(pointing, "pointing"), angular};
|
|
174
|
+
});
|
|
175
|
+
nb::class_<Star>(m, "Star").def("__init__", [](Star *self, const std::vector<double> &towards,
|
|
176
|
+
double distance_m, double disk_radius_m,
|
|
177
|
+
double disk_z_m, double start_z_m) {
|
|
178
|
+
new (self)
|
|
179
|
+
Star{to_vec3(towards, "towards_source"), distance_m, disk_radius_m, disk_z_m, start_z_m};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
nb::class_<Source>(m, "Source")
|
|
183
|
+
.def("__init__",
|
|
184
|
+
[](Source *self, int frame, nb::object geometry, const Spectrum &spectrum,
|
|
185
|
+
const Pulse &pulse, double photons) {
|
|
186
|
+
if (frame != 0 && frame != 2 && frame != 3)
|
|
187
|
+
throw std::invalid_argument("frame must be 0 (sky), 2 (telescope) or 3 (camera)");
|
|
188
|
+
new (self) Source();
|
|
189
|
+
self->frame = static_cast<obdeect::stream::Frame>(frame);
|
|
190
|
+
if (nb::isinstance<Emitter>(geometry))
|
|
191
|
+
self->geometry = nb::cast<Emitter>(geometry);
|
|
192
|
+
else if (nb::isinstance<Star>(geometry))
|
|
193
|
+
self->geometry = nb::cast<Star>(geometry);
|
|
194
|
+
else
|
|
195
|
+
throw std::invalid_argument("geometry must be an Emitter or a Star");
|
|
196
|
+
self->spectrum = spectrum;
|
|
197
|
+
self->pulse = pulse;
|
|
198
|
+
self->photons = photons;
|
|
199
|
+
validate(*self);
|
|
200
|
+
})
|
|
201
|
+
.def("set_disc_target",
|
|
202
|
+
[](Source &s, const std::vector<double> ¢re, const std::vector<double> &normal,
|
|
203
|
+
double radius) {
|
|
204
|
+
s.target = DiscTarget{to_vec3(centre, "centre"), to_vec3(normal, "normal"), radius};
|
|
205
|
+
})
|
|
206
|
+
.def("set_sphere_target",
|
|
207
|
+
[](Source &s, const std::vector<double> ¢re, double radius) {
|
|
208
|
+
s.target = SphereTarget{to_vec3(centre, "centre"), radius};
|
|
209
|
+
})
|
|
210
|
+
.def("validate", [](const Source &s) { validate(s); })
|
|
211
|
+
.def("expected_count", [](const Source &s) { return expected_count(s); })
|
|
212
|
+
.def("photon_count",
|
|
213
|
+
[](const Source &s, std::uint64_t seed, std::uint32_t event, std::uint32_t telescope) {
|
|
214
|
+
return photon_count(s, seed, event, telescope);
|
|
215
|
+
})
|
|
216
|
+
.def("emit", [](const Source &s, std::uint64_t seed, std::uint32_t event,
|
|
217
|
+
std::uint32_t telescope, std::uint64_t first_index, std::size_t rows) {
|
|
218
|
+
obdeect::stream::PhotonChunk chunk;
|
|
219
|
+
emit(s, seed, event, telescope, first_index, rows, chunk);
|
|
220
|
+
nb::dict out;
|
|
221
|
+
for (std::uint32_t c = 0; c < obdeect::stream::n_columns; ++c)
|
|
222
|
+
if ((source_columns >> c) & 1U) {
|
|
223
|
+
const auto column = static_cast<obdeect::stream::Column>(c);
|
|
224
|
+
out[obdeect::stream::column_name(column)] = to_array(chunk[column]);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Night-sky background: photons start on the sphere of one telescope and travel inwards, with
|
|
4
|
+
// directions uniform in solid angle inside a cone (docs/spec/background.md).
|
|
5
|
+
|
|
6
|
+
#include "obdeect/sources/density.hpp"
|
|
7
|
+
#include "obdeect/sources/source.hpp"
|
|
8
|
+
#include "obdeect/sources/vec3.hpp"
|
|
9
|
+
|
|
10
|
+
#include "obdeect/stream/photons.hpp"
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <cmath>
|
|
14
|
+
#include <cstddef>
|
|
15
|
+
#include <cstdint>
|
|
16
|
+
#include <stdexcept>
|
|
17
|
+
|
|
18
|
+
namespace obdeect::sources {
|
|
19
|
+
|
|
20
|
+
struct BackgroundSource {
|
|
21
|
+
Vec3 centre{};
|
|
22
|
+
double radius_m{};
|
|
23
|
+
Vec3 pointing{0.0, 0.0, -1.0};
|
|
24
|
+
double viewcone_rad{pi / 2.0};
|
|
25
|
+
Spectrum spectrum{Fixed{400.0}};
|
|
26
|
+
double pulse_width_ns{};
|
|
27
|
+
double pulse_delay_ns{};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Rotation about the y axis and about the z axis.
|
|
31
|
+
inline Vec3 rotate_y(const Vec3 &v, double a) {
|
|
32
|
+
return {v.x * std::cos(a) + v.z * std::sin(a), v.y, v.z * std::cos(a) - v.x * std::sin(a)};
|
|
33
|
+
}
|
|
34
|
+
inline Vec3 rotate_z(const Vec3 &v, double a) {
|
|
35
|
+
return {v.x * std::cos(a) - v.y * std::sin(a), v.x * std::sin(a) + v.y * std::cos(a), v.z};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
inline std::uint64_t background_count(double mean_bunches, std::uint64_t seed, std::uint32_t event,
|
|
39
|
+
std::uint32_t telescope) {
|
|
40
|
+
return round_count(mean_bunches, seed, event, telescope);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
inline Photon sample_background(const BackgroundSource &s, std::uint64_t seed, std::uint32_t event,
|
|
44
|
+
std::uint32_t telescope, std::uint64_t index) {
|
|
45
|
+
const Uniforms ud = philox_draw(seed, event, telescope, index, Stream::direction);
|
|
46
|
+
const Vec3 axis = normalised(s.pointing);
|
|
47
|
+
const Basis basis = orthonormal_basis(axis);
|
|
48
|
+
const double cos_theta = 1.0 - ud[0] * (1.0 - std::cos(s.viewcone_rad));
|
|
49
|
+
const double sin_theta = std::sqrt(std::max(0.0, (1.0 - cos_theta) * (1.0 + cos_theta)));
|
|
50
|
+
const Vec3 d = direction_about(axis, basis, cos_theta, sin_theta, two_pi * ud[1]);
|
|
51
|
+
|
|
52
|
+
const Uniforms ut = philox_draw(seed, event, telescope, index, Stream::target);
|
|
53
|
+
const double ri = 1.0 - std::sqrt(ut[0]);
|
|
54
|
+
const double phi = two_pi * ut[1];
|
|
55
|
+
const double zi = std::sqrt(std::max(0.0, 1.0 - ri * ri));
|
|
56
|
+
const Vec3 local{ri * std::cos(phi), ri * std::sin(phi), zi};
|
|
57
|
+
const double za = std::acos(std::clamp(-d.z, -1.0, 1.0));
|
|
58
|
+
const double az = std::atan2(-d.y, -d.x);
|
|
59
|
+
const Vec3 unit = rotate_z(rotate_y(local, za), az);
|
|
60
|
+
|
|
61
|
+
Photon p;
|
|
62
|
+
p.position = s.centre + unit * s.radius_m;
|
|
63
|
+
p.direction = d;
|
|
64
|
+
double jitter = 0.0;
|
|
65
|
+
if (s.pulse_width_ns > 0.0) {
|
|
66
|
+
const double u = philox_draw(seed, event, telescope, index, Stream::pulse)[0];
|
|
67
|
+
jitter = s.pulse_width_ns / fwhm_per_sigma * (u - 0.5);
|
|
68
|
+
}
|
|
69
|
+
p.time_ns = s.pulse_delay_ns - zi * s.radius_m / speed_of_light_m_per_ns + jitter;
|
|
70
|
+
p.wavelength_nm =
|
|
71
|
+
sample(s.spectrum, philox_draw(seed, event, telescope, index, Stream::wavelength));
|
|
72
|
+
p.weight = 1.0;
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
inline void emit_background(const BackgroundSource &s, std::uint64_t seed, std::uint32_t event,
|
|
77
|
+
std::uint32_t telescope, std::uint64_t first, std::size_t rows,
|
|
78
|
+
double weight, obdeect::stream::PhotonChunk &chunk) {
|
|
79
|
+
detail::require(s.radius_m > 0.0 && std::isfinite(s.radius_m), "background: radius must be > 0");
|
|
80
|
+
detail::require(s.viewcone_rad > 0.0 && s.viewcone_rad <= pi,
|
|
81
|
+
"background: viewcone must lie in (0, pi]");
|
|
82
|
+
detail::require(rows <= obdeect::stream::max_chunk_rows, "a chunk holds at most 65536 rows");
|
|
83
|
+
chunk.resize(rows, source_columns);
|
|
84
|
+
chunk.telescope_id = telescope;
|
|
85
|
+
chunk.frame = obdeect::stream::Frame::sky;
|
|
86
|
+
chunk.first_index = first;
|
|
87
|
+
using obdeect::stream::Column;
|
|
88
|
+
for (std::size_t i = 0; i < rows; ++i) {
|
|
89
|
+
const Photon p = sample_background(s, seed, event, telescope, first + i);
|
|
90
|
+
chunk[Column::x][i] = static_cast<float>(p.position.x);
|
|
91
|
+
chunk[Column::y][i] = static_cast<float>(p.position.y);
|
|
92
|
+
chunk[Column::z][i] = static_cast<float>(p.position.z);
|
|
93
|
+
chunk[Column::cx][i] = static_cast<float>(p.direction.x);
|
|
94
|
+
chunk[Column::cy][i] = static_cast<float>(p.direction.y);
|
|
95
|
+
chunk[Column::cz][i] = static_cast<float>(p.direction.z);
|
|
96
|
+
chunk[Column::t][i] = static_cast<float>(p.time_ns);
|
|
97
|
+
chunk[Column::w][i] = static_cast<float>(weight);
|
|
98
|
+
chunk[Column::wavelength][i] = static_cast<float>(p.wavelength_nm);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
} // namespace obdeect::sources
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Option parsing shared by the sky tools (docs/spec/cli-conventions.md). Errors are
|
|
4
|
+
// std::invalid_argument with the option name first.
|
|
5
|
+
|
|
6
|
+
#include "obdeect/sources/vec3.hpp"
|
|
7
|
+
|
|
8
|
+
#include <cctype>
|
|
9
|
+
#include <cerrno>
|
|
10
|
+
#include <cmath>
|
|
11
|
+
#include <cstdint>
|
|
12
|
+
#include <cstdlib>
|
|
13
|
+
#include <limits>
|
|
14
|
+
#include <stdexcept>
|
|
15
|
+
#include <string>
|
|
16
|
+
#include <vector>
|
|
17
|
+
|
|
18
|
+
namespace obdeect::sources::cli {
|
|
19
|
+
|
|
20
|
+
// True when strtod consumes the whole text (so "4e2" and "-5" are numbers, "4e2x" is not).
|
|
21
|
+
inline bool is_number(const std::string &text) {
|
|
22
|
+
if (text.empty())
|
|
23
|
+
return false;
|
|
24
|
+
char *end = nullptr;
|
|
25
|
+
(void)std::strtod(text.c_str(), &end);
|
|
26
|
+
return end != text.c_str() && *end == '\0';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A finite real number; the whole text must be a number.
|
|
30
|
+
inline double parse_number(const std::string &text, const std::string &option) {
|
|
31
|
+
if (!is_number(text))
|
|
32
|
+
throw std::invalid_argument(option + ": not a number: " + text);
|
|
33
|
+
errno = 0;
|
|
34
|
+
const double value = std::strtod(text.c_str(), nullptr);
|
|
35
|
+
if (errno == ERANGE || !std::isfinite(value))
|
|
36
|
+
throw std::invalid_argument(option + ": number out of range: " + text);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A signed decimal integer (digits only).
|
|
41
|
+
inline long long parse_integer(const std::string &text, const std::string &option) {
|
|
42
|
+
std::size_t i = (!text.empty() && (text[0] == '+' || text[0] == '-')) ? 1 : 0;
|
|
43
|
+
bool digits = i < text.size();
|
|
44
|
+
for (std::size_t k = i; k < text.size(); ++k)
|
|
45
|
+
digits = digits && std::isdigit(static_cast<unsigned char>(text[k]));
|
|
46
|
+
if (!digits)
|
|
47
|
+
throw std::invalid_argument(option + ": not an integer: " + text);
|
|
48
|
+
errno = 0;
|
|
49
|
+
const long long value = std::strtoll(text.c_str(), nullptr, 10);
|
|
50
|
+
if (errno == ERANGE)
|
|
51
|
+
throw std::invalid_argument(option + ": integer out of range: " + text);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
inline double to_double(const std::string &text, const std::string &option) {
|
|
56
|
+
return parse_number(text, option);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// An event count: integer in 0..4294967295.
|
|
60
|
+
inline std::uint32_t to_count(const std::string &text, const std::string &option) {
|
|
61
|
+
bool digits = !text.empty();
|
|
62
|
+
for (const char c : text)
|
|
63
|
+
digits = digits && std::isdigit(static_cast<unsigned char>(c));
|
|
64
|
+
errno = 0;
|
|
65
|
+
const unsigned long long value = digits ? std::strtoull(text.c_str(), nullptr, 10) : 0;
|
|
66
|
+
if (!digits || errno == ERANGE || value > std::numeric_limits<std::uint32_t>::max())
|
|
67
|
+
throw std::invalid_argument(option + ": must be an integer in 0..4294967295: " + text);
|
|
68
|
+
return static_cast<std::uint32_t>(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Comma separated items (an empty item is an error).
|
|
72
|
+
inline std::vector<std::string> split_items(const std::string &text, const std::string &option) {
|
|
73
|
+
std::vector<std::string> items;
|
|
74
|
+
std::size_t start = 0;
|
|
75
|
+
while (true) {
|
|
76
|
+
const std::size_t comma = text.find(',', start);
|
|
77
|
+
const std::string item = text.substr(start, comma == std::string::npos ? comma : comma - start);
|
|
78
|
+
if (item.empty())
|
|
79
|
+
throw std::invalid_argument(option + ": empty item in list: " + text);
|
|
80
|
+
items.push_back(item);
|
|
81
|
+
if (comma == std::string::npos)
|
|
82
|
+
break;
|
|
83
|
+
start = comma + 1;
|
|
84
|
+
}
|
|
85
|
+
return items;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
inline std::vector<double> parse_list(const std::string &text, const std::string &option) {
|
|
89
|
+
std::vector<double> values;
|
|
90
|
+
for (const auto &item : split_items(text, option))
|
|
91
|
+
values.push_back(parse_number(item, option));
|
|
92
|
+
return values;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// "x,y,z" with exactly three items.
|
|
96
|
+
inline Vec3 parse_vector(const std::string &text, const std::string &option) {
|
|
97
|
+
const auto items = split_items(text, option);
|
|
98
|
+
if (items.size() != 3)
|
|
99
|
+
throw std::invalid_argument(option + ": needs three comma-separated values");
|
|
100
|
+
return {parse_number(items[0], option), parse_number(items[1], option),
|
|
101
|
+
parse_number(items[2], option)};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
inline std::string lower_case(std::string text) {
|
|
105
|
+
for (char &c : text)
|
|
106
|
+
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
107
|
+
return text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// True when `text` starts with `prefix`, ignoring case.
|
|
111
|
+
inline bool starts_with_nocase(const std::string &text, const std::string &prefix) {
|
|
112
|
+
return text.size() >= prefix.size() && lower_case(text.substr(0, prefix.size())) == prefix;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
} // namespace obdeect::sources::cli
|