-
Notifications
You must be signed in to change notification settings - Fork 4
/
Simulation.m
387 lines (352 loc) · 17 KB
/
Simulation.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
classdef Simulation
% Base class for a 2-D or 3D wave simulation
% Ivo Vellekoop & Gerwin Osnabrugge 2016-2020
%
% Data storage:
% Internally, all data is stored as 2-D or 3-D arrays. For vector simulations,
% the data for each polarization components is stored as a separate
% array in a 1x3 cell array.
%
% The size of the simulation grid is determined by the refractive index
% distribution that is passed in the constructor (see Medium.m).
% The grid size includes the region of interest (ROI), boundaries, and
% padding for efficient application of the fft algorithm. In the end,
% only the field within the ROI is returned by exec(), although it is
% possible to recover the full field including boundaries and padding
% through the 'state' variable that is an optional return argument.
%
properties
sample; % a medium object containing the map of the relative
% dielectric constant including the boundary layers
%options:
roi = []; % Part of the simulated field that is returned as output,
% this matrix contains 2 rows, one for the start index and one for
% the last index. It contains 4 columns (one for each dimension and
% one to indicate the polarization component, which is always 1 for
% scalar simulations)
% Defaults to the full medium. To save memory, a smaller
% output_roi can be specified.
lambda = 1; % Wavelength (in micrometers)
gpu_enabled; % flag to determine if simulation are run on the GPU
single_precision = true; % flag to determine if single precision or
% double precision calculations are used.
% Note that on a typical GPU, double
% precision calculations are about 10
% times as slow as single precision.
% Callback function options
% The simulation code calls a callback function every few
% iteratations. There are two callbacks already implemented:
% default_callback and abs_image_callback.
% you can choose which one to use to set the 'callback' option
% note that you can also specify your own custom callback function
callback = @Simulation.default_callback; % callback function that is called for showing the progress of the simulation. Default shows image of the absolute value of the field.
callback_options = struct; %other options for the callback
callback_interval = 50; % the callback is called every 'callback_interval' iterations.
% Stopping criteria
% To determine when to stop, the algorithm calculates the amount of
% energy added in the last iteration. The
% simulation is stopped when the total energy in this 'Ediff' is
% lower than a threshold value.
%
energy_threshold = 1E-2; % Threshold for terminating the simulation.
% The threshold is specified as a fraction
% of the amount of energy added during the
% first iteration of the algorithm. Therefore
% it is compensates for the
% amplitude of the source terms.
usemex = false; %Flag to decide if a vector simulation should be run entirely within mex.
energy_calculation_interval = 8; % only calculate energy difference every N steps (to reduce overhead)
max_cycles = inf; % Maximum number of wave periods to run the simulation.
% Note that the number of actual iterations per optical cycle
% depends on the algorithm and its parameters
% (notably the refractive index contrast).
% Therefore, it is not recommended to specify
% max_cycles explicitly.
%internal:
grid; % simgrid object
N; % size of simulation grid (in pixels)
x_range; %x-coordinates of roi
y_range; %y-coordinates of roi
z_range; %z-coordinates of roi
%internal:
iterations_per_cycle;%must be set by derived class
end
methods
function obj = Simulation(refractive_index, options)
%% Constructs a simulation object
% sample = SampleMedium object
% options = simulation options.
% options.max_cycles = maximum number of optical cycles for
% which to run the simulation. Note that the number of
% required iterations per cycle depends on the algorithm and
% its parameters
% copy values from 'options' to 'obj' (only if the field is present in obj)
fs = intersect(fields(obj), fields(options));
for f=1:length(fs)
fieldname = fs{f};
obj.(fieldname) = options.(fieldname);
end
% if gpu_enabled not specified, then check for gpu to use for
% computations
if ~isfield(options,'gpu_enabled')
try
if gpuDeviceCount > 0 % check for gpu
obj.gpu_enabled = true;
gpu = gpuDevice;
disp(['GPU found. Performing simulations on: ',gpu.Name]);
end
catch %
obj.gpu_enabled = false;
end
end
% generate Medium object and set corresponding grid
[obj.sample, obj.grid] = Medium(refractive_index, options);
% set grid and number of grid points
obj.N = [obj.grid.N, 1]; %vector simulations set last parameter to 3
% set region of interest
% by default the full field is returned (excluding boundaries)
default_roi = [obj.sample.Bl + 1; obj.grid.N - obj.sample.Br];
if isempty(obj.roi)
obj.roi = [Simulation.make4(default_roi(1,:)); Simulation.make4(default_roi(2,:))];
else % use specified region of interest
obj.roi = [Simulation.make4(obj.roi(1,:)); Simulation.make4(obj.roi(2,:))];
obj.roi(:,1:3) = obj.roi(:,1:3) + default_roi(1,:) - 1; % shift xyz to match grid coordinates
end
% calculate roi coordinates
obj.x_range = obj.grid.x_range(obj.roi(1,2):obj.roi(2,2));
obj.x_range = obj.x_range - obj.x_range(1);
obj.y_range = obj.grid.y_range(obj.roi(1,1):obj.roi(2,1));
obj.y_range = obj.y_range - obj.y_range(1);
obj.z_range = obj.grid.z_range(obj.roi(1,3):obj.roi(2,3));
obj.z_range = obj.z_range - obj.z_range(1);
%update the energy at least every callback
obj.energy_calculation_interval = min(obj.energy_calculation_interval, obj.callback_interval);
end
function [E, state] = exec(obj, source)
% EXEC Executes the simulation with the given source
% source is a 'source' object (see documentation for source)
%
tic;
% convert the source to the correct data type (single, double,
% gpuarray or not).
% shift source so that [1,1,1] corresponds to start of roi
% then crops source so that anything outside the simulation
% grid is removed. Note: it is allowed to put a
% source energy outside of the roi, but inside the grid (that
% is, inside the absorbing boundary).
%
if (~isa(source, 'Source'))
error('Expecting a Source object as input parameter');
end
state.source = obj.data_array(source)...
.shift(obj.roi(1,:))...
.crop([1,1,1,1; obj.N]);
state.source_energy = sum(state.source.energy);
if state.source_energy == 0
warning('There is no source inside the grid boundaries, expect to get zero field everywhere.');
return;
end
%%% prepare state (contains all data that is unique to a single
% run of the simulation)
%
state.it = 1; %iteration
state.max_iterations = ceil(obj.max_cycles * obj.iterations_per_cycle);
state.diff_energy = [];
state.last_step_energy = inf;
state.calculate_energy = true;
state.has_next = true;
%%% Execute simulation
% the run_algorithm function should:
% - update state.last_step_energy when required
% - call next(obj, state)
% - return calculated field in state.E
%(see wavesim for an example)
state = run_algorithm(obj, state);
state.time=toc;
if state.converged
disp(['Reached steady state in ' num2str(state.it) ' iterations']);
else
disp('Did not reach steady state');
end
disp(['Time consumption: ' num2str(state.time) ' s']);
E = gather(state.E); % convert gpuArray back to normal array
end
%% Continue to the next iteration. Returns false to indicate that the simulation has terminated
function state = next(obj, state, can_terminate)
%% store energy (relative to total energy in source)
state.diff_energy(state.it) = state.last_step_energy;
%% check if simulation should terminate
if can_terminate && state.diff_energy(state.it) / state.diff_energy(1) < obj.energy_threshold
%if can_terminate && state.diff_energy(state.it) / state.source_energy < obj.energy_threshold
state.has_next = false;
state.converged = true;
elseif can_terminate && state.it >= state.max_iterations
state.has_next = false;
state.converged = false;
else
state.has_next = true;
end
%% do we need to calculate the last added energy? (only do this once in a while, because of the overhead)
state.calculate_energy = mod(state.it - 1, obj.energy_calculation_interval)==0;
%% call callback function if neened
if (mod(state.it, obj.callback_interval)==0 || ~state.has_next) %now and then, call the callback function to give user feedback
obj.callback(obj, state);
end
state.it = state.it+1;
end
end
methods(Access = protected)
function d = data_array(obj, data, N)
% data_array(obj, data) - converts the data to an array of the correct format
% if gpu_enabled is true, the array is created on the gpu
% the data is converted to
% single/double precision based on the
% options set when creating the
% simulation object.
% data_array(obj, [], N)- creates an empty array (filled with zeros) of size N
% Check whether single precision and gpu computation options are enabled
if obj.single_precision
p = 'single';
else
p = 'double';
end
%obj.N
if isempty(data) %no data is specified, generate empty array of specified size
if obj.gpu_enabled
d = zeros(N, p, 'gpuArray');
else
d = zeros(N, p);
end
return;
end
d = cast(full(data), p);
if(~isreal(data))
d = complex(d);
end
if obj.gpu_enabled
d = gpuArray(d);
end
end
function Ecrop = crop_field(obj,E)
% Removes the boundary layer from the simulated field by
% cropping field dataset
Ecrop = E(obj.roi(1,1):obj.roi(2,1),...
obj.roi(1,2):obj.roi(2,2),...
obj.roi(1,3):obj.roi(2,3),...
obj.roi(1,4):obj.roi(2,4));
end
end
methods(Static)
function en = energy(E_x, roi)
% calculculate energy (absolute value squared) of E_x
% optionally specify roi to indicate which values are to be
% included
if nargin == 2
E_x = E_x(roi(1,1):roi(2,1), roi(1,2):roi(2,2), roi(1,3):roi(2,3), roi(1,4):roi(2,4));
end
en = full(gather(sum(abs(E_x(:)).^2)));
end
% callback functions
function default_callback(obj, state, dim, pol)
%default callback function. Shows total energy evolution and
%real value of field along specified dimension (longest
%dimension by default)
%
figure(1); clf;
energy = state.diff_energy(1:state.it);
threshold = obj.energy_threshold * state.source_energy;
%E = state.E;
E = state.dE;
E = max(max(E, [], 1), [], 1);
subplot(2,1,1); plot(1:length(energy),log10(energy),'b',[1,length(energy)],log10(threshold)*ones(1,2),'--r');
title(length(energy)); xlabel('# iterations'); ylabel('log_{10}(energy added)');
% plot midline along longest dimension
xpos = ceil(size(E, 2)/2);
ypos = ceil(size(E, 1)/2);
zpos = ceil(size(E, 3)/2);
if nargin < 3
[~,dim] = max([ypos,xpos,zpos]);
end
if nargin < 4 % default show horizontal polarization
pol = 1;
end
switch dim
case 1 % y-dimension
sig = log(abs(E(:, xpos, zpos,pol)));
ax = obj.grid.y_range(:);
sroi = ax(obj.roi(:,1));
ax_label = 'y (\mum)';
case 2 % x-dimension
sig = log(abs(E(ypos, :, zpos,pol)));
ax = obj.grid.x_range(:);
sroi = ax(obj.roi(:,2));
ax_label = 'x (\mum)';
case 3 % z-dimension
sig = log(abs(E(ypos, xpos, :,pol)));
ax = obj.grid.z_range(:);
sroi = ax(obj.roi(:,3));
ax_label = 'z (\mum)';
end
subplot(2,1,2);
plot(ax, squeeze(sig),'b'); hold on;
title('midline cross-section')
xlabel(ax_label); ylabel('log(|E|)');
% draw dashed lines to indicate start of boundary layer
ca = gca;
plot(sroi(1)*ones(1,2), [ca.YLim(1),ca.YLim(2)],'--k', ...
sroi(end)*ones(1,2), [ca.YLim(1),ca.YLim(2)],'--k');
%disp(['Added energy ', num2str(energy(end))]);
drawnow;
end
function abs_crossimage_callback(obj, state)
% callback function that displays the intensity along the
% propagation direction
%
figure(1);
if size(state.E,3) == 1 % 1D/2D simulation
Eprop = abs(squeeze(state.E(:,:,1,1)));
else
Eprop = abs(squeeze(state.E(ceil(end/2),:,:,1)));
end
imagesc(Eprop);
axis image;
title(['Differential energy ' num2str(state.diff_energy(state.it)/state.diff_energy(1))]);
drawnow;
end
%skip the callback function, for benchmarking speed
function no_callback(obj, state)
end
function sz = make3(sz, pad)
% makes sure sz is a row vector with exactly 3 elements
% (pads when needed)
% (this function is needed because 'size' by default removes
% trailing singleton dimensions)
sz = sz(:).'; %make row vector
if numel(sz) < 3
sz((end+1):3) = pad;
end
end
function sz = make4(sz)
% converts the vector sz to a 4-element vector by appending 1's
% when needed. This function is useful since 'size' removes
% trailing singleton dimensions of arrays, so a 100x100x1x1
% array returns a size of [100, 100], whereas a 100x100x1x3
% array retuns a size of [100, 100, 1, 3]
% As a workaround for this inconsistency, we always use
% 4-element size vectors.
sz = sz(:).';
if numel(sz) < 4
sz((end+1):4) = 1;
else
if numel(sz) > 4
error('Position and size vectors can be 4-dimensional at most');
end
end
end
% e.g. call Simulation.use('utilities') to add the 'utilities' folder
% to the MATLAB path
function use(subpath)
addpath([fileparts(mfilename('fullpath')) '\' subpath]); %todo: put in more sensible location
end
end
end