Skip to content

feat[FieldData]: export to ZBF #2397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open

Conversation

bzhangflex
Copy link
Contributor

Adds a to_zbf() method to FieldData for exporting E fields to a ZBF file.

Closes #2321

Copy link
Collaborator

@yaugenst-flex yaugenst-flex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, looks great! Only some minor comments.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Two comments on the tests:

  1. It looks like you already implemented all the logic for reading a zbf file here. Should we also add a FieldDataset.from_zbf()? That'd make it really easy to create custom field sources from zbf data.
  2. Instead of re-implementing dummy field data generation here it's probably cleaner to set up a simulation and use the emulated run to get the field data? That'd also be a bit more representative of the actual workflow I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 2. are you referring to this for the emulated run?

def run_emulated(simulation: td.Simulation, path=None, **kwargs) -> td.SimulationData:

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea exactly

fname: str,
background_refractive_index: float,
freq: Optional[float] = None,
mode_index: Optional[int] = 0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is not really Optional but would always expect an int?


def read_beam_file(self, filename: str) -> tuple:
"""Read in a Zemax Beam file
Adapted from https://community.zemax.com/code-exchange-10/python-class-to-open-zbf-file-4845
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we worry about the (lack of) license in the original code? Is it assumed to be public domain because it was shared in a forum? I'm not sure if that's how it works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I'm not sure either 😅

# beamfilename = self.open_zbf()
f = open(filename, "rb")
# zemax version number
version = unpack("i", f.read(4))[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This use of unpack is not setting alignment and size, so it will only work if the native settings match the ZBF specs. In my original implementation I fixed the sizes and left native order (=) because the manual did not specify it, but it should probably be set little-endian (<).

Also, it could probably be a lot more efficient if it unpacked more values at once:

version, nx, ny, ispol, units = unpack("<5i", f.read(4 * 5))

Copy link
Collaborator

@tylerflex tylerflex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bzhangflex , this looks great! Will be super useful and a highly requested feature!

I had a few minor comments. I'm not familiar with zemax configuration so take what I say with a bit of a grain of salt.

mode_index : int
Mode index selection.
num_samples: int
Number of samples to use. Must be a power of 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain a bit more what is being sampled? and what does the default value correspond to?

e_x = e_x.sel(f=freq, drop=True)
e_y = e_y.sel(f=freq, drop=True)

if "mode_index" in e_x.coords:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what case does this cover? I guess if the object is a ModeSolverData?

If the mode_index is only used in this case, maybe it does actually make sense for the default value to be None? And then only do the isel here if it's not None?

@yaugenst-flex

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I assumed it was for ModeSolverData but I haven't tested it yet. I'll test it out and also change the default value

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if so maybe we can mention this in the changelog too


# Use the central frequency if freq is not specified
if freq is None:
freq = e_x.coords["f"].values[e_x.coords["f"].size // 2]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to worry if the number of frequency coords is even? wonder if we should rather interpolate to the mean frequency?

# Header info
version = 1
polarized = 1
units = 0 # "mm": 0, "cm": 1, "in": 2, "m": 3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be something that the user can configure through the kwargs?

version = 1
polarized = 1
units = 0 # "mm": 0, "cm": 1, "in": 2, "m": 3
lda = C_0 / freq * 1e-3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this 1e-3 floating around here and below makes me a bit nervous. I think it would be better if we had something like

# kwarg length_unit = "mm"

length_scale_map = dict(mm=1e-3, cm=1e-2, in=...) # maps kwarg to length scale factor

if length_unit not in length_scale_map:
    raise KeyError(...)

length_scale = length_scale_map[length_unit]

and then use length_scale in place of 1e-3.

If there are free units floating around like this it can lead to some unexpected bugs.

r_y = 0

# Pilot beam z position w.r.t. the waist
z_x = 0 * 1e-3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's with the 0 * 1e-3? are we planning to allow this to be modified eventually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! I'm not fully sure what the pilot beam is used for (seems like it helps guide the actual beam) but it sounds like a good idea to let the user define values for it.

# Number of samples: n_x = 2 ** a, a >= 5 and a <= 13 (32-bit), or a <= 14 (64-bit)
if num_samples is not None:
# custom user defined number of samples
# Raise an error if the number of samples is not a power of 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain why the num_samples needs to be a factor of 2? in principle we could interpolate using however points they provide?

Copy link
Contributor Author

@bzhangflex bzhangflex Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a requirement set by zemax. My guess is it might have to do with FFTs in their beam propagation algorithm.

Should we not enforce it here and let the user choose an arbitrary number? That could be fine too if Zemax emits a clear error message, or changes behavior in the future

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's probably fine to enforce it here. maybe we can just mention in the error message that this is a requirement for zemax

n_x = num_samples
n_y = num_samples
else:
n_x = 2 ** min(13, max(5, int(np.log2(x.size) + 1)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these 5 and 13 be hardcoded somewhere? eg as a module level constant? or configurable? not sure exactly where they come from. is it a requirement of zemax?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm it was left in from @lucas-flexcompute's code, I wonder if there was a specific reason for the values. If not maybe we don't specify any default n_x or n_y?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Zemax manual I have (dated July 8, 2011) defines that the number of samples must be a power of 2 between 32 and 8192 (inclusive)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be worth also adding this to the FAQ section of the docs, as we recently had some users unaware of our export to MATLAB. so probably some extra information about this capability in the docs wouldn't hurt. Also, not sure if it applies, but it might be nice also to add it to at least one notebook example.

@tomflexcompute
Copy link
Contributor

Since this is a bit experimental and largely untested, there might be cases that it will not work properly. Should we add a warning when it's called? Something like "This is an experimental feature. If you run into any issues, contact us at flexcompute.com/tidy3d/technical-support/"

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.

Add zbf file export in 2.9
5 participants