Skip to content

Aberrations simulation support : from twisted to PyTango#67

Merged
utkarshp1161 merged 7 commits intomainfrom
dev-up
May 3, 2026
Merged

Aberrations simulation support : from twisted to PyTango#67
utkarshp1161 merged 7 commits intomainfrom
dev-up

Conversation

@utkarshp1161
Copy link
Copy Markdown
Contributor

@utkarshp1161 utkarshp1161 commented Apr 9, 2026

Discussed in this issue: #66

What is done:

  1. Client Notebook: 2_b_Aberrations_twin : Generate aberrations using PyTemlib and register in twin and get images
  2. changes in CORRECTOR.py
  3. changes in ThermoDigitalTwin.py
  4. The simulation code is written in asyncroscopy/simulation/StemSim.py which borrows logic from https://github.com/pycroscopy/asyncroscopy/blob/twisted-legacy/asyncroscopy/cloned_repos/pystemsim/data_generator.py
  5. 2 cif files added to data/cif_files for simulation
  6. Test : tests/test_stem_sim.py added to test asyncroscopy/simulation/StemSim.py

Maybe future:
A) The image simulation returns a fixed (905,905) image:

def _acquire_stem_image_aberrations(self, imsize: int, dwell_time: float, detector_list: list) -> np.ndarray:
"""
"""
size = imsize
beam_current = 100 # pA
ab = self.ab
ab['acceleration_voltage'] = 60e3 # eV
fov = 96 # angstroms
ab['FOV'] = fov /12 # Angstroms
ab['convergence_angle'] = 30 # mrad
ab['wavelength'] = it.get_wavelength(ab['acceleration_voltage'])
cif_path = (
PROJECT_ROOT
/ "data"
/ "cif_files"
/ "WS2_ortho.cif"
)
print("Reading CIF from:", cif_path)
xtal = read(cif_path)
xtal = xtal * (30, 20, 1)
positions = xtal.get_positions()[:, :2]
pixel_size = 0.106 # angstrom/pixel
frame = (0,fov,0,fov) # limits of the image in angstroms
potential = dg.create_pseudo_potential(xtal, pixel_size, sigma=1, bounds=frame, atom_frame=11)
probe = dg.get_probe(ab, potential)
image = dg.convolve_kernel(potential, probe)
noisy_image = dg.lowfreq_noise(image, noise_level=0.5, freq_scale=.04)
scan_time = dwell_time * size * size
counts = scan_time * (beam_current * 1e-12) / (1.602e-19)
sim_im = dg.poisson_noise(noisy_image, counts=counts)
# convert args dict
# time.sleep(1)
image = np.array(image, dtype=np.float32)
sim_im = np.array(sim_im, dtype=np.float32)
print(sim_im.shape) # TODO: the simulation is independent of size and always returns - (905, 905) image -> need to check
return sim_im

B) The simulation related to nanoparticles
def _acquire_stem_image(self, imsize: int, dwell_time: float, detector_list: list) -> np.ndarray:
can go to https://github.com/pycroscopy/asyncroscopy/blob/dev-up/asyncroscopy/simulation/StemSim.py ?
C) Call CORRECTOR's all other commands through microscope as illustrated here for this case
ab = corrector.get_aberrations_coeff_sim()# is a json string
self.ab = json.loads(ab)

  • Why makes sense ? as EELS detector should be also called from microscope.get_spectrum(detector = "eels")

@AustinHouston
Copy link
Copy Markdown
Collaborator

A few things I would overhaul before merging

there should be no _sim functions (such as set_aberrations_coeff_sim and get_scanned_image_with_aberrations)
that's the whole point of asyncroscopy - the notebook can run on a thermo, a jeol, a nion, or a digital twin - doesn't matter.

to fix this, just the _get_scanned_image function in ThermoMicroscope does not change
the _get_scanned_image in TwinMicroscope just checks the corrector, makes the probe, etc.
(the abberations should be attributes of the corrector, with init values and limits)

@AustinHouston
Copy link
Copy Markdown
Collaborator

Now that you have implemented the ceos communication this should all be easier to do.

@utkarshp1161
Copy link
Copy Markdown
Contributor Author

(the abberations should be attributes of the corrector, with init values and limits)

I see two caveat's to it.
a) The actual corrector values are currently handled using json dump and not as attributes,

def acquire_tableau(self, args: str) -> str:
"""
Run a correction tableau on the CEOS corrector.
Parameters are packed into one space-separated string because
Tango scalar commands accept only one input argument::
proxy.run_tableau("Fast 18")
proxy.run_tableau("Full 0")
"""
parts = args.strip().split()
tab_type, angle_str = parts
angle = float(angle_str)
return self._call("acquireTableau", {"tabType": tab_type, "angle": angle})

b) The tableau acquired from the corrector has different enteries(even across different type: enhanced, fast) than from one we get from the simulation:

  • The one we get on actual corrector: - Case 1: ab_msg = corrector_proxy.acquire_tableau('Enhanced 40')

{'A1': [-1.722984470953829e-09, -1.3730557569257383e-09], 'A2': [1.3574035073166904e-07, -3.743269769849526e-10], 'C3': [-1.9124382493333032e-07, 0.0], 'C1': [-8.459171809676403e-10, 0.0], 'A4': [2.5050956356475984e-07, -5.679158622685287e-07], 'A3': [8.007267595329108e-08, 1.8635595817710287e-07], 'A5': [2.8761931131477272e-05, -2.9930062430712584e-05], 'B2': [5.072942708488215e-09, -1.0153237290799634e-08], 'B4': [5.410183467091773e-07, 6.431281147405137e-07], 'S3': [6.957003967213865e-07, 2.806877916263183e-07], 'C5': [-0.00015297021485109578, 0.0], 'D4': [8.243145596145829e-07, 1.0160618230844962e-05], 'WD': [-4.329105939535138e-06, -9.531246135832501e-06], 'acceleration_voltage': 60000.0, 'FOV': 10.0, 'convergence_angle': 3, 'wavelength': np.float64(0.004866060499839593), 'C01a': 0.0, 'C01b': 0.0, 'C10': 0.0, 'C12a': 0.0, 'C12b': 0.0, 'C21a': 0.0, 'C21b': 0.0, 'C23a': 0.0, 'C23b': 0.0, 'C30': 0.0, 'C32a': 0.0, 'C32b': 0.0, 'C34a': 0.0, 'C34b': 0.0, 'C41a': 0.0, 'C41b': 0.0, 'C43a': 0.0, 'C43b': 0.0, 'C45a': 0.0, 'C45b': 0.0, 'C50': 0.0, 'C52a': 0.0, 'C52b': 0.0, 'C54a': 0.0, 'C54b': 0.0, 'C56a': 0.0, 'C56b': 0.0}

  • The one we get on actual corrector: - Case 2: ab_msg = corrector_proxy.acquire_tableau('Fast 18')

{'A1': [9.74690226274642e-11, -1.3434929311990205e-09], 'A2': [7.590644738179404e-08, -3.566485459511854e-08], 'B2': [1.1154583587841222e-08, 3.025011301977759e-09], 'C1': [-1.6599977642222055e-09, 0.0], 'WD': [-8.611027075525367e-05, -0.00024514653261427925]}

  • The one we get from simulation:
    {'C10': 0, 'C12a': 0, 'C12b': 0.38448128113770325, 'C21a': -68.45251255685642, 'C21b': 64.85359774641199, 'C23a': 11.667578600494137, 'C23b': -29.775627778458194, 'C30': 123, 'C32a': 95.3047364258614, 'C32b': -189.72105710231244, 'C34a': -47.45099594807912, 'C34b': -94.67424667529909, 'C41a': -905.31842572806, 'C41b': 981.316128853203, 'C43a': 4021.8433526960034, 'C43b': 131.72716642732158, 'C45a': -4702.390968272048, 'C45b': -208.25028574642903, 'C50': 552000.0, 'C52a': -0.0, 'C52b': 0.0, 'C54a': -0.0, 'C54b': -0.0, 'C56a': -36663.643489934424, 'C56b': 21356.079837905396, 'acceleration_voltage': 200000, 'FOV': 34.241659495148205, 'Cc': 1000000.0, 'convergence_angle': 30, 'wavelength': 0.0025079340450548005}

@utkarshp1161
Copy link
Copy Markdown
Contributor Author

Since, this needs more thought about how to go about attributes. Merging this for now as it doesn't break other features and tests.

@utkarshp1161 utkarshp1161 merged commit 602b217 into main May 3, 2026
2 checks passed
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.

2 participants