Skip to content

Commit d7d80c0

Browse files
author
Ben Mazin
committed
Grow E-ELT subaperture packing
1 parent 150c5d6 commit d7d80c0

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

nullsim/physics/pupil_geometry.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,106 @@ def _evaluate_placement(
788788
return float(-log_dist_sum)
789789

790790

791+
def _max_feasible_diameter_for_positions(
792+
positions: NDArray[np.float64],
793+
pupil: PupilGeometry,
794+
diameter_m: float,
795+
) -> float:
796+
"""Largest sub-aperture diameter allowed by fixed center positions."""
797+
798+
positions = np.asarray(positions, dtype=np.float64)
799+
max_diameter = np.inf
800+
r_obs = pupil.central_obstruction_diameter_m / 2
801+
r_outer = diameter_m / 2
802+
803+
for x, y in positions:
804+
radial = float(np.hypot(x, y))
805+
if pupil.boundary_vertices is not None:
806+
edge_clearance = _signed_distance_to_polygon_edge(
807+
float(x), float(y), pupil.boundary_vertices
808+
)
809+
max_diameter = min(max_diameter, 2.0 * edge_clearance)
810+
else:
811+
max_diameter = min(max_diameter, 2.0 * (r_outer - radial))
812+
813+
if r_obs > 0.0:
814+
max_diameter = min(max_diameter, 2.0 * (radial - r_obs))
815+
816+
for vane in pupil.spider_vanes:
817+
vane_clearance = point_to_segment_distance(
818+
float(x),
819+
float(y),
820+
vane.x_start,
821+
vane.y_start,
822+
vane.x_end,
823+
vane.y_end,
824+
) - vane.width_m / 2
825+
max_diameter = min(max_diameter, 2.0 * vane_clearance)
826+
827+
if positions.shape[0] > 1:
828+
diffs = positions[:, None, :] - positions[None, :, :]
829+
distances = np.sqrt((diffs ** 2).sum(axis=-1))
830+
distances = distances[np.triu_indices(positions.shape[0], k=1)]
831+
max_diameter = min(max_diameter, float(np.min(distances)))
832+
833+
if not np.isfinite(max_diameter):
834+
return 0.0
835+
return max(0.0, float(max_diameter))
836+
837+
838+
def _grow_refined_placement(
839+
positions: NDArray[np.float64],
840+
d_sub: float,
841+
d_sub_hi: float,
842+
n_sub: int,
843+
pupil: PupilGeometry,
844+
diameter_m: float,
845+
) -> tuple[NDArray[np.float64], float]:
846+
"""Grow a feasible placement using hard-constraint refinement.
847+
848+
The seed search is intentionally conservative. For crowded pupils with
849+
spider vanes, such as E-ELT N=48, a feasible seed can often be enlarged by
850+
jointly relaxing all sub-aperture centers. This pass keeps the binary
851+
search's proven feasible layout as the starting point and only accepts a
852+
larger diameter when the existing hard-constraint score remains feasible.
853+
"""
854+
855+
if pupil.boundary_vertices is not None or not pupil.spider_vanes or n_sub < 24:
856+
return np.asarray(positions, dtype=np.float64), float(d_sub)
857+
858+
best_positions = np.asarray(positions, dtype=np.float64)
859+
best_d_sub = float(d_sub)
860+
low = best_d_sub
861+
high = max(float(d_sub_hi), low)
862+
if high <= low:
863+
return best_positions, best_d_sub
864+
865+
for _ in range(12):
866+
trial_d_sub = 0.5 * (low + high)
867+
trial_r_sub = 0.5 * trial_d_sub
868+
refined = _refine_positions(
869+
best_positions,
870+
n_sub,
871+
trial_r_sub,
872+
pupil,
873+
diameter_m,
874+
)
875+
max_diameter = _max_feasible_diameter_for_positions(
876+
refined,
877+
pupil,
878+
diameter_m,
879+
)
880+
accepted_d_sub = min(trial_d_sub, max_diameter)
881+
if accepted_d_sub > best_d_sub:
882+
best_positions = refined
883+
best_d_sub = accepted_d_sub
884+
low = accepted_d_sub
885+
else:
886+
high = trial_d_sub
887+
888+
return best_positions, best_d_sub
889+
890+
791891
def _place_single_subaperture(
792892
diameter_m: float, r_sub: float, pupil: PupilGeometry,
793893
) -> NDArray[np.float64]:
@@ -852,6 +952,7 @@ def optimize_subaperture_positions(
852952

853953
ring_d_max = _max_ring_d_sub(n_sub, pupil, diameter_m)
854954
d_sub_hi = max(diameter_m / np.sqrt(n_sub), ring_d_max) * 1.1
955+
d_sub_upper_bound = d_sub_hi
855956
d_sub_lo = 0.1
856957
best_d_sub = d_sub_lo
857958

@@ -919,6 +1020,15 @@ def optimize_subaperture_positions(
9191020
f"diameter={diameter_m:.1f}m"
9201021
)
9211022

1023+
best_result, best_d_sub = _grow_refined_placement(
1024+
best_result,
1025+
best_d_sub,
1026+
d_sub_upper_bound,
1027+
n_sub,
1028+
pupil,
1029+
diameter_m,
1030+
)
1031+
9221032
return best_result, best_d_sub
9231033

9241034

tests/physics/test_pupil_geometry.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,19 @@ def test_optimize_eelt_n18_returns_feasible_subapertures() -> None:
274274
r_sub = d_sub / 2.0
275275
for x, y in positions:
276276
assert is_valid_position(x, y, r_sub, pupil, EELT_DIAMETER_M)
277+
278+
279+
def test_optimize_eelt_n48_grows_packed_layout() -> None:
280+
pupil = eelt_pupil()
281+
positions, d_sub = optimize_subaperture_positions(48, EELT_DIAMETER_M, pupil)
282+
assert positions.shape == (48, 2)
283+
assert d_sub > 4.5
284+
285+
r_sub = d_sub / 2.0
286+
for x, y in positions:
287+
assert is_valid_position(x, y, r_sub, pupil, EELT_DIAMETER_M)
288+
289+
diffs = positions[:, np.newaxis, :] - positions[np.newaxis, :, :]
290+
distances = np.sqrt(np.sum(diffs * diffs, axis=2))
291+
distances = distances[np.triu_indices(positions.shape[0], k=1)]
292+
assert float(np.min(distances)) >= d_sub - 1.0e-8

0 commit comments

Comments
 (0)