Conversation
The old backward-tracing approach sampled rays from the sensor edge through the on-axis exit pupil, which fails for wide-angle lenses (0 valid rays due to pupil aberration at full field). Now sweeps 64 FOV angles in parallel via forward tracing and picks the angle whose centroid image height matches r_sensor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sample_ring_arm_rays now uses self.rfov (ray-traced) instead of self.rfov_eff (pinhole) so the full distorted field is covered. Remove the on-axis ring replacement (fov=0 → 0.01*max) that shifted the on-axis sample away from the optical axis. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unify ray sampling into a single function that takes FOV angles and depth. Infinite depth produces collimated parallel rays, finite depth produces diverging point-source rays with position determined by FOV and depth. Remove the intermediate sample_from_points_by_fov function and the unused prop_to parameter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
prune_surf used rfov_eff (pinhole FOV) to determine the ray tracing range, missing the outer field of wide-angle lenses with barrel distortion. Now uses rfov (ray-traced FOV) so the full field is covered and surface clear apertures are not underestimated. Also remove now-unused numpy import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sample_radial_rays(), calc_distortion_radial(), draw_distortion_radial(), and draw_field_curvature() all used rfov_eff (pinhole FOV) via a direct self.rfov access that predated the rfov/rfov_eff rename, missing the outer field of wide-angle lenses with barrel distortion. Now prioritize rfov (ray-traced FOV) with rfov_eff as fallback so the full sensor is covered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sample_radial_rays(), calc_distortion_radial(), draw_distortion_radial(), and draw_field_curvature() all used rfov_eff (pinhole FOV), missing the outer field of wide-angle lenses with barrel distortion. Now use rfov (ray-traced FOV) to cover the full sensor. No fallback needed since self.rfov is always set after post_computation(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… set_fnum bisection Replace Python branch on tensor k in Aspheric.is_within_data_range with torch.where so the function is traceable through torch.compile without graph breaks. Replace GeoLens.set_fnum with true midpoint bisection: wider bracket [0.1r, 5r], relative tolerance 1e-3, 40 iterations, and a logging.warning on non-convergence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…from layout title Clamp the radicand of the conic sqrt in Aspheric._sag and _dfdxy to >= EPSILON so out-of-range rays produce finite sag instead of NaN, allowing the optimizer to recover gracefully. Also drop the equivalent focal length segment from the GeoLens layout title in GeoLensVis. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace "Sensor Diagonal" with IMGH (semi-diagonal), append RGB wavelengths (nm) to the first title line, and add a second line with per-FoV RMS spot sizes from analysis_spot(). Set suptitle font to Nimbus Sans on both single and multi plot paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the green-centroid reference with a combined polychromatic centroid pooled from R, G, B ray intercepts per field point, matching Zemax's "RMS Spot Radius w.r.t. Centroid" definition. The previous approach averaged per-wavelength RMS values, which underweighted lateral chromatic aberration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pdate_r; fix io WAVL Drop match_mat flag and the conditional match_materials() call inside the curriculum loop in 2_autolens_rms.py; material snapping is now always done once after the curriculum stage. Add Phase.update_r() for flat phase surfaces: updates r/w/h without rescaling coefficients since norm_radii is fixed. Emit WAVL from WAVE_RGB in write_lens_zmx instead of hardcoded Fraunhofer triplet, keeping the output consistent with the project wavelength config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add two clearly labelled sections in config.py: - Tunable per-experiment: DEPTH, SPP_*, PSF_KS, GEO_GRID, DEFAULT_WAVE - Physical/numerical (do not modify): EPSILON/DELTA thresholds, WAVE_RGB, Fraunhofer lines, narrow-band spectra, hyperspectral constants Improve inline comments throughout to explain units and intent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add sample_more_off_axis=False parameter to optimize() and thread it through to sample_ring_arm_rays, allowing callers to concentrate ray samples toward the field edge for better off-axis correction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrite loss_rms to mirror optimize()'s per-FOV reweighting: green wavelength anchors a detached normalized weight mask so FOV points with larger spot error receive proportionally more gradient signal. Invalid rays are zeroed before squaring to avoid Inf*0 = NaN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…straints to post_computation Rename geolens_pkg/utils.py to optim_init.py so the three-file optimization workflow is discoverable as a group: optim_init (build starting point) → optim (gradient loop) → optim_ops (discrete edits). Also resolves naming collision with optics/utils.py. Move init_constraints() call from get_optimizer() into post_computation() so constraints are always refreshed when lens geometry is loaded or modified, not only when an optimizer is created. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- post_computation: mention constraint initialization in Calculates list - get_optimizer: correct Returns type (Adam, not list); improve Args wording - draw_layout: remove stale entrance_pupil arg; document auto-title behavior (IMGH, RGB wavelengths, RMS spot second line) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…misc cleanup - deeplens/optics/utils.py → ops.py: file only contains pure differentiable torch ops (interp1d, grid_sample_xy, foc_dist_balanced, diff_float, diff_quantize); added module docstring to prevent future drift; dropped numpy import (only wave_rgb needed it) - Removed wave_rgb() from ops.py — callers can compose from WAVE_RED/GREEN/BLUE constants already in config.py; removed TestWaveRgb class from test_basics.py - geolens_pkg/optim.py: replaced `from transformers import get_cosine_schedule_with_warmup` with a local implementation using only math + torch; dropped transformers dependency - geolens_pkg/io.py: removed redundant self.post_computation() call from read_lens_json() and updated docstring accordingly - Updated all import sites (hybridlens, diffraclens, psf_compute, diffractive, test_utils, test_basics) to import from ops instead of utils Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
foc_dist_balanced is experiment/EDoF plumbing, not a differentiable tensor op, so it does not belong in ops.py. Moved to deeplens/utils.py under a new EDoF section; updated test_utils.py import accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…GPU moves Replace torch.tensor/zeros/linspace().to(device) with device= at construction across six files. Eliminates implicit CPU→GPU syncs in hot paths: - geolens.py: calc_entrance_pupil ray_o + phi (already done in prior commit) - geolens_pkg/eval.py: linspace in chief_rays(), zeros for chief_ray_o/d, and zeros for o1/o2 in the spot-diagram branch - geolens_pkg/optim_init.py: d_sensor tensor in init_constraints - geometric_surface/base.py: surface_with_offset and surface_sag scalar→tensor conversion guards - geometric_surface/thinlens.py: __init__ and set_f - imgsim/psf.py: psf_map_crop and psf_map allocation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move heavy optional imports off the module-level critical path so that GeoLens load/inspect workflows avoid paying the cost of unused deps: - light/wave.py: save_image → save_npz, show; tqdm → RayleighSommerfeldIntegral - lens.py: make_grid/save_image → draw_psf_radial; tqdm → render psf_map branch - geolens_pkg/eval.py: skimage.metrics + save_image → analysis_rendering - deeplens/utils.py: peak_signal_noise_ratio → batch_PSNR; structural_similarity → batch_ssim; tqdm → create_video_from_images Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cumulation with softplus ray.obliq was a multiplicative product of cos(bend) values across surfaces, making it hard to interpret and sensitive to surface count. The new bend_penalty is an additive softplus accumulator: per_surf = softplus(cos(30°) - cos_bend, beta=50) ray.bend_penalty += per_surf * valid This rises smoothly once a surface bend exceeds 30° and stays near zero for mild refractions, giving an uncoupled per-surface signal. Changes: - ray.py: obliq → bend_penalty; init zeros instead of ones (additive identity) - geometric_surface/base.py: replace multiplicative obliq update with softplus accumulation using 30° gate - geolens_pkg/optim.py: obliq_min → bend_penalty_max (cellphone: 0.6, other: 1.0); loss_ray_angle now penalizes rays exceeding the max - test/test_ray.py: update clone attribute check Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…code Rule 2 (fix aperture distance when aper_idx == 0) is no longer needed. Removed it along with the now-unused aper_idx, optim_surf_range, and shape_changed variables, the dead `if shape_changed` print, and the bool return value. Function now contains only Rule 1 (shift first surface to z=0) and prune_surf. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ce_pupil=False When not using the entrance pupil, pupilz was hardcoded to 0, which assumed the first surface sits at z=0. Replace with `surfaces[0].d.item()` so ray sampling starts at the actual axial position of the first surface. Applies to `geolens.py` (forward_rays) and both visualization methods in `geolens_pkg/vis.py`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ized softplus - `init_constraints`: rename bound attrs to `air_edge_min/max` style; replace `bend_penalty_max` with `obliq_min`; add `ttl_min`; fix cellphone `thick_center_min` (0.4→0.25) and `ttl_max` (15→20). - `loss_reg`: new signature `(w_clearance, w_envelope, w_profile)`; delegates to `loss_bound()` + `loss_profile()`. - `loss_infocus`: add `wvln` param; switch to `softplus(rms-target)`. - `loss_surface` → `loss_profile`: softplus-based sag/grad penalties, range-normalized by constraint value (beta=10). - `loss_intersec` + `loss_thickness` → `loss_bound`: single sampling pass returning `(loss_clearance, loss_envelope)`; violations normalized by (max-min) band so gradient magnitudes are balanced. - `loss_ray_angle`: softplus CRA + bend penalties, `num_ring` 8→4; both terms use `cra_scale = 1 - cos_ref` normalization. - `base.py` bend penalty: add `bend_scale = 1 - cos_gate`; normalize softplus arg by `bend_scale`; lower beta 50→10 to match optim.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ss functions Reflect the refactor in the previous commit: - `air_min_edge` → `air_edge_min`, `thick_min_center` → `thick_center_min` - Check new attrs `ttl_min` and `obliq_min` exist after init_constraints - `loss_reg` dict keys: `loss_intersec/thickness/surf` → `loss_clearance/envelope/profile` - `test_loss_surface_scalar` → `test_loss_profile_scalar` (calls loss_profile) - `test_loss_intersec_scalar` + `test_loss_thickness_scalar` → `test_loss_bound_returns_tuple` (verifies loss_bound returns two scalar tensors, both >= 0) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- geolens.py sample_sensor: call .item() on self.d_sensor before passing to torch.full_like; d_sensor is a Tensor and full_like requires a scalar fill_value (TypeError in script 1). - 2_autolens_rms.py: fix stale import path deeplens.optics.geolens_pkg.utils → deeplens.optics.geolens_pkg (create_lens was moved to optim_init and re-exported from __init__). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors core optics simulation utilities and the GeoLens pipeline, with a focus on more robust field-of-view (FoV) estimation, consolidated ray sampling APIs, and improved device-safe tensor creation. It also reorganizes differentiable ops/imports and updates tests accordingly.
Changes:
- Refactor
GeoLens.calc_fov()to a forward-tracing sweep-based FoV estimate with fallback to effective FoV, plus additional constraints initialization inpost_computation(). - Consolidate ray sampling by folding point-source-by-FoV sampling into
sample_from_fov(), and replace the legacyRay.obliqwith an accumulatedbend_penalty. - Import/organization cleanup: move differentiable ops to
deeplens/optics/ops.py, move EDoF helper todeeplens/utils.py, and reduce heavy top-level imports by lazy-importing (skimage/torchvision/tqdm).
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
test/test_utils.py |
Updates imports to reflect ops/util split. |
test/test_ray.py |
Updates Ray cloning test for bend_penalty attribute. |
test/test_geolens_optim.py |
Aligns tests with renamed constraints and new loss breakdown. |
test/test_basics.py |
Removes wave_rgb() tests and keeps constants validation. |
deeplens/utils.py |
Moves heavy imports to function scope; adds foc_dist_balanced(). |
deeplens/optics/phase_surface/phase.py |
Adds update_r() convenience method for phase surface sizing. |
deeplens/optics/ops.py |
Narrows module to differentiable tensor ops; removes non-core helpers. |
deeplens/optics/light/wave.py |
Lazy-imports torchvision/tqdm for IO-heavy methods. |
deeplens/optics/light/ray.py |
Replaces obliq with bend_penalty and updates cloning/squeeze behavior. |
deeplens/optics/lens.py |
Lazy-imports torchvision/tqdm in visualization/render loops. |
deeplens/optics/imgsim/psf.py |
Fixes device placement for new tensors via device=. |
deeplens/optics/hybridlens.py |
Updates diff_float import to come from ops. |
deeplens/optics/geometric_surface/thinlens.py |
Ensures f tensor is created on the correct device. |
deeplens/optics/geometric_surface/base.py |
Adds per-surface bend penalty accumulation in refraction; device-safe tensor creation. |
deeplens/optics/geometric_surface/aspheric.py |
Improves numerical safety and tensorizes validity checks for compile/tracing. |
deeplens/optics/geolens_pkg/vis.py |
Improves titles/docs and adjusts default pupil z-reference; adds spot RMS to title. |
deeplens/optics/geolens_pkg/psf_compute.py |
Updates diff_float import to come from ops. |
deeplens/optics/geolens_pkg/optim_ops.py |
Refines pruning/shape correction flow and removes numpy dependency in pruning. |
deeplens/optics/geolens_pkg/optim_init.py |
Fixes device placement when creating d_sensor. |
deeplens/optics/geolens_pkg/optim.py |
Replaces legacy geometry losses with loss_profile/loss_bound; adds local cosine LR scheduler. |
deeplens/optics/geolens_pkg/io.py |
Uses WAVE_RGB for Zemax export and removes redundant post_computation() call on JSON read. |
deeplens/optics/geolens_pkg/eval.py |
Device-safe tensor creation and updated spot computation to pooled polychromatic centroid. |
deeplens/optics/geolens_pkg/__init__.py |
Exports create_lens from optim_init. |
deeplens/optics/geolens.py |
Forward-traced FoV calculation; unified sample_from_fov; adds init_constraints() to post_computation(). |
deeplens/optics/diffractive_surface/diffractive.py |
Updates diff_quantize import to come from ops. |
deeplens/optics/diffraclens.py |
Updates diff_float import to come from ops. |
deeplens/optics/config.py |
Reorganizes constants into “tunable” vs “physical/numerical” sections with clearer comments. |
2_autolens_rms.py |
Updates create_lens import path and removes unused curriculum args/logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if isinstance(rfov, float) and rfov > 0: | ||
| rfov = torch.linspace(0, rfov, 2) | ||
| rfov = torch.linspace(0, rfov, 2, device=self.device) | ||
| rfov = rfov.to(self.device) | ||
|
|
There was a problem hiding this comment.
calc_chief_ray_infinite() converts scalar rfov to a tensor only when rfov > 0. If a caller passes rfov=0.0 (which is a valid/useful case per the docstring), the next line rfov = rfov.to(self.device) will fail because a Python float has no .to(). Handle all scalar rfov values (e.g., int/float, including 0) by converting via torch.as_tensor/torch.tensor before calling .to().
There was a problem hiding this comment.
@copilot apply changes based on this feedback
| fov_deg = float(fov) * 180 / torch.pi | ||
| print(f"Using fov_deg: {fov_deg} during surface pruning.") | ||
| elif self.rfov_eff is not None: | ||
| fov_deg = self.rfov_eff * 180 / torch.pi |
There was a problem hiding this comment.
prune_surf() can reference fov_deg before assignment when both self.rfov and self.rfov_eff are None (the previous fallback based on atan(r_sensor/foclen) was removed). This will raise at runtime in the fov_y = ... line. Consider restoring a safe fallback (e.g., compute from r_sensor/foclen) or explicitly raising a clear error if FoV has not been computed yet.
| fov_deg = self.rfov_eff * 180 / torch.pi | |
| fov_deg = self.rfov_eff * 180 / torch.pi | |
| elif getattr(self, "r_sensor", None) is not None and getattr(self, "foclen", None) is not None: | |
| fov_deg = torch.atan(self.r_sensor / self.foclen) * 180 / torch.pi | |
| else: | |
| raise ValueError( | |
| "Cannot prune surfaces because field of view is unavailable: " | |
| "expected self.rfov or self.rfov_eff to be set, or both " | |
| "self.r_sensor and self.foclen to be available for fallback computation." | |
| ) |
When rfov=0.0 or rfov=0 (int), the old code skipped the tensor conversion (condition was `isinstance(rfov, float) and rfov > 0`), causing `rfov.to(self.device)` to fail with AttributeError on a Python float/int. Now all scalar rfov values (int or float, including 0) are converted to tensors before the `.to()` call: - rfov > 0: linspace([0, rfov]) two-element tensor (previous behavior) - rfov <= 0: single-element tensor [rfov] Agent-Logs-Url: https://github.com/vccimaging/DeepLens/sessions/daf7625d-9fb7-45c3-b490-b41fcef1662f Co-authored-by: singer-yang <25293821+singer-yang@users.noreply.github.com>
This pull request makes several improvements and cleanups to the optics simulation codebase, particularly in the
GeoLensclass and related configuration. The main changes include a refactor and enhancement of field-of-view (FoV) calculations, consolidation of ray sampling logic, and improvements to code clarity and maintainability. There are also some import cleanups and documentation improvements.Field-of-View Calculation and Ray Sampling:
calc_fovmethod inGeoLensto use forward ray tracing for real FoV calculation, improving robustness for wide-angle lenses and fixing failures in the previous backward-tracing approach. Now, FoV is determined by tracing rays from object space and matching image height to the sensor, with a fallback to effective FoV if tracing fails. [1] [2]sample_from_points_by_fovmethod intosample_from_fov, which now handles both collimated and point-source rays depending on thedepthargument. [1] [2] [3]calc_entrance_pupil,sample_sensor) to ensure compatibility with GPU/CPU execution and prevent device mismatch errors. [1] [2]fov_deginsample_radial_raysto avoid unnecessary conversion and ensure proper units.Configuration and Documentation:
deeplens/optics/config.pyby separating tunable experiment constants from physical/numerical constants, adding extensive comments, and clarifying the purpose of each parameter. No functional changes to values, but greatly improved readability and maintainability.deeplens/optics/geolens.py, including tracking of lens design constraints inpost_computation. [1] [2] [3] [4] [5] [6]API and Import Cleanups:
diff_floatanddiff_quantizenow imported from.opsinstead of.utils, andcreate_lensis imported directly fromgeolens_pkg. [1] [2] [3]curriculum_design(e.g.,match_matparameter and related logic). [1] [2] [3]These changes collectively improve the accuracy, reliability, and maintainability of the optics simulation codebase.