-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpbe_solver.m
357 lines (315 loc) · 14.4 KB
/
pbe_solver.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
classdef pbe_solver < handle
% Base solver
properties
props
options
end
properties
prevStates
end
methods
function obj = pbe_solver(options, props)
% When props has more than one elements, it represents a
% polymorphism system.
obj.options = options;
obj.props = props;
end
end
methods
function obj = set_initial_states(obj, states)
obj.prevStates = states;
end
function nextState = step(obj, currentState, inputs, tSpan)
%% Derive next state
% currentState: structure:
% conc: double scalar
% csd: double matrix
% moment3: double (optional)
% when the currentState is passed as an array, only the first
% concentration is respected as the initial concentration. The
% element number should match the length of the props. They are
% treated as different polymorphism so that the concentration
% should be shared.
%
% When the currentState is [], the stored `prevStates` is used
% and updated for the purpose of stateful solver.
%
% inputs: structure:
% tC: double - temperature in degC
% inletCsd: double - inlet CSD
% residenceTime: double
% inletConc: double
%% Initialization
% determine whether the solver is currently stateful or
% stateless.
if isempty(currentState)
s = obj.prevStates;
stateful = true;
else
s = currentState;
stateful = false;
end
properties = obj.props;
nProps = numel(properties);
szProps = size(properties);
assert(numel(s) == nProps);
tNow = 0;
timeStepScale = obj.options.timeStepScale;
isMSMPR = obj.options.isMSMPR;
if isMSMPR
% Populate continuous inputs
resTime = inputs.resTime;
inConc = inputs.inConc;
inCSDs = inputs.inCSDs;
residenceTimeStepScale = obj.options.residenceTimeStepScale;
end
sizeGrids = [properties.sizeGrids];
lStep = sizeGrids.interval();
lGrids = cell(1, nProps);
for i = 1 : nProps
lGrids{i} = sizeGrids(i).to_array();
end
kShapes = [properties.kShape];
cKsDR = kShapes .* [properties.densityRatio] / 1e18;
x = cell(size(properties));
for i = 1 : nProps
x{i} = s(i).csd;
end
c = s(1).conc; % only first concentration is used.
m3 = [s.moment3];
% Compute solubility
cStars = zeros(szProps);
for i = 1 : nProps
cStars(i) = properties(i).solubility(inputs.tC);
end
% Loop variables initialization
svar = struct();
GD = cell(szProps);
Bs = zeros(szProps);
Bp = zeros(szProps);
massRate = zeros(szProps);
newM3 = zeros(szProps);
%% Locate the current in-use size range and Step-artifact prevention
% This optimization should help reducing the computational load
% by excluding the unused channels in integration.
% When continuous crystallization is involved, the effective
% CSD is the union of the state CSD and the inlet CSD.
optUseSubCSD = obj.options.useSubCSD;
effectiveCSDs = x;
effectiveLGrids = lGrids;
if optUseSubCSD
rightSizeCaps = zeros(szProps);
rPositions = zeros(szProps);
for i = 1 : nProps
mark = find(x{i} > eps , 1, 'last');
if isempty(mark)
mark = 1; % first channel must be zero (nucleation point)
end
if isMSMPR
inCSDMark = find(inCSDs{i} > eps , 1, 'last');
if isempty(inCSDMark)
inCSDMark = 1;
end
mark = max(mark, inCSDMark);
end
rightSizeCaps(i) = lGrids{i}(mark);
% Find the first channel that is above the current cap.
% rPositions(i) = find(lGrids{i}>=cap, 1);
rPositions(i) = mark;
% Because each step the growth/dissolution cannot
% propagate more than one channel, leaving one spare is
% enough.
if rPositions(i) == numel(lGrids{i})
idx = 1:rPositions(i);
else
idx = 1:rPositions(i)+1;
end
effectiveLGrids{i} = lGrids{i}(idx);
effectiveCSDs{i} = x{i}(idx);
end
end
%% Loop
while tNow < tSpan
vf = m3 / 1e18 .* kShapes;
sigma = c ./ cStars - 1;
for i = 1 : nProps
svar.tC = inputs.tC;
svar.vf = vf(i);
svar.sigma = sigma(i);
svar.lGrids = effectiveLGrids{i};
% Kinetics
[GD{i}, Bs(i), Bp(i)] = properties(i).kinetics(svar);
end
% early stop
if obj.check_early_stop(tNow, tSpan, GD, sigma)
break;
end
% calculate time step with the predicted change of
% 1. concentration change (should not bypass the
% solubility line)
% 2. CSD change (should respect stability condition)
% TODO: get_time_step slot
% TODO: when no crystal and dissolving, remove the time
% step limit on PBE.
for i = 1 : nProps
massRate(i) = particle_moment(lStep(i), effectiveCSDs{i} .* GD{i}, 2) .* cKsDR(i);
end
massStepLimit = abs((c - cStars) ./ massRate);
csdTimeLimits = zeros(size(GD));
% TODO size_dependent_mismatch: when disabling SubCSD optimization, and if GD is
% increasing over size, the time step of unused channels
% will result in smaller time steps and affects the result
% consistency between SubCSD ON and SubCSD OFF.
for i = 1 : numel(GD)
if optUseSubCSD
noParticle = rPositions(i) == 1;
else
noParticle = all(effectiveCSDs{i}==0);
end
if all(GD{i} == 0) || (noParticle && GD{i}(1) < 0)
% GD is not useful when no growth/dissolution
% or when there is no crystal and it is
% dissolving.
csdTimeLimits(i) = inf;
else
csdTimeLimits(i) = min(abs(lStep ./ GD{i}));
end
end
csdTimeLimit = min(csdTimeLimits);
% TODO: for MSMPR, the time step and tau should be related
if isMSMPR
resTimeStep = resTime * residenceTimeStepScale;
timeLimits = [massStepLimit csdTimeLimit resTimeStep];
else
timeLimits = [massStepLimit csdTimeLimit];
end
tStep = min(timeLimits) * timeStepScale;
tNow = tNow + tStep;
if tNow > tSpan
tNow = tSpan;
tStep = tSpan - tNow;
end
for i = 1 : nProps
if isMSMPR
inCSD = inCSDs{i};
n = effectiveCSDs{i};
if inCSD == 0
% Inlet is clear.
effectiveCSDs{i} = n + tStep * (0 - n)/resTime;
else
% Inlet contains crystals.
if optUseSubCSD
% inCSD may have different effective CSD range.
% In this branch, the current CSD and the
% effective CSD range should be merged.
nIn = inCSD(1: numel(n));
else
% Assume that the inlet CSD should always be
% the same size as the effective CSD.
nIn = inCSD;
end
effectiveCSDs{i} = n + tStep * (nIn - n)/resTime;
end
end
if optUseSubCSD
gd = GD{i};
% Determine next CSD right end location
rSzCap = rightSizeCaps(i);
if isscalar(gd)
rightSizeCaps(i) = rSzCap + gd * tStep;
else
gdInterp = interp1(effectiveLGrids{i}, gd, rSzCap);
rightSizeCaps(i) = rSzCap + gdInterp * tStep;
end
% After update, the right size cap may enter next
% channel. That's why one extra channel is included in
% the above code. If the size cap does not move to next
% channel, the extra calculated channel should be
% discarded.
oldPosition = rPositions(i);
newPosition = find(lGrids{i}>=rightSizeCaps(i), 1);
if isempty(newPosition)
% All channels are overflown
newPosition = numel(lGrids{i});
end
effCSD = effectiveCSDs{i};
% Calculate next CSD
effCSD = obj.step_csd(effCSD, Bp(i), Bs(i), gd, tStep, lStep(i));
if newPosition == oldPosition
% Growing, dissolving, anything, but not
% entering other channel. oldPosition is the
% previous right side position. We use it to
% drop the right extra channel.
if numel(effCSD) < numel(lGrids{i})
effCSD(end) = 0;
end
effectiveCSDs{i} = effCSD;
elseif newPosition == oldPosition - 1
% Dissolution-induced mismatch. Drop the old
% channel
effectiveCSDs{i} = effCSD(1:newPosition);
% Update the right (old) position & effective LGrids
rPositions(i) = newPosition;
idx = 1:(newPosition+1);
effectiveLGrids{i} = lGrids{i}(idx);
elseif newPosition == oldPosition + 1
% Growth-induced mismatch, use full channels
effectiveCSDs{i} = [effCSD; 0];
% Update the right (old) position & effective LGrids
rPositions(i) = newPosition;
if newPosition == numel(lGrids{i})
idx = 1:(newPosition);
else
idx = 1:(newPosition+1);
end
effectiveLGrids{i} = lGrids{i}(idx);
else
error('Unexpected branch reached.')
end
else
effectiveCSDs{i} = obj.step_csd(effectiveCSDs{i}, Bp(i), Bs(i), GD{i}, tStep, lStep(i));
end
% Derive mass change
newM3(i) = particle_moment(lStep(i), effectiveCSDs{i}, 3);
end
deltaMass = (newM3 - m3) .* cKsDR;
m3 = newM3;
c = c - sum(deltaMass);
if isMSMPR
% TODO use higher order solver scheme.
c = c + tStep * (inConc - c) / resTime;
end
end
% TODO this order affects code generation
nextState = repmat(struct('conc', 0, 'moment3', 0, 'csd', zeros(size(x{1}))), 1, nProps);
for i = 1 : nProps
effCSD = effectiveCSDs{i};
csd = zeros(size(lGrids{i}));
csd(1:numel(effCSD)) = effCSD;
nextState(i).csd = csd;
nextState(i).moment3 = m3(i);
nextState(i).conc = c;
end
if stateful
obj.prevStates = nextState;
end
end
end
methods (Access = protected)
function nextCSD = step_csd(obj, x, Bp, Bs, GD, tStep, lStep)
%% Derive next CSD based on current conditions
end
function shouldEarlyStop = check_early_stop(obj, tNow, tSpan, GD, sigma)
%% Check whether current condition should trigger early stop
% tNow: cuurrent time
% tSpan: current solution time span
% GD: growth or dissolution rates
% sigma: supersaturations
GDs = zeros(size(GD));
for i = 1 : numel(GD)
GDs(i) = max(abs(GD{i}));
end
shouldEarlyStop = max(GDs) < obj.options.earlyStopThreshold;
end
end
end