-
Notifications
You must be signed in to change notification settings - Fork 54
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
base: develop
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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.
There was a problem hiding this comment.
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:
- 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. - 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.
There was a problem hiding this comment.
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?
Line 920 in b848f5d
def run_emulated(simulation: td.Simulation, path=None, **kwargs) -> td.SimulationData: |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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))
There was a problem hiding this 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 |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
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/" |
Adds a
to_zbf()
method toFieldData
for exporting E fields to a ZBF file.Closes #2321