Parameter Reconstruction¶
In order to use the new JCMoptimizer MATLAB interface, please follow the instructions in the JCMoptimizer Matlab documentation to install the interface for the new optimizer. The new interface (+jcmoptimizer) is placed in this directory.
In this tutorial we briefly discuss how we can perform a parameter reconstruction using a Mueller matrix ellipsometry dataset. We use the same project files that were also used in the discussion on Mueller matrix ellipsometry in the EM tutorial example.
We assume that we have acquired a set of measurements from a Mueller matrix ellipsometry
experiment on a grating, that was performed for a series of different incident light
wavelengths
. These measurements are arranged in a target vector
. As we control its construction we know which Mueller matrix element and wavelength contributes to which vector element.
We further assume that we can assign a measurement uncertainty
to each of the components in
, we denote the measurement uncertainty vector
as
. Please note that the actual ordering of the components within the
vector is of no importance for the reconstruction.
The information contained in the target vector
can be used to infer the
geometrical parameters of the investigated grating. This can be done by solving an inverse
problem. The approach for this is as follows. A parameterized model of the measurement
process is created. The model parameters are then varied in a systematic fashion, until a
set of model parameters is determined for which the calculated output of the model is
similar to the set of experimental measurements of the grating.
The parameterized model for the Mueller matrix ellipsometry experiment is created using
JCMsuite. A function is created which computes the Mueller matrix entries
using the FEM model for the same set of incident wavelengths
that
were used during the actual experiment. This involves the Fourier transformation and the
scattering matrix postprocesses discussed in the EM tutorial. The various matrix entries
are assembled in a vector
with the same ordering as the target vector
, and then returned.
The actual parameter reconstruction, that is the fit of the model output to the target
vector, can efficiently be performed using the BayesianLeastSquares driver of the
JCMoptimizer. The approach is closely related to Bayesian optimization and similarly
employs Gaussian processes (a machine learning surrogate model), and allows to perform a
global black box optimization of the least-squares problems. By using Gaussian processes
the method is very well suited for expensive model functions, such as a wavelength
dependent Mueller matrix calculation and is capable of finding a set of model parameters
that explain the experiment in fewer iterations than conventional methods. An in-depth
discussion and explanation of the approach is presented in an article on our Blog.
Reconstruction setup
The reconstruction script follows the same evaluator-based workflow as the Optimization tutorial. An archive to perform the reconstruction locally can be downloaded here. The complete script looks as follows. Please note that some logic is abstracted away into helper functions at the end of the script.
1% Add JCMsuite to MATLAB path
2jcm_root = getenv('JCMROOT'); % Linux: use: jcm_root = <JCMROOT> -> set your JCMROOT installation directory
3addpath(fullfile(jcm_root, 'ThirdPartySupport', 'Matlab'));
4
5%% Shutdown a possibly running daemon
6jcmwave_daemon_shutdown();
7
8
9% Parameters to play with
10NUM_ITERATIONS = 20;
11DERIVATIVE_ORDER = 1;
12FEM_DEGREE = 3;
13MULTIPLICITY = 1; % max of 1 for demo version
14
15
16%% Register a new computer resource
17jcmwave_daemon_add_workstation('Hostname', 'localhost', ...
18 'Multiplicity', MULTIPLICITY, ...
19 'NThreads', 2);
20
21% Main script
22
23% Define the paths
24root_dir = fileparts(mfilename('fullpath'));
25data_dir = fullfile(root_dir, 'data');
26project_file = fullfile(root_dir, 'jcm', 'project.jcmp');
27
28
29% Model parameters
30model_param_keys = struct('h', 55, 'width', 31, 'swa', 88, 'radius', 8);
31
32% Rest of the parameters for the JCMsuite project
33keys = struct('derivative_order', DERIVATIVE_ORDER, 'fem_degree', FEM_DEGREE, 'precision', 1e-3, ...
34 'n1', 1, 'n2', 1.4, 'n3', 1.967 + 4.443 * 1i, ...
35 'theta', 65, 'phi', 45, 'vacuum_wavelength', 365e-9);
36
37% Merge the two
38keys = mergeStructs(keys, model_param_keys);
39
40% Load the material data
41material = loadSiMaterial(data_dir);
42
43% Define wavelengths
44wl_count = 11;
45wavelengths = linspace(266, 800, wl_count);
46
47% Define the optimization domain
48optimization_domain = struct('name', {'h', 'width', 'swa', 'radius'}, ...
49 'type', {'continuous', 'continuous', 'continuous', 'continuous'}, ...
50 'domain', {[50, 60], [25, 35], [84, 90], [6, 8]});
51
52% Load target parameters and values
53[target_keys, target_vector, uncertainty_vector] = loadTargetData(data_dir);
54
55% Set up the study
56server = jcmoptimizer.Server("server_location", "local");
57client = jcmoptimizer.Client('host', server.host);
58
59study = client.create_study( ...
60 'design_space', optimization_domain, ...
61 'driver','BayesianLeastSquares',...
62 'study_name','Ellipsometry reconstruction example',...
63 'study_id', 'ellipsometry_reconstruction', ...
64 'save_dir', '.');
65
66
67% Define the objective function
68objective = @(study, sample) objectiveFunction(study, sample, keys, optimization_domain, project_file, wavelengths, material);
69
70% Set study parameters
71study.configure('target_vector', target_vector', ...
72 'uncertainty_vector', uncertainty_vector', ...
73 'max_iter', NUM_ITERATIONS);
74
75fprintf('\n\n');
76fprintf('The target parameter to be reconstructed is\n');
77for i = 1:length(optimization_domain)
78 parameter_name = optimization_domain(i).name;
79 fprintf('\t%s: %g\n', parameter_name, target_keys.Value(i));
80end
81fprintf('\n');
82
83if keys.derivative_order > 0
84 fprintf('Derivative information of the FEM solver is being used\n');
85else
86 fprintf('Derivative information of the FEM solver is not being used\n');
87end
88fprintf('\n');
89
90% Run the minimization
91study.set_evaluator(objective);
92study.run();
93
94% Plot the reconstruction results and compare to target
95plotReconstructionResults(study, target_vector, uncertainty_vector, wavelengths, '.', project_file, keys, material);
96
97% Helper functions
98
99function result = mergeStructs(struct1, struct2)
100 % Merge two structures into one
101 result = struct1;
102 fields = fieldnames(struct2);
103 for i = 1:numel(fields)
104 result.(fields{i}) = struct2.(fields{i});
105 end
106end
107
108function material = loadSiMaterial(data_dir)
109 material_file = fullfile(data_dir, 'Aspnes.csv');
110
111 % Read the material file
112 n_data = readtable(material_file, 'Range', 'A2:B47');
113 k_data = readtable(material_file, 'Range', 'A49:B94');
114
115 % Create interpolators for material data
116 material.n_interpolator = griddedInterpolant(n_data{:, 1} * 1e3, n_data{:, 2}, 'linear');
117 material.k_interpolator = griddedInterpolant(k_data{:, 1} * 1e3, k_data{:, 2}, 'linear');
118end
119
120function nk = get_nk(material, wavelength)
121 nk = material.n_interpolator(wavelength) + 1i * material.k_interpolator(wavelength);
122end
123
124function [mueller_matrices, mueller_matrices_derivatives] = solve_forward_problem(project_file, wavelengths, material, keys)
125 job_ids = [];
126
127 for wavelength = wavelengths
128 % Get a local copy of the keys for modifying and dispatching
129 inner_keys = keys;
130 inner_keys.vacuum_wavelength = wavelength * 1e-9;
131 inner_keys.n3 = get_nk(material, wavelength);
132 job_id = jcmwave_solve(project_file, inner_keys, 'temporary', 'yes');
133 job_ids = [job_ids, job_id];
134 end
135
136 % Here we wait until all jobs are finished
137 [results, logs] = jcmwave_daemon_wait(job_ids); % Assuming a similar function exists
138
139 % Initialize mueller_matrices
140 mueller_matrices = zeros(length(wavelengths), 4, 4);
141
142 % Iterate over all results
143 for idx = 1:length(results)
144 result = results{idx};
145 % First get the Mueller matrix
146 M = result{3}.Mueller_ps; % Corrected indexing
147
148 % Extract the 4x4 matrix from the cell array
149 M_matrix = M{1};
150 mueller_matrices(idx, :, :) = M_matrix;
151 end
152
153 % Initialize derivatives
154 mueller_matrices_derivatives = struct();
155 param_names = {'h', 'width', 'swa', 'radius'};
156
157 for p = 1:length(param_names)
158 param = param_names{p};
159 derivative_key = ['d_', param];
160 param_derivative = zeros(length(wavelengths), 4, 4);
161
162 for idx = 1:length(results)
163 result = results{idx};
164
165 if isfield(result{3}, derivative_key)
166 dM = result{3}.(derivative_key).Mueller_ps{1};
167 param_derivative(idx, :, :) = dM;
168 end
169 end
170
171 mueller_matrices_derivatives.(param) = param_derivative;
172 end
173end
174
175function [target_keys, target_vector, uncertainty_vector] = loadTargetData(data_dir)
176 % Load target parameters
177 target_keys_table = readtable(fullfile(data_dir, 'target_parameters.csv'));
178 target_keys = table2struct(target_keys_table, 'ToScalar', true);
179
180 % Load target values
181 target_values = readtable(fullfile(data_dir, 'target_values.csv'));
182 target_vector = target_values.target_vector;
183 uncertainty_vector = target_values.uncertainty_vector;
184end
185
186
187function observation = objectiveFunction(study, sample, keys, optimization_domain, project_file, wavelengths, material)
188 % Merge the new parameters with the existing ones
189 objective_keys = mergeStructs(keys, sample);
190
191 % Solve the forward problem
192 [mueller_matrix, mueller_matrix_derivatives] = solve_forward_problem(project_file, wavelengths, material, objective_keys);
193
194 % Create a new observation
195 observation = study.new_observation();
196
197 % Add the Mueller matrix
198 flat_mueller_matrix = flatten_C_style(mueller_matrix);
199 observation.add(flat_mueller_matrix');
200
201 % Add derivatives if available
202 if objective_keys.derivative_order > 0
203 for p = 1:length(optimization_domain)
204 parameter = optimization_domain(p);
205 if strcmp(parameter.type, 'continuous')
206 derivative_value = flatten_C_style(mueller_matrix_derivatives.(parameter.name));
207 observation.add(derivative_value', 'derivative', parameter.name);
208 end
209 end
210 end
211end
212
213function plotReconstructionResults(study, target_vector, uncertainty_vector, wavelengths, optimization_dir, project_file, keys, material)
214 % Reshape target and uncertainty vectors
215 target_matrix = reshape(target_vector, 4, 4, length(wavelengths));
216
217 % Get the minimum parameters
218 study_info = study.info();
219 min_params = study_info.min_params;
220 keys = mergeStructs(keys, min_params);
221
222 % Generate reconstruction data for comparison
223 [reconstructed_mueller_matrix, ~] = solve_forward_problem(project_file, wavelengths, material, keys);
224
225 flat_reconstructed_mueller_matrix = flatten_C_style(reconstructed_mueller_matrix)';
226 reconstructed_mueller_matrix = reshape(flat_reconstructed_mueller_matrix, 4, 4, length(wavelengths));
227
228 % Plot the results
229 fig = figure;
230 tiledlayout(4, 4, 'TileSpacing', 'Compact');
231 sgtitle('Mueller matrix entries');
232
233 for i = 1:4
234 for j = 1:4
235 nexttile;
236 plot(wavelengths, squeeze(target_matrix(j, i, :)), 'DisplayName', 'Target');
237 hold on;
238 plot(wavelengths, squeeze(reconstructed_mueller_matrix(j, i, :)), 'DisplayName', 'Reconstructed');
239 hold off;
240 title(['M' num2str(i) num2str(j)]);
241 if i == 4
242 xlabel('Wavelength (nm)');
243 end
244 end
245 end
246
247 legend('show');
248 saveas(fig, fullfile(optimization_dir, 'reconstruction.png'));
249end
250
251% The target dataset was created using numpy, which uses a different array flattening
252% scheme. Hence, we have to account for this and flatten arrays in matlab in C style as
253% opposed to F style.
254function flattened_C_style_list = flatten_C_style(matrices)
255 % Initialize an empty array to store the flattened results
256 flattened_C_style_list = [];
257
258 % Get the size of the first dimension
259 num_matrices = size(matrices, 1);
260 % Iterate over each slice of the 3D array
261 for k = 1:num_matrices
262 matrix = squeeze(matrices(k, :, :));
263 % Transpose the matrix to switch row-major to column-major
264 matrix_T = matrix';
265 % Flatten the transposed matrix and concatenate
266 flattened_C_style_list = [flattened_C_style_list; matrix_T(:)];
267 end
268end
The constants at the beginning of the script control the number of reconstruction
iterations, whether derivative information is requested from the FEM model, the FEM
degree, and the number of parallel JCMsolve jobs.
9% Parameters to play with
10NUM_ITERATIONS = 20;
11DERIVATIVE_ORDER = 1;
12FEM_DEGREE = 3;
13MULTIPLICITY = 1; % max of 1 for demo version
Before the study is created, the script registers the local machine with the JCMsuite
daemon. This allows the optimizer to submit the FEM evaluations through the usual
JCMsolve job infrastructure.
16%% Register a new computer resource
17jcmwave_daemon_add_workstation('Hostname', 'localhost', ...
18 'Multiplicity', MULTIPLICITY, ...
19 'NThreads', 2);
The script defines the fixed simulation keys, loads the material data, creates the wavelength grid, and specifies the four continuous parameters that should be reconstructed.
47% Define the optimization domain
48optimization_domain = struct('name', {'h', 'width', 'swa', 'radius'}, ...
49 'type', {'continuous', 'continuous', 'continuous', 'continuous'}, ...
50 'domain', {[50, 60], [25, 35], [84, 90], [6, 8]});
The target parameters, target vector, and uncertainty vector are loaded from the example
data. The target vector contains the Mueller matrix entries, while the matching
uncertainty_vector assigns one uncertainty value to each component of the target
vector.
52% Load target parameters and values
53[target_keys, target_vector, uncertainty_vector] = loadTargetData(data_dir);
The optimizer is started through a local Server and Client. The study uses the
BayesianLeastSquares driver because the reconstruction compares a vector-valued model
response with a vector-valued target measurement.
55% Set up the study
56server = jcmoptimizer.Server("server_location", "local");
57client = jcmoptimizer.Client('host', server.host);
58
59study = client.create_study( ...
60 'design_space', optimization_domain, ...
61 'driver','BayesianLeastSquares',...
62 'study_name','Ellipsometry reconstruction example',...
63 'study_id', 'ellipsometry_reconstruction', ...
64 'save_dir', '.');
The objective function receives one candidate parameter set from the study, updates the
JCMsuite project keys, and solves the forward problem. It returns an observation
containing the flattened Mueller matrix. When derivatives are enabled, the corresponding
parameter derivatives are added to the same observation.
187function observation = objectiveFunction(study, sample, keys, optimization_domain, project_file, wavelengths, material)
188 % Merge the new parameters with the existing ones
189 objective_keys = mergeStructs(keys, sample);
190
191 % Solve the forward problem
192 [mueller_matrix, mueller_matrix_derivatives] = solve_forward_problem(project_file, wavelengths, material, objective_keys);
193
194 % Create a new observation
195 observation = study.new_observation();
196
197 % Add the Mueller matrix
198 flat_mueller_matrix = flatten_C_style(mueller_matrix);
199 observation.add(flat_mueller_matrix');
200
201 % Add derivatives if available
202 if objective_keys.derivative_order > 0
203 for p = 1:length(optimization_domain)
204 parameter = optimization_domain(p);
205 if strcmp(parameter.type, 'continuous')
206 derivative_value = flatten_C_style(mueller_matrix_derivatives.(parameter.name));
207 observation.add(derivative_value', 'derivative', parameter.name);
208 end
209 end
210 end
211end
Finally, the target vector, uncertainty vector, and iteration limit are passed to the
study. After the objective has been registered as evaluator, study.run() performs the
reconstruction loop.
67% Define the objective function
68objective = @(study, sample) objectiveFunction(study, sample, keys, optimization_domain, project_file, wavelengths, material);
69
70% Set study parameters
71study.configure('target_vector', target_vector', ...
72 'uncertainty_vector', uncertainty_vector', ...
73 'max_iter', NUM_ITERATIONS);
90% Run the minimization
91study.set_evaluator(objective);
92study.run();
After the minimization, the script reads the best sample from the study information and
inserts the reconstructed parameters into the JCMsuite keys. The forward problem is then
solved once more for comparison with the target data.
217 % Get the minimum parameters
218 study_info = study.info();
219 min_params = study_info.min_params;
220 keys = mergeStructs(keys, min_params);
221
222 % Generate reconstruction data for comparison
223 [reconstructed_mueller_matrix, ~] = solve_forward_problem(project_file, wavelengths, material, keys);
224
225 flat_reconstructed_mueller_matrix = flatten_C_style(reconstructed_mueller_matrix)';
226 reconstructed_mueller_matrix = reshape(flat_reconstructed_mueller_matrix, 4, 4, length(wavelengths));
The target dataset was created using Python/NumPy, which stores the flattened arrays
in a different ordering than Matlab. The helper function flatten_C_style therefore
converts the simulated Mueller matrices to the same ordering before they are passed to the
optimizer or plotted.
251% The target dataset was created using numpy, which uses a different array flattening
252% scheme. Hence, we have to account for this and flatten arrays in matlab in C style as
253% opposed to F style.
254function flattened_C_style_list = flatten_C_style(matrices)
255 % Initialize an empty array to store the flattened results
256 flattened_C_style_list = [];
257
258 % Get the size of the first dimension
259 num_matrices = size(matrices, 1);
260 % Iterate over each slice of the 3D array
261 for k = 1:num_matrices
262 matrix = squeeze(matrices(k, :, :));
263 % Transpose the matrix to switch row-major to column-major
264 matrix_T = matrix';
265 % Flatten the transposed matrix and concatenate
266 flattened_C_style_list = [flattened_C_style_list; matrix_T(:)];
267 end
268end
This particular reconstruction can typically be performed in very few iterations despite containing four different parameters, each with a flat prior.
The parameter reconstruction reaches
values close to 1 after only a few iterations.¶
After 20 iterations the Mueller matrix values have been sufficiently reconstructed.
After 20 iterations the reconstructed Mueller matrix entries are indistinguishable from the target vector.¶