Skip to content

Commit 2adafc1

Browse files
committed
adding control on experiment detector field configuration
1 parent 5b180cc commit 2adafc1

7 files changed

Lines changed: 205 additions & 5 deletions

File tree

PyMieSim/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@
2121
__version__ = version = '5.1.0'
2222
__version_tuple__ = version_tuple = (5, 1, 0)
2323

24-
__commit_id__ = commit_id = 'g3fac92db6'
24+
__commit_id__ = commit_id = 'g5b180cc9b'

PyMieSim/cpp/experiment/detector_set/detector_set.cpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ PhotodiodeSet::PhotodiodeSet(
99
const std::vector<double> &gamma_offset,
1010
const PolarizationSet &polarization_filter_set,
1111
const MediumSet &medium_set,
12+
const std::vector<std::vector<complex128>> &angular_weights,
1213
const bool is_sequential
1314
)
1415
: sampling(sampling),
@@ -17,8 +18,32 @@ PhotodiodeSet::PhotodiodeSet(
1718
phi_offset(phi_offset),
1819
gamma_offset(gamma_offset),
1920
polarization_filter_set(polarization_filter_set),
20-
medium(medium_set)
21+
medium(medium_set),
22+
angular_weights(angular_weights),
23+
has_angular_weights(!angular_weights.empty())
2124
{
25+
if (this->has_angular_weights) {
26+
if (!is_sequential) {
27+
throw std::invalid_argument(
28+
"angular_weights are currently supported only for sequential PhotodiodeSet builds."
29+
);
30+
}
31+
32+
if (this->angular_weights.size() != this->sampling.size()) {
33+
throw std::invalid_argument(
34+
"angular_weights must provide one weight vector per sequential detector."
35+
);
36+
}
37+
38+
for (size_t index = 0; index < this->angular_weights.size(); ++index) {
39+
if (this->angular_weights[index].size() != this->sampling[index]) {
40+
throw std::invalid_argument(
41+
"Each angular weight vector must match the detector sampling at the same sequential index."
42+
);
43+
}
44+
}
45+
}
46+
2247
this->is_sequential = is_sequential;
2348
this->is_empty = false;
2449
this->update_shape();
@@ -51,6 +76,12 @@ PhotodiodeSet::get_detector_by_index(long long flat_index) const {
5176
this->medium[indices[6]]
5277
);
5378

79+
if (this->has_angular_weights) {
80+
throw std::invalid_argument(
81+
"angular_weights are currently supported only in sequential PhotodiodeSet mode."
82+
);
83+
}
84+
5485
detector->indices = indices;
5586

5687
return detector;
@@ -67,6 +98,11 @@ PhotodiodeSet::get_detector_by_index_sequential(size_t index) const {
6798
this->polarization_filter_set.polarization_states[index],
6899
this->medium[index]
69100
);
101+
102+
if (this->has_angular_weights) {
103+
detector->set_angular_weights(this->angular_weights[index]);
104+
}
105+
70106
detector->indices = {index};
71107
return detector;
72108
}

PyMieSim/cpp/experiment/detector_set/detector_set.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class PhotodiodeSet : public BaseDetectorSet
4747
std::vector<double> gamma_offset;
4848
PolarizationSet polarization_filter_set;
4949
MediumSet medium;
50+
std::vector<std::vector<complex128>> angular_weights;
51+
bool has_angular_weights = false;
5052

5153
PhotodiodeSet() = default;
5254

@@ -58,6 +60,7 @@ class PhotodiodeSet : public BaseDetectorSet
5860
const std::vector<double> &gamma_offset,
5961
const PolarizationSet &polarization_filter_set,
6062
const MediumSet &medium,
63+
const std::vector<std::vector<complex128>> &angular_weights,
6164
const bool is_sequential
6265
);
6366

PyMieSim/cpp/experiment/detector_set/interface.cpp

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,105 @@
1414

1515
namespace py = pybind11;
1616

17+
namespace {
18+
std::vector<std::vector<complex128>> cast_py_to_sequential_angular_weights(
19+
const py::object& angular_weights,
20+
const std::vector<unsigned>& sampling,
21+
const size_t target_size
22+
) {
23+
std::vector<std::vector<complex128>> weights_by_detector;
24+
25+
if (angular_weights.is_none()) {
26+
return weights_by_detector;
27+
}
28+
29+
if (py::isinstance<py::array>(angular_weights)) {
30+
py::array_t<std::complex<double>, py::array::c_style | py::array::forcecast> array =
31+
angular_weights.cast<py::array_t<std::complex<double>, py::array::c_style | py::array::forcecast>>();
32+
33+
if (array.ndim() == 1) {
34+
const size_t sampling_size = static_cast<size_t>(array.shape(0));
35+
36+
for (const unsigned detector_sampling : sampling) {
37+
if (sampling_size != detector_sampling) {
38+
throw py::value_error(
39+
"One-dimensional angular_weights arrays require a uniform detector sampling."
40+
);
41+
}
42+
}
43+
44+
std::vector<complex128> shared_weight_vector;
45+
vector_assign_from_numpy(shared_weight_vector, array);
46+
weights_by_detector.assign(target_size, shared_weight_vector);
47+
return weights_by_detector;
48+
}
49+
50+
if (array.ndim() == 2) {
51+
if (static_cast<size_t>(array.shape(0)) != target_size) {
52+
throw py::value_error(
53+
"Two-dimensional angular_weights arrays must have shape (target_size, sampling)."
54+
);
55+
}
56+
57+
const size_t sampling_size = static_cast<size_t>(array.shape(1));
58+
for (const unsigned detector_sampling : sampling) {
59+
if (sampling_size != detector_sampling) {
60+
throw py::value_error(
61+
"Two-dimensional angular_weights arrays require a uniform sampling shared by all detectors."
62+
);
63+
}
64+
}
65+
66+
auto unchecked = array.unchecked<2>();
67+
weights_by_detector.resize(target_size);
68+
69+
for (size_t detector_index = 0; detector_index < target_size; ++detector_index) {
70+
weights_by_detector[detector_index].resize(sampling_size);
71+
72+
for (size_t sample_index = 0; sample_index < sampling_size; ++sample_index) {
73+
weights_by_detector[detector_index][sample_index] = unchecked(
74+
static_cast<py::ssize_t>(detector_index),
75+
static_cast<py::ssize_t>(sample_index)
76+
);
77+
}
78+
}
79+
80+
return weights_by_detector;
81+
}
82+
83+
throw py::value_error("angular_weights must be one- or two-dimensional.");
84+
}
85+
86+
py::sequence sequence = py::reinterpret_borrow<py::sequence>(angular_weights);
87+
88+
if (static_cast<size_t>(py::len(sequence)) != target_size) {
89+
throw py::value_error(
90+
"Sequence angular_weights inputs must provide one weight vector per sequential detector."
91+
);
92+
}
93+
94+
weights_by_detector.resize(target_size);
95+
96+
for (size_t detector_index = 0; detector_index < target_size; ++detector_index) {
97+
py::array_t<std::complex<double>, py::array::c_style | py::array::forcecast> vector =
98+
py::cast<py::array_t<std::complex<double>, py::array::c_style | py::array::forcecast>>(sequence[detector_index]);
99+
100+
std::vector<complex128> cast_weights;
101+
vector_assign_from_numpy(cast_weights, vector);
102+
103+
if (cast_weights.size() != sampling[detector_index]) {
104+
throw py::value_error(
105+
"Each angular weight vector must match the detector sampling at the same sequential index."
106+
);
107+
}
108+
109+
weights_by_detector[detector_index] = std::move(cast_weights);
110+
}
111+
112+
return weights_by_detector;
113+
}
114+
}
115+
17116

18117
PYBIND11_MODULE(detector_set, module) {
19118
py::object ureg = get_shared_ureg();
@@ -109,6 +208,7 @@ PYBIND11_MODULE(detector_set, module) {
109208
Casting::cast_py_to_vector<double>(gamma_offset, "radian"),
110209
Casting::Polarization::cast_py_to_polarization_set(polarization_filter),
111210
Casting::Material::create_material_set_from_pyobject<MediumSet, double, BaseMedium, ConstantMedium>(medium, "medium"),
211+
std::vector<std::vector<complex128>>{},
112212
false
113213
);
114214
}
@@ -243,7 +343,8 @@ PYBIND11_MODULE(detector_set, module) {
243343
const py::object& sampling,
244344
const py::object& cache_numerical_aperture,
245345
const py::object& polarization_filter,
246-
const py::object& medium
346+
const py::object& medium,
347+
const py::object& angular_weights
247348
) {
248349
std::vector<unsigned> sampling_value =
249350
Casting::cast_py_to_broadcasted_vector<unsigned>(
@@ -295,6 +396,13 @@ PYBIND11_MODULE(detector_set, module) {
295396
target_size
296397
);
297398

399+
std::vector<std::vector<complex128>> angular_weights_value =
400+
cast_py_to_sequential_angular_weights(
401+
angular_weights,
402+
sampling_value,
403+
target_size
404+
);
405+
298406
return std::make_shared<PhotodiodeSet>(
299407
sampling_value,
300408
numerical_aperture_value,
@@ -303,6 +411,7 @@ PYBIND11_MODULE(detector_set, module) {
303411
gamma_offset_value,
304412
polarization_filter_set,
305413
medium_set,
414+
angular_weights_value,
306415
true
307416
);
308417
},
@@ -314,6 +423,7 @@ PYBIND11_MODULE(detector_set, module) {
314423
py::arg("cache_numerical_aperture") = py::float_(0.0),
315424
py::arg("polarization_filter") = PolarizationState(),
316425
py::arg("medium") = ConstantMedium(1.0),
426+
py::arg("angular_weights") = py::none(),
317427
R"pdoc(
318428
Build a sequential photodiode detector set.
319429
@@ -344,6 +454,12 @@ PYBIND11_MODULE(detector_set, module) {
344454
Surrounding medium. This can be provided either as medium
345455
objects or as refractive index values. Default is
346456
``ConstantMedium(1.0)``.
457+
angular_weights : array-like, optional
458+
Explicit complex angular weight vectors for each sequential
459+
detector. This can be a single one-dimensional array shared
460+
by all detectors, a two-dimensional array of shape
461+
``(target_size, sampling)``, or a list of one-dimensional
462+
arrays when detector sampling varies by sequential index.
347463
348464
Returns
349465
-------

PyMieSim/cpp/single/detector/base.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ class BaseDetector {
9898
gamma_offset(_gamma_offset),
9999
polarization_filter(polarization_filter),
100100
medium(std::move(_medium)),
101-
angular_weights(_sampling, complex128(1.0, 0.0)),
102-
mean_coupling(_mean_coupling)
101+
mean_coupling(_mean_coupling),
102+
angular_weights(_sampling, complex128(1.0, 0.0))
103103
{}
104104

105105
void set_angular_weights(const std::vector<complex128>& weights) {

docs/examples/extras/README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ Contents
1414
- ``plot_Qsca_vs_permittivity_vs_size_parameter.py`` - inspect scattering as material and size vary.
1515
- ``array_scattering.py`` - compute fields and Stokes parameters for arbitrary angles.
1616
- ``photodiode_angular_weights.py`` - apply a custom angular mask on a single photodiode detector.
17+
- ``experiment_photodiode_angular_weights.py`` - use explicit angular weight vectors in a sequential experiment detector set.

tests/validation/test_sequential.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,50 @@ def test_sequential_vs_standard_no_detector():
8888
data_sequential == data_standard
8989
), "Mismatch between sequential and standard computed data."
9090

91+
def test_sequential_photodiode_angular_weights_reduce_coupling():
92+
size = 2
93+
sampling = 128
94+
95+
source_sequential = GaussianSet.build_sequential(
96+
target_size=size,
97+
wavelength=[1000, 1000] * ureg.nanometer,
98+
polarization=PolarizationSet(angles=[0, 0] * ureg.degree),
99+
optical_power=[1, 1] * ureg.watt,
100+
numerical_aperture=[0.2, 0.2],
101+
)
102+
103+
scatterer_sequential = SphereSet.build_sequential(
104+
target_size=size,
105+
diameter=[1000, 1000] * ureg.nanometer,
106+
material=[1.5 + 0.5j, 1.5 + 0.5j],
107+
medium=[1.0, 1.0],
108+
)
109+
110+
angular_weights = numpy.ones((size, sampling), dtype=numpy.complex128)
111+
angular_weights[1, :] = 0.0
112+
113+
detector_sequential = PhotodiodeSet.build_sequential(
114+
target_size=size,
115+
phi_offset=[90, 90] * ureg.degree,
116+
gamma_offset=[0, 0] * ureg.degree,
117+
numerical_aperture=[0.1, 0.1],
118+
cache_numerical_aperture=[0.0, 0.0],
119+
sampling=[sampling, sampling],
120+
polarization_filter=[0, 0] * ureg.degree,
121+
medium=[1.0, 1.0],
122+
angular_weights=angular_weights,
123+
)
124+
125+
setup_sequential = Setup(
126+
scatterer_set=scatterer_sequential,
127+
source_set=source_sequential,
128+
detector_set=detector_sequential,
129+
)
130+
131+
coupling = setup_sequential.get_sequential("coupling")
132+
133+
assert coupling[0] > 0
134+
assert numpy.isclose(coupling[1], 0.0, atol=1e-20)
91135

92136
def test_sequential_vs_standard_detector():
93137
source_standard = GaussianSet(

0 commit comments

Comments
 (0)