Skip to main content

Hello,

We are urgently looking for an alternative to ZPL-GETZERNIKE using ZOS-API.

The point is that we cannot tell “TheSystem.MFE.GetOperandValue(MeritOperandType_ZERN, “ the maximum term of Zernike.

“TheSystem.MFE.GetOperandValue(MeritOperandType_ZERN, “ always develops only up to the given coefficient :-(

MAny Thanks

Hans-Jürgen

Hi Hans-Jürgen,

 

I’m not sure I understand the question correctly, but as a Merit Function operand, ZERN can only ever return a single value. The way this operand is inteded to be used in the Merit Function is by having a first ZERN operand with the Term parameter equal to the maximum term for the fitting (note that the minimum is 11 from the Help File). Then, subsequent ZERN operands (they need to strictly follow that first operand, you cannot have another operand in between!) that have the same parameters but a different Term value will return the Term fitted by the number of terms in the first operand, that way, OpticStudio only does the fit once (I hope that makes sense).

Here is a dummy example that illustrates this principle:

As you can see, the first ZERN has a Term of 25, this means the fit is made with 25 terms. Then, the following operands return a different term but since they dirrectly follow the ZERN at 25 term, they are all fitted to 25 terms.

This means that you cannot simply use the GetOperandValue from the ZOS-API MFE.

At this point, I can see two options. First, you could reproduce this Merit Function in the ZOS-API, evaluate it, and retrieve each operand value. Second, you could use the Analyze..Wavefront..Zernike Standard Coefficients (or Fringe or Annular) analysis inside the ZOS-API. This analysis is not fully implemented, so you’ll have to save the results to a text-file and then parse it. I think the first solution might be more efficient, so I’ll make an example of it below.

Here is the code I used (hopefully the comments are self-explanatory) with Python 3.8.13 and Pythonnet 2.5.2:

# The Merit Function Editor
TheMFE = TheSystem.MFE

# Delete all rows
TheMFE.DeleteAllRows()

# Maximum term
MaxTerm = 25

# ZERN Operand type
ZERNType = ZOSAPI.Editors.MFE.MeritOperandType.ZERN

# Get first operand
FirstOperand = TheMFE.GetOperandAt(1)

# Change its type to ZERN
FirstOperand.ChangeType(ZERNType)

# Common settings
Wave = 1
Samp = 2
Field = 1
Type = 1 # Standard coefficients
Epsilon = 0.0
Vertex = 0

# Adjust settings of first ZERN operand
FirstOperand.GetOperandCell(2).IntegerValue = MaxTerm # Term
FirstOperand.GetOperandCell(3).IntegerValue = Wave # Wave
FirstOperand.GetOperandCell(4).IntegerValue = Samp # Samp
FirstOperand.GetOperandCell(5).IntegerValue = Field # Field
FirstOperand.GetOperandCell(6).IntegerValue = Type # Type
FirstOperand.GetOperandCell(7).DoubleValue = Epsilon # Epsilon
FirstOperand.GetOperandCell(8).IntegerValue = Vertex # Vertex?

# Insert all other operands with a loop changing only the term number
# Note that we do not need the last term as it is already in the first operand
for Term in range(1, MaxTerm):
# Put new operand after the first ZERN
NewOperand = TheMFE.InsertNewOperandAt(2)

# Change new operand type to ZERN
NewOperand.ChangeType(ZERNType)

# Adjust its settings
NewOperand.GetOperandCell(2).IntegerValue = Term # Term
NewOperand.GetOperandCell(3).IntegerValue = Wave # Wave
NewOperand.GetOperandCell(4).IntegerValue = Samp # Samp
NewOperand.GetOperandCell(5).IntegerValue = Field # Field
NewOperand.GetOperandCell(6).IntegerValue = Type # Type
NewOperand.GetOperandCell(7).DoubleValue = Epsilon # Epsilon
NewOperand.GetOperandCell(8).IntegerValue = Vertex # Vertex?

# Evaluate the Merit Function
TheMFE.CalculateMeritFunction()

# Read and print the results (starting from the bottom)
for Index in range(MaxTerm, 0, -1):
# Current operand
CurrentOperand = TheMFE.GetOperandAt(Index)

# Corresponding term
Term = CurrentOperand.GetOperandCell(2).IntegerValue

# Corresponding value
TermValue = CurrentOperand.Value

# Print
print('Z {0} \t {1: .8f}'.format(Term, TermValue))

And here are the results:

Z 1 	 -0.97593483
Z 2 0.00000000
Z 3 0.00000000
Z 4 -0.28473181
Z 5 0.00000000
Z 6 0.00000000
Z 7 0.00000000
Z 8 -0.00000000
Z 9 -0.00000000
Z 10 0.00000000
Z 11 0.21694194
Z 12 0.00000000
Z 13 -0.00000000
Z 14 -0.00000008
Z 15 -0.00000000
Z 16 0.00000000
Z 17 0.00000000
Z 18 -0.00000000
Z 19 -0.00000000
Z 20 -0.00000000
Z 21 0.00000000
Z 22 0.00088701
Z 23 0.00000000
Z 24 0.00000000
Z 25 -0.00000000

Note that the time consuming part of the code is creating the Merti Function. However, as it is fairly independant of the file, you could create it once and save it to a *.MF file, and then just load it with the ZOS-API.

Let me know if this helps.


Take care,

 

David


Hi David,

thanks for the quick response.

We need the Zernike coefficients for quick analysis without changing anything on the MFE, without opening an analysis window.

So we found: TheSystem.MFE.GetOperandValue(MeritOperandType_ZERN, 

But this functionality only makes the Zernike expansion up to the specified term.

How can we force the GetOperandValue to use a defined maximum term?

To the use in MFE: 

It is sufficient that all the desired coefficients are present in one "group". This can also be with an ascending index.
Then the same maximum term is used for all coefficients in this “group”.

Viele Grüße

Hans-Jürgen


Hi Hans-Jürgen,

 

To the best of my knowledge, you can’t force GetOperandValue to use a defined maximum term.

Another solution could be to create a user-defined operand (UDOC) that you could call with GetOperandValue, but it could be as slow as using an analysis.

How fast do you need the calculation to be?

Thanks for the comment on the ascending order, I didn’t know about it.

Take care,

 

David


Hi David,

UDOC’s are to slow. And by the way a “big” workaround.

We cannot understand that ZPL can do something that modern programming cannot.
ZEMAX has to improve here.

 

We may have to open an analysis window and read out the data. But I've never done that.

 

Great that I could help with the ascending term numbers. It's much "nicer"!

Hans-Jürgen


Hi Hans-Jürgen,

 

Unfortunately, I know they are slow :(

You can always send a feedback email to Zemax support and they might create a feature request for you. Its probably not going to happen in the short term though.

Opening and parsing an analysis window isn’t that bad. Its much easier and faster if the analysis is fully implemented, but the Zernike ones aren’t.

Here is an example for you:

# Open a Zernike Standard analysis
ZernikeStd = TheSystem.Analyses.New_ZernikeStandardCoefficients()

# Get analysis settings
Settings = ZernikeStd.GetSettings()

# Settings values
Field = 1
Surface = 3
Wave = 1
Samp = ZOSAPI.Analysis.SampleSizes.S_64x64
Vertex = False
Sub_x = 0.0
Sub_y = 0.0
Sub_z = 0.0
Eps = 0.0
MaxTerms = 25

# Update settings
Settings.Field.SetFieldNumber(Field)
Settings.Surface.SetSurfaceNumber(Surface)
Settings.Wavelength.SetWavelengthNumber(Wave)
Settings.SampleSize = Samp
Settings.ReferenceOBDToVertex = Vertex
Settings.Sx = Sub_x
Settings.Sy = Sub_y
Settings.Sz = Sub_z
Settings.Epsilon = Eps
Settings.MaximumNumberOfTerms = MaxTerms

# Apply settings and run analysis
ZernikeStd.ApplyAndWaitForCompletion()

# Save results to text file
Path = 'E:\ZernikeStd.txt'
ZernikeStd.GetResults().GetTextFile(Path)

# Close the analysis
ZernikeStd.Close()

# Read the text file
with open(Path) as F:
Lines = F.readlines()

# The relevant content seem to start at line 39 in the text file
# (hopefully this is conserved while modifying the settings)

# Read the relevant lines
for Index in range(38, len(Lines)):
# Split the line to isolate the numbers
SplitLine = Lines=Index].split()

# Term number
Term = int(SplitLinel1])
Value = float(SplitLinel2])

# Print the results
print('Z {0} \t {1: .8f}'.format(Term, Value))

And the results for the same dummy file I did earlier:

Z 1 	 -0.97593483
Z 2 0.00000000
Z 3 0.00000000
Z 4 -0.28473181
Z 5 0.00000000
Z 6 0.00000000
Z 7 0.00000000
Z 8 0.00000000
Z 9 0.00000000
Z 10 0.00000000
Z 11 0.21694194
Z 12 0.00000000
Z 13 0.00000000
Z 14 -0.00000008
Z 15 0.00000000
Z 16 0.00000000
Z 17 0.00000000
Z 18 0.00000000
Z 19 0.00000000
Z 20 0.00000000
Z 21 0.00000000
Z 22 0.00088701
Z 23 0.00000000
Z 24 0.00000000
Z 25 0.00000000

Hope that helps.

Take care,

 

David


Hi David,

Thanks!
We now do so - with the text file.
A support request is out. And we hope it will come true.

All the best!

Hans-Jürgen


Thanks for posting this question Hans!  I was having this exact issue so I’m going to implement David’s suggestion.  It would be great to have GetOperandValue for a ZERN operand.  It also seems like it should work according to their API documentation, but it’s not clear.  Hope they add that feature as it would be great to add any operand to the MFE without actually creating a .mf file to load each time.


Hi Hans-Jürgen,

 

Unfortunately, I know they are slow :(

You can always send a feedback email to Zemax support and they might create a feature request for you. Its probably not going to happen in the short term though.

Opening and parsing an analysis window isn’t that bad. Its much easier and faster if the analysis is fully implemented, but the Zernike ones aren’t.

Here is an example for you:

# Open a Zernike Standard analysis
ZernikeStd = TheSystem.Analyses.New_ZernikeStandardCoefficients()

# Get analysis settings
Settings = ZernikeStd.GetSettings()

# Settings values
Field = 1
Surface = 3
Wave = 1
Samp = ZOSAPI.Analysis.SampleSizes.S_64x64
Vertex = False
Sub_x = 0.0
Sub_y = 0.0
Sub_z = 0.0
Eps = 0.0
MaxTerms = 25

# Update settings
Settings.Field.SetFieldNumber(Field)
Settings.Surface.SetSurfaceNumber(Surface)
Settings.Wavelength.SetWavelengthNumber(Wave)
Settings.SampleSize = Samp
Settings.ReferenceOBDToVertex = Vertex
Settings.Sx = Sub_x
Settings.Sy = Sub_y
Settings.Sz = Sub_z
Settings.Epsilon = Eps
Settings.MaximumNumberOfTerms = MaxTerms

# Apply settings and run analysis
ZernikeStd.ApplyAndWaitForCompletion()

# Save results to text file
Path = 'E:\ZernikeStd.txt'
ZernikeStd.GetResults().GetTextFile(Path)

# Close the analysis
ZernikeStd.Close()

# Read the text file
with open(Path) as F:
Lines = F.readlines()

# The relevant content seem to start at line 39 in the text file
# (hopefully this is conserved while modifying the settings)

# Read the relevant lines
for Index in range(38, len(Lines)):
# Split the line to isolate the numbers
SplitLine = Lines=Index].split()

# Term number
Term = int(SplitLinel1])
Value = float(SplitLinel2])

# Print the results
print('Z {0} \t {1: .8f}'.format(Term, Value))

And the results for the same dummy file I did earlier:

Z 1 	 -0.97593483
Z 2 0.00000000
Z 3 0.00000000
Z 4 -0.28473181
Z 5 0.00000000
Z 6 0.00000000
Z 7 0.00000000
Z 8 0.00000000
Z 9 0.00000000
Z 10 0.00000000
Z 11 0.21694194
Z 12 0.00000000
Z 13 0.00000000
Z 14 -0.00000008
Z 15 0.00000000
Z 16 0.00000000
Z 17 0.00000000
Z 18 0.00000000
Z 19 0.00000000
Z 20 0.00000000
Z 21 0.00000000
Z 22 0.00088701
Z 23 0.00000000
Z 24 0.00000000
Z 25 0.00000000

Hope that helps.

Take care,

 

David

FYI, I’ve notice that I had to specify that the encoding was utf-16 when opening the file.  So that may be necessary for other files.



 

FYI, I’ve notice that I had to specify that the encoding was utf-16 when opening the file.  So that may be necessary for other files.

@Alex.TripsasThat's right, and you can configure the encoding of text output files under OpticStudio Preferences > General > TXT File Encoding. On your system, this is probably set to Unicode, and then you need to set the encoding to utf-16-le when reading the files.

Autodetection of the encoding was recently added to ZOSPy: https://github.com/MREYE-LUMC/ZOSPy/blob/75784f4f9ca33d1c905a938c8c67093cbab0778b/zospy/zpcore.py#L628.
If you use the Zernike Standard Coefficients analysis from that library, it will detect the encoding from your OpticStudio preferences and use it to parse the Zernike Standard Coefficients table to a dataframe.


Hi @chaasjes,

 

I was gonna mention ZOSPy as well, but I had not realized how you implemented this. Does that mean that if I have text files that I generated with ANSI encoding, and then later on I changed this setting to Unicode in OpticStudio. Then ZOSPy won’t parse them correctly? Isn’t it safer to check the encoding for every text file? I haven’t tried this myself, but there seem to be a possibility to do that with chardet in Python:

https://www.kaggle.com/code/rtatman/automatically-detecting-character-encodings

Take care,

 

David


Hi David,

In principle, that's true: ZOSPy will always open text files with the encoding configured in OpticStudio. However, the problem you mention does not apply to ZOSPy, because its analysis functions do not offer the ability to parse existing text output files. The text files that are opened by ZOSPy are therefore always generated using the encoding configured in the current OpticStudio instance.

If you want to store analysis results and load them later on, ZOSPy offers the ability to convert the analysis results to a JSON string. You can also load these JSON strings to an AnalysisResult object.

 


I see, it makes sense. Thank you for explaining.

For anyone trying chardet, this is what I obtained:

  • ANSI encoding: 
ascii
  • Unicode encoding:
UTF-16

Take care,

 

David


@David.Nguyen Thanks for trying out! A small addition regarding the ANSI encoding: ANSI is a bit weird, and usually means the “preferred encoding”, which depends on the system locale. On my system, this is cp1252, and OpticStudio indeed saves its text output files with this encoding when set to “ANSI”.


If you want to store analysis results and load them later on, ZOSPy offers the ability to convert the analysis results to a JSON string. You can also load these JSON strings to an AnalysisResult object.

I think it is indeed best to store things as JSON if you want to use/refer to them later, as it is an open and platform independent format

 

Reply