Skip to content

Add Chromatix thin-phase sample core - #70

Open
efe-bastug wants to merge 1 commit into
georgeoshardo:mainfrom
efe-bastug:brightfield-thin-sample
Open

Add Chromatix thin-phase sample core#70
efe-bastug wants to merge 1 commit into
georgeoshardo:mainfrom
efe-bastug:brightfield-thin-sample

Conversation

@efe-bastug

@efe-bastug efe-bastug commented Aug 27, 2026

Copy link
Copy Markdown

This adds the physical specimen input and the vector-field sample interaction as package code. It stops at the field immediately after the sample: the returned object is a Chromatix VectorField, not a brightfield image.

Two new files, no existing file touched:

  • SyMBac/brightfield.py
  • tests/test_brightfield.py

What each function does

scene_to_thickness_um(opl_scene, *, pix_mic_conv_um, resize_amount)

Converts a SyMBac scene into projected sample thickness in micrometres:

thickness_um = opl_scene * pix_mic_conv_um / resize_amount

Despite its legacy name, OPL_scene holds projected geometric thickness measured in supersampled simulation pixels, not an optical path length, so this is a pure change of units and no refractive-index contrast enters it. The scene must be two-dimensional, finite and non-negative, and both scale factors must be positive and finite. The shape is preserved, a new float64 array is returned, and the input is not modified. This function is pure NumPy and needs neither JAX nor Chromatix.

apply_thin_phase_sample(field, thickness_um, *, refractive_index_difference)

Applies that thickness map to one monochromatic Chromatix VectorField as a thin, isotropic, non-absorbing (pure-phase) sample:

phase_rad = 2 * pi * refractive_index_difference * thickness_um / wavelength_vacuum_um

The vacuum wavelength is read from the field itself, so there is no second wavelength argument that could disagree with the field it is applied to. The transmittance exp(1j * phase_rad) has unit modulus everywhere, and the same real phase reaches all three components, so intensity, power and polarization are unchanged. The function requires the field and the thickness map to share a spatial shape, requires a finite (possibly negative) refractive-index difference, and returns a new VectorField with the same spatial shape and the same [z, y, x] component order.

JAX and Chromatix are imported inside the call rather than at module import time, because this project installs them only for Python 3.12. Nothing in the module executes on import, and neither function plots, prints, writes files nor normalizes anything.

Units

Quantity Unit
opl_scene projected thickness, in supersampled simulation pixels
pix_mic_conv_um micrometres per pixel of the final, non-supersampled image
resize_amount dimensionless supersampling factor
thickness_um micrometres
refractive_index_difference dimensionless, may be signed
vacuum wavelength read from the field micrometres
phase_rad radians

Chromatix function used

chromatix.functional.phase_change, called as

cf.phase_change(field, jnp.asarray(phase_rad), spectrally_modulate=False)

The phase is already calculated for the single field wavelength, so the spectral-modulation choice is made explicit instead of relying on the default. cf.thin_sample is not used: in pinned Chromatix 0.6.0 it is documented for scalar fields, whereas this production path stays vectorial. In pinned 0.6.0 phase_change broadcasts the 2D phase to (y, x, 1) and returns field * exp(1j * phase), so one real phase reaches all three components. The production code never multiplies the field by exp(i * phase) by hand; that independent calculation exists only in the test.

Tests

tests/test_brightfield.py skips cleanly when JAX or Chromatix are unavailable, via pytest.importorskip at module level, and runs in the Python 3.12 Pixi environment. It covers:

  1. an asymmetric, non-square scene converted at exactly pix_mic_conv_um / resize_amount micrometres per supersampled pixel, with shape preserved and neither axis transposed;
  2. a small scene from the current draw_scene_from_segments passing through both functions;
  3. the resulting complex field against an independent input_field * exp(i * phase_rad) calculation;
  4. a vector plane wave with both transverse components non-zero receiving the same scalar phase on every component, leaving the component ratio, i.e. the polarization, unchanged;
  5. zero thickness and zero refractive-index difference leaving the field unchanged;
  6. VectorField.intensity and Field.power preserved by the pure-phase sample;
  7. output type, spatial shape, [z, y, x] component dimension, finite values and input immutability, for both the field and the thickness array;
  8. clear errors for non-two-dimensional, non-finite or negative scenes and thickness maps, non-positive or non-finite scale values, a non-finite refractive-index difference, a field/thickness shape mismatch, and a scalar field.

Two further tests check that the phase follows the wavelength stored in the field (0.405, 0.532 and 0.660 µm) and that a negative refractive-index difference produces a negative phase without changing the intensity.

Test commands and results

$ pixi run pytest tests/test_brightfield.py -q
28 passed in 64.84s

$ pixi run pytest -q
91 passed, 1 warning in 130.56s

The full suite was 63 passed with the same single warning before this branch, so the 28 new tests are additive and nothing existing changed. The warning is the pre-existing CPU-fallback notice from SyMBac/renderer.py:55 (no GPU backend on this machine).

git diff --check and git diff --cached --check are both clean.

AI assistance

An AI coding assistant was used to draft SyMBac/brightfield.py and tests/test_brightfield.py.

Not part of this PR

Propagation away from the sample, an objective, pupil or camera relay, defocus, partial coherence, multiple wavelengths, two-state unpolarized illumination, detector sampling, photons or noise, a scalar production mode, Renderer integration, figures, notebooks or example images, and performance optimization. PR #68 is untouched.

@efe-bastug
efe-bastug force-pushed the brightfield-thin-sample branch from 8482a4b to 72604d1 Compare August 27, 2026 20:23
@efe-bastug
efe-bastug force-pushed the brightfield-thin-sample branch from 72604d1 to 12570f7 Compare August 30, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant