gpimg 0.1.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.
gpimg-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francisco Förster
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include LICENSE README.md
2
+ graft c
3
+ graft tests
4
+ prune c/build
5
+ global-exclude *.o *.so *.a *.pyc
6
+ include src/gpimg/*.c
gpimg-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: gpimg
3
+ Version: 0.1.0
4
+ Summary: Exact O(n) two-dimensional Gaussian-process fits and images of multi-band light curves
5
+ Author-email: Francisco Förster <francisco.forster@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/fforster/gpimg
8
+ Project-URL: Documentation, https://gpimg.readthedocs.io
9
+ Project-URL: Source, https://github.com/fforster/gpimg
10
+ Keywords: gaussian process,light curves,state space,kalman filter,astronomy,LSST,transients
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: C
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy>=1.23
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest>=7; extra == "test"
25
+ Requires-Dist: scipy>=1.9; extra == "test"
26
+ Provides-Extra: docs
27
+ Requires-Dist: sphinx>=7; extra == "docs"
28
+ Requires-Dist: furo>=2024.1; extra == "docs"
29
+ Requires-Dist: numpydoc>=1.6; extra == "docs"
30
+ Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
31
+ Requires-Dist: matplotlib>=3.7; extra == "docs"
32
+ Requires-Dist: myst-parser>=2; extra == "docs"
33
+ Provides-Extra: plot
34
+ Requires-Dist: matplotlib>=3.7; extra == "plot"
35
+ Dynamic: license-file
36
+
37
+ # gpimg
38
+
39
+ **Exact O(n) two-dimensional Gaussian processes for multi-band light curves.**
40
+
41
+ gpimg fits a Gaussian process jointly in time and wavelength to a multi-band light curve and returns the posterior
42
+ flux on any time × wavelength grid. The kernel is a Matérn-3/2 in time times a squared exponential in ln λ:
43
+
44
+ k((t,λ),(t',λ')) = A² (1 + √3|t−t'|/ℓt) exp(−√3|t−t'|/ℓt) · exp(−(ln λ − ln λ')² / 2ℓλ²)
45
+
46
+ Because the time kernel is a linear stochastic differential equation and the kernel is separable, a light curve
47
+ observed in up to six bands is *exactly* a small linear Gaussian state-space model. gpimg evaluates the likelihood,
48
+ its analytic gradient, maximum-likelihood fits and posterior images with Kalman filters and smoothers in **O(n)**,
49
+ with no approximation.
50
+
51
+ - Exact: likelihoods agree with a dense Cholesky GP to ~1e-11 (relative).
52
+ - Fast: one likelihood + gradient of an 18 668-point light curve in 4.7 ms on one core; a fit of a typical
53
+ deep-drilling LSST light curve (~660 points) in ~19 ms with five starts — 279× faster than PISCOLA's GP fit, and
54
+ 1 100× faster for wide-fast-deep light curves (~64 points).
55
+ - Compiled C core, no JIT and no warm-up; AVX2 + FMA code selected at run time; OpenMP batches.
56
+ - Posteriors at any wavelength, smoothed or causal (real time), with an optional numerically stable
57
+ Rauch–Tung–Striebel smoother, and exact band–band covariances for colors.
58
+ - Optional fitted jitter for scatter the quoted errors do not describe; off by default, and off costs nothing.
59
+ - Python package and plain C library (Makefile, CMake, pkg-config).
60
+
61
+ Documentation: the Sphinx sources under `docs/` (guide, Python API, C API, theory). A hosted build is not up yet.
62
+
63
+ ## Installation
64
+
65
+ ```console
66
+ $ pip install gpimg
67
+ ```
68
+
69
+ gpimg ships as a source distribution, so this compiles the C core for your machine: you need a C compiler (gcc or
70
+ clang). OpenMP is optional — without it `gpimg.batch` runs serially and everything else is unaffected. An AVX2 +
71
+ FMA copy is built alongside the portable one and chosen at import when the CPU supports it.
72
+
73
+ From a source checkout, which also gets you the plain C library:
74
+
75
+ ```console
76
+ $ pip install . # the Python package
77
+ $ pytest # optional: pip install ".[test]" first
78
+ $ make -C c && make -C c test # the C library: build/libgpimg.so, build/libgpimg.a
79
+ $ sudo make -C c install # or: cmake -S c -B build && cmake --build build
80
+ ```
81
+
82
+ ## Python quick start
83
+
84
+ ```python
85
+ import numpy as np
86
+ import gpimg
87
+
88
+ # t [days], band ('u','g','r','i','z','y' or 0..5; LSST by default), flux and flux_err in any flux unit
89
+ gp = gpimg.GP(t, band, flux, flux_err)
90
+ res = gp.fit() # maximum likelihood, 5 starting points
91
+ print(res.amplitude, res.length_time, res.length_wave, res.message)
92
+ res = gp.fit(jitter=True) # + a white jitter for extra scatter; res.jitter
93
+
94
+ img = gp.predict(np.linspace(-30, 120, 300), # times [d]
95
+ np.geomspace(3200, 10200, 200)) # wavelengths [Å]
96
+ img.mean.shape, img.std.shape # (200, 300): wavelengths x times
97
+
98
+ curves = gp.predict_bands(np.linspace(-30, 120, 300)) # {'u': (mean, std), ...}
99
+ curves, covs = gp.predict_bands(t_grid, cov=True) # + {('g','r'): cov, ...} for color errors
100
+ live = gp.predict(t_grid, causal=True) # filtered: data up to each time only
101
+ stable = gp.predict(t_grid, wave_grid, smoother="rts") # numerically stable smoother
102
+ ```
103
+
104
+ Many light curves from one long table (pandas, astropy, dict of arrays), fitted and imaged in parallel:
105
+
106
+ ```python
107
+ lcs = gpimg.LightCurves.from_table(table, id="oid", t="t", band="band", flux="flux", flux_err="flux_err")
108
+ res = gpimg.batch(lcs, np.linspace(-30, 120, 300), np.geomspace(3200, 10200, 200))
109
+ res.mean.shape # (n_objects, 200, 300), float32
110
+ ```
111
+
112
+ Other surveys: `gpimg.GP(t, band, flux, err, wavelengths={"g": 4800.0, "r": 6400.0, "i": 7900.0})`.
113
+
114
+ Runnable examples are in `examples/` (`quickstart.py`, `batch_table.py`, `causal_vs_smoothed.py`,
115
+ `other_survey.py`).
116
+
117
+ ## C quick start
118
+
119
+ ```c
120
+ #include "gpimg.h"
121
+
122
+ double lnwave[6]; /* ln of the band wavelengths */
123
+ gp_fit_opts opts;
124
+ gp_fit_default_opts(&opts);
125
+ opts.nstart = 5;
126
+ gp_fit_result res;
127
+ gp_fit(n, t, band, y, err, lnwave, theta0, &opts, &res); /* res.theta = (ln A, ln lt, ln ll) */
128
+ gp_image(n, t, band, y, err, 6, lnwave, res.theta, nt, tg, nl, lng, mean, var, GPIMG_SMOOTH(GPIMG_RTS));
129
+ /* gp_image_cov(...) additionally fills cov[npair][nt] for pairs (pa[p], pb[p]) of grid wavelengths */
130
+ ```
131
+
132
+ See `c/examples/example.c` and the C API reference in `docs/c_api.rst`.
133
+
134
+ ## Performance and accuracy
135
+
136
+ Single core, Intel Core Ultra 7 255U; simulated LSST light curves (details in `docs/guide/performance.rst` and
137
+ `docs/guide/accuracy.rst`).
138
+
139
+ | likelihood + gradient | gpimg | dense GP (tinygp, warm) | smolgp, same model (warm) |
140
+ |---|---|---|---|
141
+ | 1 000 points | 0.20 ms | 165 ms | 12 ms |
142
+ | 12 000 points | 2.8 ms | 242 s | 0.27 s |
143
+ | 18 668 points | 4.7 ms | — | 0.41 s |
144
+
145
+ | median per fit | DDF (~660 points) | WFD (~64 points) |
146
+ |---|---|---|
147
+ | gpimg, 5 starts | 18.7 ms | 1.7 ms |
148
+ | PISCOLA as shipped | 4.95 s | 2.17 s |
149
+
150
+ Against a dense double-precision GP with the same hyperparameters: likelihood ≤ 3.5e-11, gradient ≤ 8.4e-9 and
151
+ posterior mean ≤ 1.1e-10 (relative). With `smoother="rts"` the posterior variance agrees with a 200-bit reference
152
+ to 7e-10 at the grid points where the default smoother is worst (4.8e-3).
153
+
154
+ ## Citing
155
+
156
+ A paper describing gpimg is in preparation (ApJ Letters). Until then please cite the software and the works in the
157
+ references (`docs/references.rst`), in particular Hartikainen & Särkkä (2010) for
158
+ the state-space formulation of temporal Gaussian processes.
159
+
160
+ ## License
161
+
162
+ MIT — see [LICENSE](LICENSE).
gpimg-0.1.0/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # gpimg
2
+
3
+ **Exact O(n) two-dimensional Gaussian processes for multi-band light curves.**
4
+
5
+ gpimg fits a Gaussian process jointly in time and wavelength to a multi-band light curve and returns the posterior
6
+ flux on any time × wavelength grid. The kernel is a Matérn-3/2 in time times a squared exponential in ln λ:
7
+
8
+ k((t,λ),(t',λ')) = A² (1 + √3|t−t'|/ℓt) exp(−√3|t−t'|/ℓt) · exp(−(ln λ − ln λ')² / 2ℓλ²)
9
+
10
+ Because the time kernel is a linear stochastic differential equation and the kernel is separable, a light curve
11
+ observed in up to six bands is *exactly* a small linear Gaussian state-space model. gpimg evaluates the likelihood,
12
+ its analytic gradient, maximum-likelihood fits and posterior images with Kalman filters and smoothers in **O(n)**,
13
+ with no approximation.
14
+
15
+ - Exact: likelihoods agree with a dense Cholesky GP to ~1e-11 (relative).
16
+ - Fast: one likelihood + gradient of an 18 668-point light curve in 4.7 ms on one core; a fit of a typical
17
+ deep-drilling LSST light curve (~660 points) in ~19 ms with five starts — 279× faster than PISCOLA's GP fit, and
18
+ 1 100× faster for wide-fast-deep light curves (~64 points).
19
+ - Compiled C core, no JIT and no warm-up; AVX2 + FMA code selected at run time; OpenMP batches.
20
+ - Posteriors at any wavelength, smoothed or causal (real time), with an optional numerically stable
21
+ Rauch–Tung–Striebel smoother, and exact band–band covariances for colors.
22
+ - Optional fitted jitter for scatter the quoted errors do not describe; off by default, and off costs nothing.
23
+ - Python package and plain C library (Makefile, CMake, pkg-config).
24
+
25
+ Documentation: the Sphinx sources under `docs/` (guide, Python API, C API, theory). A hosted build is not up yet.
26
+
27
+ ## Installation
28
+
29
+ ```console
30
+ $ pip install gpimg
31
+ ```
32
+
33
+ gpimg ships as a source distribution, so this compiles the C core for your machine: you need a C compiler (gcc or
34
+ clang). OpenMP is optional — without it `gpimg.batch` runs serially and everything else is unaffected. An AVX2 +
35
+ FMA copy is built alongside the portable one and chosen at import when the CPU supports it.
36
+
37
+ From a source checkout, which also gets you the plain C library:
38
+
39
+ ```console
40
+ $ pip install . # the Python package
41
+ $ pytest # optional: pip install ".[test]" first
42
+ $ make -C c && make -C c test # the C library: build/libgpimg.so, build/libgpimg.a
43
+ $ sudo make -C c install # or: cmake -S c -B build && cmake --build build
44
+ ```
45
+
46
+ ## Python quick start
47
+
48
+ ```python
49
+ import numpy as np
50
+ import gpimg
51
+
52
+ # t [days], band ('u','g','r','i','z','y' or 0..5; LSST by default), flux and flux_err in any flux unit
53
+ gp = gpimg.GP(t, band, flux, flux_err)
54
+ res = gp.fit() # maximum likelihood, 5 starting points
55
+ print(res.amplitude, res.length_time, res.length_wave, res.message)
56
+ res = gp.fit(jitter=True) # + a white jitter for extra scatter; res.jitter
57
+
58
+ img = gp.predict(np.linspace(-30, 120, 300), # times [d]
59
+ np.geomspace(3200, 10200, 200)) # wavelengths [Å]
60
+ img.mean.shape, img.std.shape # (200, 300): wavelengths x times
61
+
62
+ curves = gp.predict_bands(np.linspace(-30, 120, 300)) # {'u': (mean, std), ...}
63
+ curves, covs = gp.predict_bands(t_grid, cov=True) # + {('g','r'): cov, ...} for color errors
64
+ live = gp.predict(t_grid, causal=True) # filtered: data up to each time only
65
+ stable = gp.predict(t_grid, wave_grid, smoother="rts") # numerically stable smoother
66
+ ```
67
+
68
+ Many light curves from one long table (pandas, astropy, dict of arrays), fitted and imaged in parallel:
69
+
70
+ ```python
71
+ lcs = gpimg.LightCurves.from_table(table, id="oid", t="t", band="band", flux="flux", flux_err="flux_err")
72
+ res = gpimg.batch(lcs, np.linspace(-30, 120, 300), np.geomspace(3200, 10200, 200))
73
+ res.mean.shape # (n_objects, 200, 300), float32
74
+ ```
75
+
76
+ Other surveys: `gpimg.GP(t, band, flux, err, wavelengths={"g": 4800.0, "r": 6400.0, "i": 7900.0})`.
77
+
78
+ Runnable examples are in `examples/` (`quickstart.py`, `batch_table.py`, `causal_vs_smoothed.py`,
79
+ `other_survey.py`).
80
+
81
+ ## C quick start
82
+
83
+ ```c
84
+ #include "gpimg.h"
85
+
86
+ double lnwave[6]; /* ln of the band wavelengths */
87
+ gp_fit_opts opts;
88
+ gp_fit_default_opts(&opts);
89
+ opts.nstart = 5;
90
+ gp_fit_result res;
91
+ gp_fit(n, t, band, y, err, lnwave, theta0, &opts, &res); /* res.theta = (ln A, ln lt, ln ll) */
92
+ gp_image(n, t, band, y, err, 6, lnwave, res.theta, nt, tg, nl, lng, mean, var, GPIMG_SMOOTH(GPIMG_RTS));
93
+ /* gp_image_cov(...) additionally fills cov[npair][nt] for pairs (pa[p], pb[p]) of grid wavelengths */
94
+ ```
95
+
96
+ See `c/examples/example.c` and the C API reference in `docs/c_api.rst`.
97
+
98
+ ## Performance and accuracy
99
+
100
+ Single core, Intel Core Ultra 7 255U; simulated LSST light curves (details in `docs/guide/performance.rst` and
101
+ `docs/guide/accuracy.rst`).
102
+
103
+ | likelihood + gradient | gpimg | dense GP (tinygp, warm) | smolgp, same model (warm) |
104
+ |---|---|---|---|
105
+ | 1 000 points | 0.20 ms | 165 ms | 12 ms |
106
+ | 12 000 points | 2.8 ms | 242 s | 0.27 s |
107
+ | 18 668 points | 4.7 ms | — | 0.41 s |
108
+
109
+ | median per fit | DDF (~660 points) | WFD (~64 points) |
110
+ |---|---|---|
111
+ | gpimg, 5 starts | 18.7 ms | 1.7 ms |
112
+ | PISCOLA as shipped | 4.95 s | 2.17 s |
113
+
114
+ Against a dense double-precision GP with the same hyperparameters: likelihood ≤ 3.5e-11, gradient ≤ 8.4e-9 and
115
+ posterior mean ≤ 1.1e-10 (relative). With `smoother="rts"` the posterior variance agrees with a 200-bit reference
116
+ to 7e-10 at the grid points where the default smoother is worst (4.8e-3).
117
+
118
+ ## Citing
119
+
120
+ A paper describing gpimg is in preparation (ApJ Letters). Until then please cite the software and the works in the
121
+ references (`docs/references.rst`), in particular Hartikainen & Särkkä (2010) for
122
+ the state-space formulation of temporal Gaussian processes.
123
+
124
+ ## License
125
+
126
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,55 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(gpimg VERSION 0.1.0 LANGUAGES C)
3
+
4
+ # gpimg C library. Usage:
5
+ # cmake -S c -B build -DCMAKE_BUILD_TYPE=Release [-DGPIMG_NATIVE=ON] && cmake --build build && ctest --test-dir build
6
+ # In another CMake project: add_subdirectory(gpimg/c) or find_package(gpimg) after installation, then
7
+ # target_link_libraries(myapp PRIVATE gpimg::gpimg)
8
+
9
+ option(GPIMG_OPENMP "Parallel gp_batch with OpenMP" ON)
10
+ option(GPIMG_NATIVE "Optimise for the build machine (-march=native)" OFF)
11
+ option(BUILD_SHARED_LIBS "Build a shared library" ON)
12
+
13
+ set(CMAKE_C_STANDARD 11)
14
+ set(CMAKE_C_EXTENSIONS ON)
15
+ if(NOT CMAKE_BUILD_TYPE)
16
+ set(CMAKE_BUILD_TYPE Release)
17
+ endif()
18
+
19
+ add_library(gpimg src/gpimg.c)
20
+ add_library(gpimg::gpimg ALIAS gpimg)
21
+ target_include_directories(gpimg PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
22
+ $<INSTALL_INTERFACE:include>)
23
+ set_target_properties(gpimg PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 0 POSITION_INDEPENDENT_CODE ON)
24
+ if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
25
+ # IEEE semantics are kept (no -ffast-math); FMA contraction is allowed
26
+ target_compile_options(gpimg PRIVATE -O3 -fno-math-errno -ffp-contract=fast -Wall -Wextra)
27
+ if(GPIMG_NATIVE)
28
+ target_compile_options(gpimg PRIVATE -march=native)
29
+ endif()
30
+ endif()
31
+ find_library(MATH_LIBRARY m)
32
+ if(MATH_LIBRARY)
33
+ target_link_libraries(gpimg PUBLIC ${MATH_LIBRARY})
34
+ endif()
35
+ if(GPIMG_OPENMP)
36
+ find_package(OpenMP COMPONENTS C)
37
+ if(OpenMP_C_FOUND)
38
+ target_link_libraries(gpimg PUBLIC OpenMP::OpenMP_C)
39
+ endif()
40
+ endif()
41
+
42
+ add_executable(gpimg_example examples/example.c)
43
+ target_link_libraries(gpimg_example PRIVATE gpimg)
44
+
45
+ enable_testing()
46
+ add_executable(test_gpimg tests/test_gpimg.c)
47
+ target_link_libraries(test_gpimg PRIVATE gpimg)
48
+ add_test(NAME test_gpimg COMMAND test_gpimg)
49
+
50
+ include(GNUInstallDirs)
51
+ install(TARGETS gpimg EXPORT gpimgTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
52
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
53
+ install(FILES include/gpimg.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
54
+ install(EXPORT gpimgTargets NAMESPACE gpimg:: FILE gpimgConfig.cmake
55
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/gpimg)
gpimg-0.1.0/c/Makefile ADDED
@@ -0,0 +1,81 @@
1
+ # gpimg C library: exact O(n) 2D Gaussian processes for multi-band light curves.
2
+ #
3
+ # make shared + static library in build/ (libgpimg.so / .dylib, libgpimg.a)
4
+ # make example build/example (fit + image of a synthetic light curve)
5
+ # make test build/test_gpimg, run it (likelihood vs dense Cholesky, gradients, smoothers)
6
+ # make install PREFIX=/usr/local by default: lib/, include/gpimg.h, lib/pkgconfig/gpimg.pc
7
+ #
8
+ # Variables: CC (default cc), ARCH (e.g. ARCH=-march=native for the fastest build on this machine),
9
+ # OPENMP=0 to build without OpenMP (gp_batch then runs serially), PREFIX, DESTDIR.
10
+ CC ?= cc
11
+ ARCH ?=
12
+ OPENMP ?= 1
13
+ PREFIX ?= /usr/local
14
+ VERSION := 0.1.0
15
+
16
+ UNAME := $(shell uname -s)
17
+ ifeq ($(UNAME),Darwin)
18
+ SHLIB := libgpimg.dylib
19
+ SHFLAGS := -dynamiclib -install_name $(PREFIX)/lib/$(SHLIB)
20
+ else
21
+ SHLIB := libgpimg.so
22
+ SHFLAGS := -shared -Wl,-soname,libgpimg.so.0
23
+ endif
24
+
25
+ # IEEE semantics are kept (no -ffast-math); FMA contraction is allowed.
26
+ CFLAGS_BASE := -O3 $(ARCH) -fno-math-errno -ffp-contract=fast -fPIC -std=gnu11 -Wall -Wextra -Iinclude
27
+ ifeq ($(OPENMP),1)
28
+ OMPFLAGS := -fopenmp
29
+ endif
30
+ CFLAGS_ALL := $(CFLAGS_BASE) $(OMPFLAGS) $(CFLAGS)
31
+ LDLIBS_ALL := $(OMPFLAGS) -lm
32
+
33
+ B := build
34
+
35
+ all: $(B)/$(SHLIB) $(B)/libgpimg.a
36
+
37
+ $(B):
38
+ mkdir -p $(B)
39
+
40
+ $(B)/gpimg.o: src/gpimg.c include/gpimg.h | $(B)
41
+ $(CC) $(CFLAGS_ALL) -c $< -o $@
42
+
43
+ $(B)/$(SHLIB): $(B)/gpimg.o
44
+ $(CC) $(SHFLAGS) $< -o $@ $(LDLIBS_ALL) $(LDFLAGS)
45
+
46
+ $(B)/libgpimg.a: $(B)/gpimg.o
47
+ $(AR) rcs $@ $<
48
+
49
+ $(B)/example: examples/example.c $(B)/libgpimg.a
50
+ $(CC) $(CFLAGS_ALL) $< $(B)/libgpimg.a -o $@ $(LDLIBS_ALL) $(LDFLAGS)
51
+
52
+ $(B)/test_gpimg: tests/test_gpimg.c $(B)/libgpimg.a
53
+ $(CC) $(CFLAGS_ALL) $< $(B)/libgpimg.a -o $@ $(LDLIBS_ALL) $(LDFLAGS)
54
+
55
+ example: $(B)/example
56
+ ./$(B)/example
57
+
58
+ test: $(B)/test_gpimg
59
+ ./$(B)/test_gpimg
60
+
61
+ $(B)/gpimg.pc: | $(B)
62
+ printf 'prefix=$(PREFIX)\nlibdir=$${prefix}/lib\nincludedir=$${prefix}/include\n\nName: gpimg\nDescription: Exact O(n) 2D Gaussian processes for multi-band light curves\nVersion: $(VERSION)\nLibs: -L$${libdir} -lgpimg\nLibs.private: $(OMPFLAGS) -lm\nCflags: -I$${includedir}\n' > $@
63
+
64
+ install: all $(B)/gpimg.pc
65
+ install -d $(DESTDIR)$(PREFIX)/lib $(DESTDIR)$(PREFIX)/include $(DESTDIR)$(PREFIX)/lib/pkgconfig
66
+ install -m 644 include/gpimg.h $(DESTDIR)$(PREFIX)/include/
67
+ install -m 755 $(B)/$(SHLIB) $(DESTDIR)$(PREFIX)/lib/
68
+ install -m 644 $(B)/libgpimg.a $(DESTDIR)$(PREFIX)/lib/
69
+ install -m 644 $(B)/gpimg.pc $(DESTDIR)$(PREFIX)/lib/pkgconfig/
70
+ ifneq ($(UNAME),Darwin)
71
+ ln -sf $(SHLIB) $(DESTDIR)$(PREFIX)/lib/libgpimg.so.0
72
+ endif
73
+
74
+ uninstall:
75
+ rm -f $(DESTDIR)$(PREFIX)/include/gpimg.h $(DESTDIR)$(PREFIX)/lib/$(SHLIB) $(DESTDIR)$(PREFIX)/lib/libgpimg.so.0 \
76
+ $(DESTDIR)$(PREFIX)/lib/libgpimg.a $(DESTDIR)$(PREFIX)/lib/pkgconfig/gpimg.pc
77
+
78
+ clean:
79
+ rm -rf $(B)
80
+
81
+ .PHONY: all example test install uninstall clean
@@ -0,0 +1,68 @@
1
+ /* example.c -- fit and image a synthetic six-band light curve with the gpimg C library.
2
+ *
3
+ * make -C c example (or: cc -O2 example.c -Iinclude -Lbuild -lgpimg -lm -fopenmp)
4
+ *
5
+ * Times in days, wavelengths in Angstrom (passed as natural logs), fluxes in any unit.
6
+ */
7
+ #include <math.h>
8
+ #include <stdint.h>
9
+ #include <stdio.h>
10
+ #include <stdlib.h>
11
+
12
+ #include "gpimg.h"
13
+
14
+ int main(void)
15
+ {
16
+ /* LSST ugrizy effective wavelengths -> ln(lambda) */
17
+ const double wave[6] = {3671.0, 4827.0, 6223.0, 7546.0, 8691.0, 9712.0};
18
+ double lnwave[6];
19
+ for (int b = 0; b < 6; b++) lnwave[b] = log(wave[b]);
20
+
21
+ /* a synthetic transient: rise and decline, redder bands peak later, observed every ~3 days in one band */
22
+ enum { N = 120 };
23
+ double t[N], y[N], err[N];
24
+ int32_t band[N];
25
+ srand(1);
26
+ for (int i = 0; i < N; i++) {
27
+ t[i] = -20.0 + 1.0 * i + 0.5 * rand() / (double)RAND_MAX;
28
+ band[i] = i % 6;
29
+ const double tp = 1.0 * band[i], x = t[i] - tp; /* peak time: one day later per band */
30
+ const double f = 1000.0 * exp(-x / 25.0) / (1.0 + exp(-x / 3.0));
31
+ err[i] = 20.0;
32
+ y[i] = f + err[i] * (2.0 * rand() / (double)RAND_MAX - 1.0);
33
+ }
34
+
35
+ /* maximum-likelihood fit: theta = (ln A, ln l_t, ln l_lambda) */
36
+ gp_fit_opts opts;
37
+ gp_fit_default_opts(&opts);
38
+ opts.nstart = 5;
39
+ const double theta0[3] = {log(1000.0), log(20.0), log(0.8)};
40
+ gp_fit_result res;
41
+ gp_fit(N, t, band, y, err, lnwave, theta0, &opts, &res);
42
+ printf("fit: A = %.1f, l_t = %.2f d, l_lambda = %.3f (ln lambda); -ln L = %.3f; %d iterations, status %d\n",
43
+ exp(res.theta[0]), exp(res.theta[1]), exp(res.theta[2]), res.nll, res.nit, res.status);
44
+
45
+ /* likelihood and its analytic gradient at the optimum */
46
+ double grad[3];
47
+ const double nll = gp_nll_grad(N, t, band, y, err, lnwave, 3, res.theta, grad);
48
+ printf("-ln L = %.6f, gradient = (%.2e, %.2e, %.2e)\n", nll, grad[0], grad[1], grad[2]);
49
+
50
+ /* posterior image on 100 times x 50 wavelengths, smoothed (MBF) and with the stable RTS smoother */
51
+ enum { NT = 100, NL = 50 };
52
+ double tg[NT], lng[NL];
53
+ for (int g = 0; g < NT; g++) tg[g] = -30.0 + 130.0 * g / (NT - 1);
54
+ for (int l = 0; l < NL; l++) lng[l] = log(3200.0) + (log(10200.0) - log(3200.0)) * l / (NL - 1);
55
+ double *mean = malloc(sizeof(double) * NL * NT), *var = malloc(sizeof(double) * NL * NT);
56
+ const int rank = gp_image(N, t, band, y, err, 6, lnwave, 3, res.theta, NT, tg, NL, lng, mean, var,
57
+ GPIMG_SMOOTH(GPIMG_RTS));
58
+ const int l = NL / 2, g = NT / 2;
59
+ printf("image (rank %d): at t = %.1f d, lambda = %.0f A: flux = %.1f +- %.1f\n", rank, tg[g], exp(lng[l]),
60
+ mean[l * NT + g], sqrt(var[l * NT + g]));
61
+
62
+ /* the same column as known in real time: filtered (causal) posterior */
63
+ gp_image(N, t, band, y, err, 6, lnwave, 3, res.theta, NT, tg, NL, lng, mean, var, GPIMG_CAUSAL);
64
+ printf("causal image at the same point: flux = %.1f +- %.1f\n", mean[l * NT + g], sqrt(var[l * NT + g]));
65
+ free(mean);
66
+ free(var);
67
+ return 0;
68
+ }
@@ -0,0 +1,113 @@
1
+ /* gpimg: exact O(n) two-dimensional Gaussian-process fits and images for multi-band light curves.
2
+ *
3
+ * Kernel
4
+ * k((t,x),(t',x')) = A^2 (1 + sqrt3|dt|/lt) exp(-sqrt3|dt|/lt) exp(-(x-x')^2 / (2 ll^2)), x = ln(lambda)
5
+ * zero mean, measurement variances err^2 on the diagonal. Hyperparameters theta = (ln A, ln lt, ln ll).
6
+ *
7
+ * Optional jitter. Every function taking theta also takes ntheta: 3 for the kernel above, or 4 to add a white
8
+ * jitter variance j^2 = exp(2 theta[3]) to every measurement variance -- extra scatter beyond the quoted errors,
9
+ * fitted like any other hyperparameter (analytic gradient, bounds opts->lo[3]/hi[3], opts->ntheta = 4). It is
10
+ * optional in the strict sense: ntheta = 3 never reads theta[3] and costs nothing -- the likelihood, its gradient
11
+ * and the whole image path are bit-identical to the three-parameter library (gp_fit lands on the same optimum to
12
+ * ~1e-8 in theta, far inside its own pgtol, since a BFGS iterate path is not bit-reproducible across builds).
13
+ * j is in the flux unit of y, so scale its bounds to your data.
14
+ *
15
+ * The Matern-3/2 time kernel is a 2-state linear SDE and the kernel is separable, so the GP observed in B <= 6
16
+ * bands is an exact 2B-state linear Gaussian state-space model: the Kalman filter gives the exact likelihood, a
17
+ * smoother the exact posterior, and any wavelength follows exactly from the band processes. No approximation is made.
18
+ *
19
+ * Inputs need not be time-sorted. band[i] in 0..nband_tot-1 indexes lnwave[]. Times in any unit consistent with
20
+ * lt (days in the Python package). All functions are re-entrant (no global state) and thread-safe.
21
+ */
22
+ #ifndef GPIMG_H
23
+ #define GPIMG_H
24
+ #include <stdint.h>
25
+
26
+ #ifdef __cplusplus
27
+ extern "C" {
28
+ #endif
29
+
30
+ #define GPIMG_VERSION "0.1.0"
31
+ #define GPIMG_MAXB 6
32
+ #define GPIMG_MAXTHETA 4 /* (ln A, ln lt, ln ll) and the optional ln jitter */
33
+ #define GPIMG_NINFO 6 /* per-object info: nll, nit, nfev, status, t_fit [s], t_image [s] */
34
+
35
+ /* image mode: GPIMG_CAUSAL (filtered posteriors) or a smoother, e.g. GPIMG_SMOOTH(GPIMG_RTS) */
36
+ #define GPIMG_CAUSAL 1
37
+ #define GPIMG_MBF 0 /* modified Bryson-Frazier: inversion-free, fastest (default) */
38
+ #define GPIMG_RTS 1 /* Rauch-Tung-Striebel, Joseph form: numerically stable */
39
+ #define GPIMG_RTS_PLAIN 2 /* Rauch-Tung-Striebel, textbook form */
40
+ #define GPIMG_SMOOTH(s) ((s) << 1)
41
+ #define GPIMG_SMOOTHER(mode) (((mode) >> 1) & 3)
42
+
43
+ /* negative log marginal likelihood (incl. 0.5 n ln 2pi) and, if grad != NULL, its gradient wrt theta */
44
+ double gp_nll_grad(int n, const double *t, const int32_t *band, const double *y, const double *err,
45
+ const double *lnwave, int ntheta, const double *theta, double *grad);
46
+
47
+ /* testing: gradient by reverse mode (adjoint) and by forward mode; gnoise = sum_i err_i^2 dL/d(err_i^2) */
48
+ /* grad_fwd[3] (the jitter) is returned as NaN: the forward tangents cover the three kernel parameters. */
49
+ double gp_nll_grad_check(int n, const double *t, const int32_t *band, const double *y, const double *err,
50
+ const double *lnwave, int ntheta, const double *theta, double *grad_adj,
51
+ double *grad_fwd, double *gnoise);
52
+
53
+ /* fit options / result */
54
+ typedef struct {
55
+ double lo[GPIMG_MAXTHETA], hi[GPIMG_MAXTHETA]; /* bounds on theta; [3] read only when ntheta = 4 */
56
+ double pgtol; /* stop when max |projected gradient| <= pgtol */
57
+ double ftol; /* stop when (f_old - f) <= ftol * max(|f|, 1) */
58
+ int maxiter;
59
+ int nstart; /* 1: start at theta0 only. k > 1: also the first k-1 points of a fixed design around
60
+ theta0 (GPIMG_NDESIGN max); keep the lowest -lnL (multimodal likelihoods) */
61
+ int ntheta; /* 3 (default) or 4: fit the jitter too. theta0 and res.theta have this many entries */
62
+ } gp_fit_opts;
63
+
64
+ #define GPIMG_NDESIGN 18
65
+
66
+ typedef struct {
67
+ double theta[GPIMG_MAXTHETA]; /* opts->ntheta entries are written */
68
+ double nll;
69
+ double pgnorm;
70
+ int nit, nfev, status; /* status: 0 pgtol, 1 ftol, 2 maxiter, 3 line search failed */
71
+ } gp_fit_result;
72
+
73
+ void gp_fit_default_opts(gp_fit_opts *o);
74
+
75
+ /* maximum-likelihood fit starting at theta0 (projected onto the bounds) */
76
+ void gp_fit(int n, const double *t, const int32_t *band, const double *y, const double *err,
77
+ const double *lnwave, const double *theta0, const gp_fit_opts *opts, gp_fit_result *res);
78
+
79
+ /* posterior mean and variance of the noise-free flux on the grid tg[nt] x lng[nl] (ln lambda); tg must be sorted
80
+ * ascending. Out arrays are row-major [nl][nt] (wavelength rows, time columns). nband_tot: bands in lnwave (<= 6).
81
+ * mode: see GPIMG_CAUSAL / GPIMG_SMOOTH above (0 = smoothed, MBF). Returns the latent rank used. */
82
+ int gp_image(int n, const double *t, const int32_t *band, const double *y, const double *err,
83
+ int nband_tot, const double *lnwave, int ntheta, const double *theta,
84
+ int nt, const double *tg, int nl, const double *lng, double *mean, double *var, int mode);
85
+
86
+ /* As gp_image, plus the same-time posterior covariance of pairs of grid wavelengths:
87
+ * cov[p * nt + g] = cov(f(tg[g], lng[pa[p]]), f(tg[g], lng[pb[p]])), p = 0..npair-1.
88
+ * pa/pb index lng[] and are NOT range-checked; pa[p] == pb[p] reproduces that wavelength's variance. cov is never
89
+ * clamped (a covariance may be negative). npair = 0 or cov = NULL behaves exactly like gp_image. Cost: O(npair nt)
90
+ * on top of the image, with no extra filter or smoother pass. */
91
+ int gp_image_cov(int n, const double *t, const int32_t *band, const double *y, const double *err,
92
+ int nband_tot, const double *lnwave, int ntheta, const double *theta,
93
+ int nt, const double *tg, int nl, const double *lng, double *mean, double *var,
94
+ int npair, const int32_t *pa, const int32_t *pb, double *cov, int mode);
95
+
96
+ /* nll computed through the latent (image) model: cross-check of the two state-space formulations */
97
+ double gp_nll_latent(int n, const double *t, const int32_t *band, const double *y, const double *err,
98
+ int nband_tot, const double *lnwave, int ntheta, const double *theta);
99
+
100
+ /* batch: objects o = 0..nobj-1 own rows off[o]..off[o+1]-1. Fits (if fit != 0, starting at theta0[o]) and
101
+ * makes images for every object with OpenMP. theta0/theta_out are [nobj][opts->ntheta].
102
+ * Outputs float32 mean/var [nobj][nl][nt], theta_out [nobj][ntheta],
103
+ * info [nobj][GPIMG_NINFO] = (nll, nit, nfev, status, t_fit, t_image). nthreads <= 0: OpenMP default. */
104
+ void gp_batch(int nobj, const int64_t *off, const double *t, const int32_t *band, const double *y,
105
+ const double *err, int nband_tot, const double *lnwave, const double *theta0, int fit,
106
+ const gp_fit_opts *opts, int nt, const double *tg, int nl, const double *lng,
107
+ float *mean, float *var, double *theta_out, double *info, int mode, int nthreads);
108
+
109
+ #ifdef __cplusplus
110
+ }
111
+ #endif
112
+
113
+ #endif