OOFEM 3.0
Loading...
Searching...
No Matches
isodamagemodel.C
Go to the documentation of this file.
1/*
2 *
3 * ##### ##### ###### ###### ### ###
4 * ## ## ## ## ## ## ## ### ##
5 * ## ## ## ## #### #### ## # ##
6 * ## ## ## ## ## ## ## ##
7 * ## ## ## ## ## ## ## ##
8 * ##### ##### ## ###### ## ##
9 *
10 *
11 * OOFEM : Object Oriented Finite Element Code
12 *
13 * Copyright (C) 1993 - 2025 Borek Patzak
14 *
15 *
16 *
17 * Czech Technical University, Faculty of Civil Engineering,
18 * Department of Structural Mechanics, 166 29 Prague, Czech Republic
19 *
20 * This library is free software; you can redistribute it and/or
21 * modify it under the terms of the GNU Lesser General Public
22 * License as published by the Free Software Foundation; either
23 * version 2.1 of the License, or (at your option) any later version.
24 *
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28 * Lesser General Public License for more details.
29 *
30 * You should have received a copy of the GNU Lesser General Public
31 * License along with this library; if not, write to the Free Software
32 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
33 */
34
35#include "isodamagemodel.h"
36#include "floatmatrix.h"
37#include "floatarray.h"
38#include "mathfem.h"
39#include "datastream.h"
40#include "contextioerr.h"
41#include "dynamicinputrecord.h"
42#include "gausspoint.h"
43
44namespace oofem {
45IsotropicDamageMaterial :: IsotropicDamageMaterial(int n, Domain *d) : StructuralMaterial(n, d)
46{
47}
48
49
50IsotropicDamageMaterial :: ~IsotropicDamageMaterial()
51{
53}
54
55bool
56IsotropicDamageMaterial :: hasMaterialModeCapability(MaterialMode mode) const
57{
58 return mode == _3dMat || mode == _PlaneStress || mode == _PlaneStrain || mode == _1dMat;
59}
60
61
63IsotropicDamageMaterial :: give3dMaterialStiffnessMatrix(MatResponseMode mode,
64 GaussPoint *gp,
65 TimeStep *tStep) const
66//
67// computes full constitutive matrix for case of gp stress-strain state.
68//
69{
70 auto status = static_cast< IsotropicDamageMaterialStatus * >( this->giveStatus(gp) );
71 double tempDamage;
72 if ( mode == ElasticStiffness ) {
73 tempDamage = 0.0;
74 } else {
75 tempDamage = status->giveTempDamage();
76 tempDamage = min(tempDamage, maxOmega);
77 }
78
79 auto d = this->linearElasticMaterial->give3dMaterialStiffnessMatrix(mode, gp, tStep);
80 return d * (1.0 - tempDamage);
81 //TODO - correction for tangent mode
82}
83
84
85void
86IsotropicDamageMaterial :: giveRealStressVector(FloatArray &answer, GaussPoint *gp,
87 const FloatArray &totalStrain,
88 TimeStep *tStep) const
89//
90// returns real stress vector in 3d stress space of receiver according to
91// previous level of stress and current
92// strain increment, the only way, how to correctly update gp records
93//
94{
95 IsotropicDamageMaterialStatus *status = static_cast< IsotropicDamageMaterialStatus * >( this->giveStatus(gp) );
96 //StructuralCrossSection *crossSection = (StructuralCrossSection*) gp -> giveElement()->giveCrossSection();
98 FloatArray reducedTotalStrainVector;
99 FloatMatrix de;
100 double f, equivStrain, tempKappa = 0.0, omega = 0.0;
101
102 this->initTempStatus(gp);
103
104 // subtract stress-independent part
105 // note: eigenStrains (temperature) are present in strains stored in gp
106 // therefore it is necessary to subtract always the total eigen strain value
107 this->giveStressDependentPartOfStrainVector(reducedTotalStrainVector, gp, totalStrain, tStep, VM_Total);
108
109 //crossSection->giveFullCharacteristicVector(totalStrainVector, gp, reducedTotalStrainVector);
110
111 // compute equivalent strain
112 equivStrain = this->computeEquivalentStrain(reducedTotalStrainVector, gp, tStep);
113
114 if ( llcriteria == idm_strainLevelCR ) {
115 // compute value of loading function if strainLevel crit apply
116 f = equivStrain - status->giveKappa();
117
118 if ( f <= 0.0 ) {
119 // damage does not grow
120 tempKappa = status->giveKappa();
121 omega = status->giveDamage();
122 } else {
123 // damage grows
124 tempKappa = equivStrain;
125 this->initDamaged(tempKappa, reducedTotalStrainVector, gp);
126 // evaluate damage parameter
127 omega = this->computeDamageParam(tempKappa, reducedTotalStrainVector, gp);
128 }
129 } else if ( llcriteria == idm_damageLevelCR ) {
130 // evaluate damage parameter first
131 tempKappa = equivStrain;
132 this->initDamaged(tempKappa, reducedTotalStrainVector, gp);
133 omega = this->computeDamageParam(tempKappa, reducedTotalStrainVector, gp);
134 if ( omega < status->giveDamage() ) {
135 // unloading takes place
136 omega = status->giveDamage();
137 }
138 } else {
139 OOFEM_ERROR("unsupported loading/unloading criterion");
140 }
141
142
143 lmat->giveStiffnessMatrix(de, SecantStiffness, gp, tStep);
144 //mj
145 // permanent strain - so far implemented only in 1D
146 if ( permStrain && reducedTotalStrainVector.giveSize() == 1 ) {
147 double epsp = evaluatePermanentStrain(tempKappa, omega);
148 reducedTotalStrainVector.at(1) -= epsp;
149 }
150 // damage deactivation in compression for 1D model
151 if ( ( reducedTotalStrainVector.giveSize() > 1 ) || ( reducedTotalStrainVector.at(1) > 0. ) ) {
152 //emj
153 de.times(1.0 - omega);
154 }
155
156 answer.beProductOf(de, reducedTotalStrainVector);
157
158 // update gp
159 status->letTempStrainVectorBe(totalStrain);
160 status->letTempStressVectorBe(answer);
161 status->setTempKappa(tempKappa);
162 status->setTempDamage(omega);
163#ifdef keep_track_of_dissipated_energy
164 status->computeWork(gp);
165#endif
166}
167
168
170IsotropicDamageMaterial :: givePlaneStressStiffMtrx(MatResponseMode mode, GaussPoint *gp, TimeStep *tStep) const
171{
172 auto status = static_cast< IsotropicDamageMaterialStatus * >( this->giveStatus(gp) );
173 double tempDamage;
174 if ( mode == ElasticStiffness ) {
175 tempDamage = 0.0;
176 } else {
177 tempDamage = status->giveTempDamage();
178 tempDamage = min(tempDamage, maxOmega);
179 }
180
181 auto d = this->linearElasticMaterial->givePlaneStressStiffMtrx(mode, gp, tStep);
182 d *= 1.0 - tempDamage;
183 if ( mode == TangentStiffness ) {
184 double damage = status->giveDamage();
185 if ( tempDamage > damage ) {
186 double tempKappa;
187 FloatArray stress, strain, eta;
188 FloatMatrix correctionTerm;
189 stress = status->giveTempStressVector();
190 strain = status->giveTempStrainVector();
191 tempKappa = status->giveTempKappa();
192 // effective stress
193 stress.times( 1. / ( 1 - tempDamage ) );
194 //Computes derivative of the equivalent strain with regards to strain
195 this->computeEta(eta, strain, gp, tStep);
196 //compute derivative of damage function
197 double damagePrime = damageFunctionPrime(tempKappa, gp);
198 // dyadic product of eff stress and eta
199 correctionTerm.beDyadicProductOf(stress, eta);
200 // times minus derivative of damage function
201 correctionTerm.times(-damagePrime);
202 // add to secant stiffness
203 d += FloatMatrixF<3,3>(correctionTerm);
204 }
205 }
206 return d;
207}
208
209
211IsotropicDamageMaterial :: givePlaneStrainStiffMtrx(MatResponseMode mode, GaussPoint *gp, TimeStep *tStep) const
212{
213 auto status = static_cast< IsotropicDamageMaterialStatus * >( this->giveStatus(gp) );
214 double tempDamage;
215 if ( mode == ElasticStiffness ) {
216 tempDamage = 0.0;
217 } else {
218 tempDamage = status->giveTempDamage();
219 tempDamage = min(tempDamage, maxOmega);
220 }
221
222 return (1.0 - tempDamage) * this->linearElasticMaterial->givePlaneStrainStiffMtrx(mode, gp, tStep);
223 //TODO - correction for tangent mode
224}
225
226
228IsotropicDamageMaterial :: give1dStressStiffMtrx(MatResponseMode mode, GaussPoint *gp, TimeStep *tStep) const
229{
230 auto status = static_cast< IsotropicDamageMaterialStatus * >( this->giveStatus(gp) );
231 double tempDamage;
232 if ( mode == ElasticStiffness ) {
233 tempDamage = 0.0;
234 } else {
235 tempDamage = status->giveTempDamage();
236 tempDamage = min(tempDamage, maxOmega);
237 }
238
239 auto answer = this->linearElasticMaterial->give1dStressStiffMtrx(mode, gp, tStep);
240 answer *= 1.0 - tempDamage;
241
242 if ( mode == TangentStiffness ) {
243 double damage = status->giveDamage();
244 if ( tempDamage > damage ) {
245 double tempKappa;
246 FloatArray stress, strain, eta;
247 FloatMatrix correctionTerm;
248 stress = status->giveTempStressVector();
249 strain = status->giveTempStrainVector();
250 tempKappa = status->giveTempKappa();
251 // effective stress
252 stress.times( 1. / ( 1 - tempDamage ) );
253 //Computes derivative of the equivalent strain with regards to strain
254 this->computeEta(eta, strain, gp, tStep);
255 //compute derivative of damage function
256 double damagePrime = damageFunctionPrime(tempKappa, gp);
257 // dyadic product of eff stress and eta
258 correctionTerm.beDyadicProductOf(stress, eta);
259 // times minus derivative of damage function
260 correctionTerm.times(-damagePrime);
261 // add to secant stiffness
262 answer += FloatMatrixF<1,1>(correctionTerm);
263 }
264 }
265 return answer;
266 //TODO - correction for tangent mode
267}
268
269#ifdef __OOFEG
270#endif
271
272
273int
274IsotropicDamageMaterial :: giveIPValue(FloatArray &answer, GaussPoint *gp, InternalStateType type, TimeStep *tStep)
275{
276 IsotropicDamageMaterialStatus *status = static_cast< IsotropicDamageMaterialStatus * >( this->giveStatus(gp) );
277 if ( type == IST_DamageScalar ) {
278 answer.resize(1);
279 answer.zero();
280 answer.at(1) = status->giveDamage();
281 return 1;
282 } else if ( type == IST_DamageTensor ) {
283 answer.resize(6);
284 answer.zero();
285 answer.at(1) = answer.at(2) = answer.at(3) = status->giveDamage();
286 return 1;
287 } else if ( type == IST_PrincipalDamageTensor ) {
288 answer.resize(3);
289 answer.zero();
290 answer.at(1) = status->giveDamage();
291 return 1;
292 } else if ( type == IST_DamageTensorTemp ) {
293 answer.resize(6);
294 answer.zero();
295 answer.at(1) = answer.at(2) = answer.at(3) = status->giveTempDamage();
296 return 1;
297 } else if ( type == IST_PrincipalDamageTempTensor ) {
298 answer.resize(3);
299 answer.zero();
300 answer.at(1) = status->giveTempDamage();
301 return 1;
302 } else if ( type == IST_MaxEquivalentStrainLevel ) {
303 answer.resize(1);
304 answer.at(1) = status->giveKappa();
305 return 1;
306 } else if ( type == IST_CharacteristicLength ) {
307 answer.resize(1);
308 answer.at(1) = status->giveLe();
309 return 1;
310 } else if ( type == IST_CrackWidth ) {
311 answer.resize(1);
312 FloatArray reducedTotalStrainVector;
313 this->giveStressDependentPartOfStrainVector(reducedTotalStrainVector, gp, status->giveStrainVector(), tStep, VM_Total);
314 double maxStrain = reducedTotalStrainVector.giveIndexMaxElem();
315 answer.at(1) = status->giveLe() * status->giveDamage() * reducedTotalStrainVector.at(maxStrain);
316 } else if ( type == IST_CrackDirs ) {
317 answer.resize(1);
318 answer.at(1) = status->giveCrackAngle();
319 return 1;
320 } else if ( type == IST_CrackVector ) {
321 answer = status->giveCrackVector();
322 return 1;
323 } else if ( type == IST_CumPlasticStrain ) {
324 if ( permStrain ) {
325 answer.at(1) = evaluatePermanentStrain(status->giveKappa(), status->giveDamage());
326 }
327 return 1;
328#ifdef keep_track_of_dissipated_energy
329 } else if ( type == IST_StressWorkDensity ) {
330 answer.resize(1);
331 answer.at(1) = status->giveStressWork();
332 return 1;
333 } else if ( type == IST_DissWorkDensity ) {
334 answer.resize(1);
335 answer.at(1) = status->giveDissWork();
336 } else if ( type == IST_FreeEnergyDensity ) {
337 answer.resize(1);
338 answer.at(1) = status->giveStressWork() - status->giveDissWork();
339 return 1;
340
341#endif
342 } else {
343 return StructuralMaterial :: giveIPValue(answer, gp, type, tStep);
344 }
345
346 return 1; // to make the compiler happy
347}
348
349
351IsotropicDamageMaterial :: giveThermalDilatationVector(GaussPoint *gp, TimeStep *tStep) const
352//
353// returns a FloatArray(6) of initial strain vector
354// eps_0 = {exx_0, eyy_0, ezz_0, gyz_0, gxz_0, gxy_0}^T
355// caused by unit temperature in direction of
356// gp (element) local axes
357//
358{
359 return {
360 this->tempDillatCoeff,
361 this->tempDillatCoeff,
362 this->tempDillatCoeff,
363 0., 0., 0.,
364 };
365}
366
367double IsotropicDamageMaterial :: give(int aProperty, GaussPoint *gp) const
368{
369 return linearElasticMaterial->give(aProperty, gp);
370}
371
372void
373IsotropicDamageMaterial :: initializeFrom(InputRecord &ir)
374{
375 StructuralMaterial :: initializeFrom(ir);
376
377 //Set limit on the maximum isotropic damage parameter if needed
379 maxOmega = min(maxOmega, 0.999999);
380 maxOmega = max(maxOmega, 0.0);
381
382 permStrain = 0; // default - no permanent strain used
384
386}
387
388
389void
390IsotropicDamageMaterial :: giveInputRecord(DynamicInputRecord &input)
391{
392 StructuralMaterial :: giveInputRecord(input);
395}
396
397
398void
400{
402 if ( ( mode & CM_Definition ) ) {
403 linearElasticMaterial->saveContext(stream, mode);
404 }
405}
406
407
408void
410{
412 if ( ( mode & CM_Definition ) ) {
413 linearElasticMaterial->saveContext(stream, mode);
414 }
415}
416
417
418IsotropicDamageMaterialStatus :: IsotropicDamageMaterialStatus(GaussPoint *g) : StructuralMaterialStatus(g)
419{
420}
421
422
423void
424IsotropicDamageMaterialStatus :: printOutputAt(FILE *file, TimeStep *tStep) const
425{
426 StructuralMaterialStatus :: printOutputAt(file, tStep);
427 fprintf(file, "status { ");
428 if ( this->kappa > 0 && this->damage <= 0 ) {
429 fprintf(file, "kappa %f", this->kappa);
430 } else if ( this->damage > 0.0 ) {
431 fprintf( file, "kappa %f, damage %f crackVector %f %f %f", this->kappa, this->damage, this->crackVector.at(1), this->crackVector.at(2), this->crackVector.at(3) );
432
433#ifdef keep_track_of_dissipated_energy
434 fprintf(file, ", dissW %f, freeE %f, stressW %f ", this->dissWork, ( this->stressWork ) - ( this->dissWork ), this->stressWork);
435 } else {
436 fprintf(file, "stressW %f ", this->stressWork);
437#endif
438 }
439
440 fprintf(file, "}\n");
441}
442
443
444void
445IsotropicDamageMaterialStatus :: initTempStatus()
446{
447 StructuralMaterialStatus :: initTempStatus();
448 this->tempKappa = this->kappa;
449 //mj 14 July 2010 - should be discussed with Borek !!!
450 this->tempDamage = this->damage;
451#ifdef keep_track_of_dissipated_energy
452 this->tempStressWork = this->stressWork;
453 this->tempDissWork = this->dissWork;
454#endif
455}
456
457
458void
459IsotropicDamageMaterialStatus :: updateYourself(TimeStep *tStep)
460{
461 StructuralMaterialStatus :: updateYourself(tStep);
462 this->kappa = this->tempKappa;
463 this->damage = this->tempDamage;
464#ifdef keep_track_of_dissipated_energy
465 this->stressWork = this->tempStressWork;
466 this->dissWork = this->tempDissWork;
467#endif
468}
469
470
471void
472IsotropicDamageMaterialStatus :: saveContext(DataStream &stream, ContextMode mode)
473{
474 StructuralMaterialStatus :: saveContext(stream, mode);
475
476 if ( !stream.write(kappa) ) {
478 }
479
480 if ( !stream.write(damage) ) {
482 }
483
484#ifdef keep_track_of_dissipated_energy
485 if ( !stream.write(stressWork) ) {
487 }
488
489 if ( !stream.write(dissWork) ) {
491 }
492
493#endif
494}
495
496void
497IsotropicDamageMaterialStatus :: restoreContext(DataStream &stream, ContextMode mode)
498{
499 StructuralMaterialStatus :: restoreContext(stream, mode);
500
501 if ( !stream.read(kappa) ) {
503 }
504
505 if ( !stream.read(damage) ) {
507 }
508
509#ifdef keep_track_of_dissipated_energy
510 if ( !stream.read(stressWork) ) {
512 }
513
514 if ( !stream.read(dissWork) ) {
516 }
517
518#endif
519}
520
521#ifdef keep_track_of_dissipated_energy
522void
523IsotropicDamageMaterialStatus :: computeWork(GaussPoint *gp)
524{
525 // strain increment
526 FloatArray deps;
528
529 // increment of stress work density
530 double dSW = ( tempStressVector.dotProduct(deps) + stressVector.dotProduct(deps) ) / 2.;
532
533 // elastically stored energy density
534 double We = tempStressVector.dotProduct(tempStrainVector) / 2.;
535
536 // dissipative work density
538}
539#endif
540} // end namespace oofem
virtual int read(int *data, std::size_t count)=0
Reads count integer values into array pointed by data.
virtual int write(const int *data, std::size_t count)=0
Writes count integer values from array pointed by data.
void setField(int item, InputFieldType id)
virtual void restoreContext(DataStream &stream, ContextMode mode)
Definition femcmpnn.C:62
virtual void saveContext(DataStream &stream, ContextMode mode)
Definition femcmpnn.C:51
void resize(Index s)
Definition floatarray.C:94
double & at(Index i)
Definition floatarray.h:202
Index giveSize() const
Returns the size of receiver.
Definition floatarray.h:261
void beDifferenceOf(const FloatArray &a, const FloatArray &b)
Definition floatarray.C:403
void zero()
Zeroes all coefficients of receiver.
Definition floatarray.C:683
int giveIndexMaxElem(void)
Definition floatarray.C:508
void times(double s)
Definition floatarray.C:834
void times(double f)
void beDyadicProductOf(const FloatArray &vec1, const FloatArray &vec2)
GaussPoint * gp
Associated integration point.
double stressWork
Density of total work done by stresses on strain increments.
double damage
Damage level of material.
double giveStressWork() const
Returns the density of total work of stress on strain increments.
double kappa
Scalar measure of the largest strain level ever reached in material.
double giveKappa() const
Returns the last equilibrated scalar measure of the largest strain level.
double tempDissWork
Non-equilibrated density of dissipated work.
FloatArrayF< 3 > giveCrackVector() const
Returns crack vector stored in receiver. This is useful for plotting cracks as a vector field (paravi...
void computeWork(GaussPoint *gp)
Computes the increment of total stress work and of dissipated work.
void setTempKappa(double newKappa)
Sets the temp scalar measure of the largest strain level to given value.
double giveLe() const
Returns characteristic length stored in receiver.
double giveDamage() const
Returns the last equilibrated damage level.
double giveTempDamage() const
Returns the temp. damage level.
FloatArrayF< 3 > crackVector
Crack orientation normalized to damage magnitude. This is useful for plotting cracks as a vector fiel...
void setTempDamage(double newDamage)
Sets the temp damage level to given value.
double giveCrackAngle() const
Returns crack angle stored in receiver.
double tempDamage
Non-equilibrated damage level of material.
double giveDissWork() const
Returns the density of dissipated work.
double dissWork
Density of dissipated work.
double tempStressWork
Non-equilibrated density of total work done by stresses on strain increments.
double tempKappa
Non-equilibrated scalar measure of the largest strain level.
double maxOmega
Maximum limit on omega. The purpose is elimination of a too compliant material which may cause conver...
virtual void initDamaged(double kappa, FloatArray &totalStrainVector, GaussPoint *gp) const
LinearElasticMaterial * linearElasticMaterial
Reference to bulk (undamaged) material.
double tempDillatCoeff
Coefficient of thermal dilatation.
int permStrain
Indicator of the type of permanent strain formulation (0 = standard damage with no permanent strain).
virtual double computeDamageParam(double kappa, const FloatArray &strain, GaussPoint *gp) const =0
enum oofem::IsotropicDamageMaterial::loaUnloCriterium llcriteria
virtual double evaluatePermanentStrain(double kappa, double omega) const
virtual double computeEquivalentStrain(const FloatArray &strain, GaussPoint *gp, TimeStep *tStep) const =0
virtual void computeEta(FloatArray &answer, const FloatArray &strain, GaussPoint *gp, TimeStep *tStep) const
virtual double damageFunctionPrime(double kappa, GaussPoint *gp) const
void saveContext(DataStream &stream, ContextMode mode) override
LinearElasticMaterial * giveLinearElasticMaterial() const
Returns reference to undamaged (bulk) material.
void restoreContext(DataStream &stream, ContextMode mode) override
virtual MaterialStatus * giveStatus(GaussPoint *gp) const
Definition material.C:206
virtual void initTempStatus(GaussPoint *gp) const
Definition material.C:221
const FloatArray & giveStrainVector() const
Returns the const pointer to receiver's strain vector.
StructuralMaterialStatus(GaussPoint *g)
Constructor. Creates new StructuralMaterialStatus with IntegrationPoint g.
FloatArray tempStrainVector
Temporary strain vector in reduced form (to find balanced state).
FloatArray tempStressVector
Temporary stress vector in reduced form (increments are used mainly in nonlinear analysis).
FloatArray stressVector
Equilibrated stress vector in reduced form.
FloatArray strainVector
Equilibrated strain vector in reduced form.
void letTempStressVectorBe(const FloatArray &v)
Assigns tempStressVector to given vector v.
void letTempStrainVectorBe(const FloatArray &v)
Assigns tempStrainVector to given vector v.
StructuralMaterial(int n, Domain *d)
virtual void giveStiffnessMatrix(FloatMatrix &answer, MatResponseMode mode, GaussPoint *gp, TimeStep *tStep) const
void giveStressDependentPartOfStrainVector(FloatArray &answer, GaussPoint *gp, const FloatArray &reducedStrainVector, TimeStep *tStep, ValueModeType mode) const
#define THROW_CIOERR(e)
#define CM_Definition
Definition contextmode.h:47
#define OOFEM_ERROR(...)
Definition error.h:79
#define IR_GIVE_OPTIONAL_FIELD(__ir, __value, __id)
Definition inputrecord.h:75
#define IR_GIVE_FIELD(__ir, __value, __id)
Definition inputrecord.h:67
#define _IFT_IsotropicDamageMaterial_talpha
#define _IFT_IsotropicDamageMaterial_maxOmega
#define _IFT_IsotropicDamageMaterial_permstrain
long ContextMode
Definition contextmode.h:43
FloatArrayF< N > min(const FloatArrayF< N > &a, const FloatArrayF< N > &b)
FloatArrayF< N > max(const FloatArrayF< N > &a, const FloatArrayF< N > &b)
@ CIO_IOERR
General IO error.

This page is part of the OOFEM-3.0 documentation. Copyright Copyright (C) 1994-2025 Borek Patzak Bořek Patzák
Project e-mail: oofem@fsv.cvut.cz
Generated at for OOFEM by doxygen 1.15.0 written by Dimitri van Heesch, © 1997-2011