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 \vec{\lambda}. These measurements are arranged in a target vector \vec{t}. 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 \vec{t}, we denote the measurement uncertainty vector as \vec{\eta}. 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 \vec{t} 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 \vec{y} using the FEM model for the same set of incident wavelengths \vec{\lambda} 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 \vec{y} with the same ordering as the target vector \vec{t}, 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.

_images/progress.png

The parameter reconstruction reaches \chi^2 values close to 1 after only a few iterations.

After 20 iterations the Mueller matrix values have been sufficiently reconstructed.

_images/reconstruction.png

After 20 iterations the reconstructed Mueller matrix entries are indistinguishable from the target vector.