1 /*-------------------------------------------------------------------
2 Copyright 2011 Ravishankar Sundararaman, Kendra Letchworth Weaver, Deniz Gunceler
3
4 This file is part of JDFTx.
5
6 JDFTx is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 JDFTx is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with JDFTx. If not, see <http://www.gnu.org/licenses/>.
18 -------------------------------------------------------------------*/
19
20 #include <electronic/Everything.h>
21 #include <fluid/NonlinearPCM.h>
22 #include <fluid/LinearPCM.h>
23 #include <fluid/PCM_internal.h>
24 #include <core/ScalarFieldIO.h>
25 #include <core/Util.h>
26
27 //Utility functions to extract/set the members of a MuEps
getMuPlus(ScalarFieldMuEps & X)28 inline ScalarField& getMuPlus(ScalarFieldMuEps& X) { return X[0]; }
getMuPlus(const ScalarFieldMuEps & X)29 inline const ScalarField& getMuPlus(const ScalarFieldMuEps& X) { return X[0]; }
getMuMinus(ScalarFieldMuEps & X)30 inline ScalarField& getMuMinus(ScalarFieldMuEps& X) { return X[1]; }
getMuMinus(const ScalarFieldMuEps & X)31 inline const ScalarField& getMuMinus(const ScalarFieldMuEps& X) { return X[1]; }
getEps(ScalarFieldMuEps & X)32 inline VectorField getEps(ScalarFieldMuEps& X) { return VectorField(&X[2]); }
getEps(const ScalarFieldMuEps & X)33 inline const VectorField getEps(const ScalarFieldMuEps& X) { return VectorField(&X[2]); }
setMuEps(ScalarFieldMuEps & mueps,ScalarField muPlus,ScalarField muMinus,VectorField eps)34 inline void setMuEps(ScalarFieldMuEps& mueps, ScalarField muPlus, ScalarField muMinus, VectorField eps) { mueps[0]=muPlus; mueps[1]=muMinus; for(int k=0; k<3; k++) mueps[k+2]=eps[k]; }
35
36
setPreconditioner(double G,double kappaSqByEpsilon,double muByEpsSq)37 inline double setPreconditioner(double G, double kappaSqByEpsilon, double muByEpsSq)
38 { double G2 = G*G;
39 double den = G2 + kappaSqByEpsilon;
40 return den ? muByEpsSq*G2/(den*den) : 0.;
41 }
42
setMetric(int i,double Gsq,double qMetricSq,double * metric)43 inline void setMetric(int i, double Gsq, double qMetricSq, double* metric)
44 { metric[i] = qMetricSq ? Gsq / (qMetricSq + Gsq) : 1.;
45 }
46
NonlinearPCM(const Everything & e,const FluidSolverParams & fsp)47 NonlinearPCM::NonlinearPCM(const Everything& e, const FluidSolverParams& fsp)
48 : PCM(e, fsp), Pulay(fsp.scfParams), pMol(0.), ionNbulk(0.), ionZ(0.), screeningEval(0), dielectricEval(0)
49 {
50 const auto& solvent = fsp.solvents[0];
51 pMol = solvent->pMol ? solvent->pMol : solvent->molecule.getDipole().length();
52
53 //Initialize dielectric evaluation class:
54 dielectricEval = new NonlinearPCMeval::Dielectric(fsp.linearDielectric,
55 fsp.T, solvent->Nbulk, pMol, epsBulk, solvent->epsInf);
56
57 //Check and setup ionic screening:
58 if(fsp.cations.size() > 1) die("NonlinearPCM currently only supports a single cationic component.\n");
59 if(fsp.anions.size() > 1) die("NonlinearPCM currently only supports a single anionic component.\n");
60 assert(fsp.anions.size() == fsp.cations.size()); //this should be ensured by charge neutrality check in FluidSolver constructor
61 if(fsp.cations.size())
62 { //Ensure charge balanced:
63 if(fabs(fsp.cations[0]->molecule.getCharge() + fsp.anions[0]->molecule.getCharge())>1e-12)
64 die("NonlinearPCM currently only supports charge-balanced (Z:Z) electrolytes.\n");
65 ionNbulk = fsp.cations[0]->Nbulk;
66 ionZ = fsp.anions[0]->molecule.getCharge();
67 double VhsCation = fsp.cations[0]->molecule.getVhs(); if(!VhsCation) VhsCation = (4*M_PI/3)*pow(fsp.cations[0]->Rvdw,3);
68 double VhsAnion = fsp.anions[0]->molecule.getVhs(); if(!VhsAnion) VhsAnion = (4*M_PI/3)*pow(fsp.anions[0]->Rvdw,3);
69 screeningEval = new NonlinearPCMeval::Screening(fsp.linearScreening, fsp.T, ionNbulk, ionZ, VhsCation, VhsAnion, epsBulk);
70 }
71 else
72 { ionNbulk = 0.;
73 screeningEval = 0;
74 }
75
76 if(fsp.nonlinearSCF)
77 { //Initialize lookup tables for SCF version:
78 //--- Dielectric lookup table
79 double dxMapped = 1./512;
80 std::vector<double> samples;
81 for(double xMapped=0.; xMapped<=1.; xMapped+=dxMapped)
82 { double x;
83 if(xMapped==0.) x = 1e-12;
84 else if(xMapped==1.) x = 1e+12;
85 else x = xMapped/(1.-xMapped); //inverse of xMapped = x / (1 + x)
86 samples.push_back(dielectricEval->eps_from_x(x)/x);
87 }
88 gLookup.init(0, samples, dxMapped);
89 //--- Screening lookup table
90 if(screeningEval)
91 {
92 double dVmapped = 1./512;
93 std::vector<double> samples;
94 for(double Vmapped=-1.; Vmapped<=1.; Vmapped+=dVmapped)
95 { if(fabs(Vmapped)==1)
96 samples.push_back(0.);
97 else
98 { double V = std::pow(Vmapped/(1.-Vmapped*Vmapped), 3.); //inverse of Vmapped = copysign(2cbrt(V) / (1 + sqrt(1 + (2cbrt(V))^2)), V)
99 double x = screeningEval->x_from_V(V);
100 double xMapped = 1./(1.+x); //maps [0,infty) -> (0,1]
101 samples.push_back(xMapped);
102 }
103 }
104 xLookup.init(1, samples, dVmapped);
105 }
106 //--- Pulay metric
107 metric = std::make_shared<RealKernel>(gInfo);
108 applyFuncGsq(e.gInfo, setMetric, std::pow(fsp.scfParams.qMetric,2), metric->data());
109 }
110 else
111 { //Initialize preconditioner (for mu channel):
112 double muByEps = (ionZ/pMol) * (1.-dielectricEval->alpha/3); //relative scale between mu and eps
113 preconditioner.init(0, 0.02, gInfo.GmaxGrid, setPreconditioner, k2factor/epsBulk, muByEps*muByEps);
114 }
115 }
116
~NonlinearPCM()117 NonlinearPCM::~NonlinearPCM()
118 { delete dielectricEval;
119 if(screeningEval) delete screeningEval;
120 if(preconditioner) preconditioner.free();
121 if(gLookup) gLookup.free();
122 if(xLookup) xLookup.free();
123 }
124
125
set_internal(const ScalarFieldTilde & rhoExplicitTilde,const ScalarFieldTilde & nCavityTilde)126 void NonlinearPCM::set_internal(const ScalarFieldTilde& rhoExplicitTilde, const ScalarFieldTilde& nCavityTilde)
127 {
128 bool setPhiFromState = false; //whether to set linearPCM phi from state (first time in SCF version when state has been read in)
129
130 if(fsp.nonlinearSCF || (!state))
131 { if(!linearPCM)
132 { logSuspend();
133 linearPCM = std::make_shared<LinearPCM>(e, fsp);
134 logResume();
135 if(state) setPhiFromState = true; //set phi from state which has already been loaded (only in SCF mode)
136 }
137 logSuspend();
138 linearPCM->atpos = atpos;
139 linearPCM->set_internal(rhoExplicitTilde, nCavityTilde);
140 logResume();
141 }
142
143 //Initialize state if required:
144 if(!state)
145 { logPrintf("Initializing state of NonlinearPCM using a similar LinearPCM: "); logFlush();
146 FILE*& fpLog = ((MinimizeParams&)e.fluidMinParams).fpLog;
147 fpLog = nullLog; //disable iteration log from LinearPCM
148 linearPCM->minimizeFluid();
149 fpLog = globalLog; //restore usual iteration log
150 logFlush();
151 //Guess nonlinear states based on the electrostatic potential of the linear version:
152 //mu:
153 ScalarField mu;
154 if(screeningEval && screeningEval->linear)
155 { mu = (-ionZ/fsp.T) * I(linearPCM->state);
156 mu -= integral(mu)/gInfo.detR; //project out G=0
157 }
158 else initZero(mu, gInfo); //initialization logic does not work well with hard sphere limit
159 //eps:
160 VectorField eps = (-pMol/fsp.T) * I(gradient(linearPCM->state));
161 ScalarField E = sqrt(eps[0]*eps[0] + eps[1]*eps[1] + eps[2]*eps[2]);
162 ScalarField Ecomb = 0.5*((dielectricEval->alpha-3.) + E);
163 ScalarField epsByE = inv(E) * (Ecomb + sqrt(Ecomb*Ecomb + 3.*E));
164 eps *= epsByE; //enhancement due to correlations
165 //collect:
166 setMuEps(state, mu, clone(mu), eps);
167 }
168
169 this->rhoExplicitTilde = rhoExplicitTilde; zeroNyquist(this->rhoExplicitTilde);
170 this->nCavity = I(nCavityTilde + getFullCore());
171
172 updateCavity();
173
174 if(setPhiFromState)
175 { ScalarFieldMuEps gradUnused;
176 ScalarFieldTilde phiFluidTilde;
177 (*this)(state, gradUnused, &phiFluidTilde);
178 linearPCM->state = phiFluidTilde + coulomb(rhoExplicitTilde);
179 }
180 }
181
operator ()(const ScalarFieldMuEps & state,ScalarFieldMuEps & Adiel_state,ScalarFieldTilde * Adiel_rhoExplicitTilde,ScalarFieldTilde * Adiel_nCavityTilde,IonicGradient * forces,matrix3<> * Adiel_RRT) const182 double NonlinearPCM::operator()(const ScalarFieldMuEps& state, ScalarFieldMuEps& Adiel_state,
183 ScalarFieldTilde* Adiel_rhoExplicitTilde, ScalarFieldTilde* Adiel_nCavityTilde, IonicGradient* forces, matrix3<>* Adiel_RRT) const
184 {
185 EnergyComponents& Adiel = ((NonlinearPCM*)this)->Adiel;
186 ScalarFieldArray Adiel_shape; if(Adiel_nCavityTilde) nullToZero(Adiel_shape, gInfo, shape.size());
187
188 ScalarFieldTilde rhoFluidTilde;
189 ScalarField muPlus, muMinus, Adiel_muPlus, Adiel_muMinus;
190 initZero(Adiel_muPlus, gInfo); initZero(Adiel_muMinus, gInfo);
191 double mu0 = 0., Qexp = 0., Adiel_Qexp = 0.;
192 if(screeningEval)
193 { //Get neutrality Lagrange multiplier:
194 muPlus = getMuPlus(state);
195 muMinus = getMuMinus(state);
196 Qexp = integral(rhoExplicitTilde);
197 mu0 = screeningEval->neutralityConstraint(muPlus, muMinus, shape.back(), Qexp);
198 //Compute ionic free energy and bound charge
199 ScalarField Aout, rhoIon;
200 initZero(Aout, gInfo);
201 initZero(rhoIon, gInfo);
202 callPref(screeningEval->freeEnergy)(gInfo.nr, mu0, muPlus->dataPref(), muMinus->dataPref(), shape.back()->dataPref(),
203 rhoIon->dataPref(), Aout->dataPref(), Adiel_muPlus->dataPref(), Adiel_muMinus->dataPref(), Adiel_shape.size() ? Adiel_shape.back()->dataPref() : 0);
204 Adiel["Akappa"] = integral(Aout);
205 rhoFluidTilde += J(rhoIon); //include bound charge due to ions
206 }
207
208 //Compute the dielectric free energy and bound charge:
209 VectorField eps = getEps(state), p, Adiel_eps;
210 { ScalarField Aout;
211 initZero(Aout, gInfo);
212 nullToZero(p, gInfo);
213 nullToZero(Adiel_eps, gInfo);
214 callPref(dielectricEval->freeEnergy)(gInfo.nr, eps.const_dataPref(), shape[0]->dataPref(),
215 p.dataPref(), Aout->dataPref(), Adiel_eps.dataPref(), Adiel_shape.size() ? Adiel_shape[0]->dataPref() : 0);
216 Adiel["Aeps"] = integral(Aout);
217 rhoFluidTilde -= divergence(J(p)); //include bound charge due to dielectric
218 if(!Adiel_RRT) p = 0; //only need later for lattice derivative
219 } //scoped to automatically deallocate temporaries
220
221 //Compute the electrostatic terms:
222 ScalarFieldTilde phiFluidTilde = coulomb(rhoFluidTilde);
223 ScalarFieldTilde phiExplicitTilde = coulomb(rhoExplicitTilde);
224 Adiel["Coulomb"] = dot(rhoFluidTilde, O(0.5*phiFluidTilde + phiExplicitTilde));
225 if(Adiel_RRT)
226 *Adiel_RRT += matrix3<>(1,1,1)*(Adiel["Coulomb"] + Adiel["Aeps"] + Adiel["Akappa"])
227 + coulombStress(rhoFluidTilde, 0.5*rhoFluidTilde+rhoExplicitTilde);
228
229 if(screeningEval)
230 { //Propagate gradients from rhoIon to mu, shape
231 ScalarField Adiel_rhoIon = I(phiFluidTilde+phiExplicitTilde);
232 callPref(screeningEval->convertDerivative)(gInfo.nr, mu0, muPlus->dataPref(), muMinus->dataPref(), shape.back()->dataPref(),
233 Adiel_rhoIon->dataPref(), Adiel_muPlus->dataPref(), Adiel_muMinus->dataPref(), Adiel_shape.size() ? Adiel_shape.back()->dataPref() : 0);
234 //Propagate gradients from mu0 to mu, shape, Qexp:
235 double Adiel_mu0 = integral(Adiel_muPlus) + integral(Adiel_muMinus), mu0_Qexp;
236 ScalarField mu0_muPlus, mu0_muMinus, mu0_shape;
237 screeningEval->neutralityConstraint(muPlus, muMinus, shape.back(), Qexp, &mu0_muPlus, &mu0_muMinus, &mu0_shape, &mu0_Qexp);
238 Adiel_muPlus += Adiel_mu0 * mu0_muPlus;
239 Adiel_muMinus += Adiel_mu0 * mu0_muMinus;
240 if(Adiel_shape.size()) Adiel_shape.back() += Adiel_mu0 * mu0_shape;
241 Adiel_Qexp = Adiel_mu0 * mu0_Qexp;
242 }
243
244 //Propagate gradients from p to eps, shape
245 { VectorField Adiel_p = I(gradient(phiFluidTilde+phiExplicitTilde)); //Because dagger(-divergence) = gradient
246 callPref(dielectricEval->convertDerivative)(gInfo.nr, eps.const_dataPref(), shape[0]->dataPref(),
247 Adiel_p.const_dataPref(), Adiel_eps.dataPref(), Adiel_shape.size() ? Adiel_shape[0]->dataPref() : 0);
248 if(Adiel_RRT) *Adiel_RRT -= gInfo.dV * dotOuter(p, Adiel_p);
249 }
250
251 //Optional outputs:
252 if(Adiel_rhoExplicitTilde)
253 { if(fsp.nonlinearSCF && !useGummel())
254 { //Non-variational version (from inner solve):
255 *Adiel_rhoExplicitTilde = linearPCM->state - phiExplicitTilde;
256 }
257 else
258 { //Variational version (from bound charge with neutrality constraint):
259 *Adiel_rhoExplicitTilde = phiFluidTilde;
260 (*Adiel_rhoExplicitTilde)->setGzero(Adiel_Qexp);
261 }
262 }
263 if(Adiel_nCavityTilde)
264 { ScalarField Adiel_nCavity;
265 propagateCavityGradients(Adiel_shape, Adiel_nCavity, *Adiel_rhoExplicitTilde, forces, Adiel_RRT);
266 *Adiel_nCavityTilde = J(Adiel_nCavity);
267 }
268
269 //Collect energy and gradient pieces:
270 setMuEps(Adiel_state, Adiel_muPlus, Adiel_muMinus, Adiel_eps);
271 Adiel_state *= gInfo.dV; //converts variational derivative to total derivative
272 return Adiel;
273 }
274
minimizeFluid()275 void NonlinearPCM::minimizeFluid()
276 { if(fsp.nonlinearSCF)
277 { clearState();
278 Pulay<ScalarFieldTilde>::minimize(compute(0,0));
279 }
280 else
281 Minimizable<ScalarFieldMuEps>::minimize(e.fluidMinParams);
282 }
283
loadState(const char * filename)284 void NonlinearPCM::loadState(const char* filename)
285 { nullToZero(state, gInfo);
286 state.loadFromFile(filename);
287 }
288
saveState(const char * filename) const289 void NonlinearPCM::saveState(const char* filename) const
290 { if(mpiWorld->isHead()) state.saveToFile(filename);
291 }
292
get_Adiel_and_grad_internal(ScalarFieldTilde & Adiel_rhoExplicitTilde,ScalarFieldTilde & Adiel_nCavityTilde,IonicGradient * extraForces,matrix3<> * Adiel_RRT) const293 double NonlinearPCM::get_Adiel_and_grad_internal(ScalarFieldTilde& Adiel_rhoExplicitTilde, ScalarFieldTilde& Adiel_nCavityTilde, IonicGradient* extraForces, matrix3<>* Adiel_RRT) const
294 { ScalarFieldMuEps Adiel_state;
295 double A = (*this)(state, Adiel_state, &Adiel_rhoExplicitTilde, &Adiel_nCavityTilde, extraForces, Adiel_RRT);
296 accumExtraForces(extraForces, Adiel_nCavityTilde);
297 return A;
298 }
299
step(const ScalarFieldMuEps & dir,double alpha)300 void NonlinearPCM::step(const ScalarFieldMuEps& dir, double alpha)
301 { ::axpy(alpha, dir, state);
302 }
303
compute(ScalarFieldMuEps * grad,ScalarFieldMuEps * Kgrad)304 double NonlinearPCM::compute(ScalarFieldMuEps* grad, ScalarFieldMuEps* Kgrad)
305 { ScalarFieldMuEps gradUnused;
306 double E = (*this)(state, grad ? *grad : gradUnused);
307 //Compute preconditioned gradient:
308 if(Kgrad)
309 { const ScalarFieldMuEps& in = grad ? *grad : gradUnused;
310 double dielPrefac = 1./(gInfo.dV * dielectricEval->NT);
311 double ionsPrefac = screeningEval ? 1./(gInfo.dV * screeningEval->NT) : 0.;
312 setMuEps(*Kgrad,
313 ionsPrefac * I(preconditioner*J(getMuPlus(in))),
314 ionsPrefac * I(preconditioner*J(getMuMinus(in))),
315 dielPrefac * getEps(in));
316 }
317 return E;
318 }
319
320
dumpDensities(const char * filenamePattern) const321 void NonlinearPCM::dumpDensities(const char* filenamePattern) const
322 { PCM::dumpDensities(filenamePattern);
323
324 //Output dielectric bound charge:
325 string filename;
326 { ScalarField Aout; initZero(Aout, gInfo);
327 VectorField p; nullToZero(p, gInfo);
328 VectorField Adiel_eps; nullToZero(Adiel_eps, gInfo);
329 callPref(dielectricEval->freeEnergy)(gInfo.nr, getEps(state).const_dataPref(), shape[0]->dataPref(),
330 p.dataPref(), Aout->dataPref(), Adiel_eps.dataPref(), 0);
331 ScalarField rhoDiel = -divergence(p); //include bound charge due to dielectric
332 FLUID_DUMP(rhoDiel, "RhoDiel");
333 }
334
335 //Output ionic bound charge (if any):
336 if(screeningEval)
337 { ScalarField Nplus, Nminus;
338 { ScalarField muPlus = getMuPlus(state);
339 ScalarField muMinus = getMuMinus(state);
340 double Qexp = integral(rhoExplicitTilde);
341 double mu0 = screeningEval->neutralityConstraint(muPlus, muMinus, shape.back(), Qexp);
342 Nplus = ionNbulk * shape.back() * (fsp.linearScreening ? 1.+(mu0+muPlus) : exp(mu0+muPlus));
343 Nminus = ionNbulk * shape.back() * (fsp.linearScreening ? 1.-(mu0+muMinus) : exp(-(mu0+muMinus)));
344 }
345 FLUID_DUMP(Nplus, "N+");
346 FLUID_DUMP(Nminus, "N-");
347 FLUID_DUMP(ionZ*(Nplus-Nminus), "RhoIon");
348 }
349 }
350
351 //--------- Interface for Pulay<ScalarFieldTilde> ---------
352
cycle(double dEprev,std::vector<double> & extraValues)353 double NonlinearPCM::cycle(double dEprev, std::vector<double>& extraValues)
354 { //Update epsilon / kappaSq based on current phi:
355 phiToState(false);
356 //Inner linear solve
357 FILE*& fpLog = ((MinimizeParams&)e.fluidMinParams).fpLog;
358 fpLog = nullLog; //disable iteration log from LinearPCM
359 linearPCM->minimizeFluid();
360 fpLog = globalLog; //restore usual iteration log
361 //Update state from new phi:
362 phiToState(true);
363 return compute(0,0);
364 }
365
366
readVariable(ScalarFieldTilde & X,FILE * fp) const367 void NonlinearPCM::readVariable(ScalarFieldTilde& X, FILE* fp) const
368 { nullToZero(X, gInfo);
369 loadRawBinary(X, fp);
370 }
371
writeVariable(const ScalarFieldTilde & X,FILE * fp) const372 void NonlinearPCM::writeVariable(const ScalarFieldTilde& X, FILE* fp) const
373 { saveRawBinary(X, fp);
374 }
375
getVariable() const376 ScalarFieldTilde NonlinearPCM::getVariable() const
377 { return clone(linearPCM->state);
378 }
379
setVariable(const ScalarFieldTilde & X)380 void NonlinearPCM::setVariable(const ScalarFieldTilde& X)
381 { linearPCM->state = clone(X);
382 }
383
precondition(const ScalarFieldTilde & X) const384 ScalarFieldTilde NonlinearPCM::precondition(const ScalarFieldTilde& X) const
385 { return fsp.scfParams.mixFraction * X;
386 }
387
applyMetric(const ScalarFieldTilde & X) const388 ScalarFieldTilde NonlinearPCM::applyMetric(const ScalarFieldTilde& X) const
389 { return (*metric) * X;
390 }
391
phiToState(bool setState)392 void NonlinearPCM::phiToState(bool setState)
393 { //Initialize inputs:
394 const ScalarField phi = I(linearPCM->state);
395 const VectorField Dphi = I(gradient(linearPCM->state));
396 //Prepare outputs:
397 ScalarField epsilon, kappaSq;
398 if(!setState)
399 { nullToZero(epsilon, gInfo);
400 if(screeningEval)
401 nullToZero(kappaSq, gInfo);
402 }
403 VectorField eps = getEps(state);
404 ScalarField& muPlus = getMuPlus(state);
405 ScalarField& muMinus = getMuMinus(state);
406 //Calculate eps/mu or epsilon/kappaSq as needed:
407 vector3<double*> vecDataUnused(0,0,0); double* dataUnused=0;
408 callPref(dielectricEval->phiToState)(gInfo.nr, Dphi.dataPref(), shape[0]->dataPref(), gLookup, setState,
409 setState ? eps.dataPref() : vecDataUnused,
410 setState ? dataUnused : epsilon->dataPref() );
411 if(screeningEval)
412 callPref(screeningEval->phiToState)(gInfo.nr, phi->dataPref(), shape.back()->dataPref(), xLookup, setState,
413 setState ? muPlus->dataPref() : dataUnused,
414 setState ? muMinus->dataPref() : dataUnused,
415 setState ? dataUnused : kappaSq->dataPref() );
416 //Save to global state or linearPCM as required:
417 if(setState)
418 setMuEps(state, muPlus, muMinus, eps);
419 else
420 linearPCM->override(epsilon, kappaSq);
421 }
422