Tras las versiones iniciales en Fortran77 y
Fortran90, LAMMPS está ahora escrito en C++,
un lenguaje orientado a objetos que permite a cualquier programador
explotar el paradigma de programación de clases. La declaración de una
clase, incluida la firma de las variables de la instancia y las
funciones (o métodos), puede accederse y ser utilizada mediante la
creación de una instancia de esa clase. Los datos y funciones de una
clase se denominan miembros de la clase. La definición (o
implementación) de una función miembro puede darse dentro o fuera de la
definición de la clase. Una clase tiene secciones de tipo
privado, publico y protegido que
contienen los correspondientes miembros de la clase.
A los miembros privados, definidos antes de la palabra clave public, no se puede acceder desde fuera de la clase. Sólo se puede acceder a ellos mediante funciones de la clase o “amigas", que se declaran como con acceso a los miembros de la clase, sin ser ellas mismas miembros. Todos los miembros de la clase son privados por defecto.
A los miembros públicos se puede acceder desde fuera de la clase en cualquier lugar dentro del ámbito del objeto de la clase.
Los miembros protegidos son similares a los miembros privados, pero pueden ser accedidos por clases derivadas o clases hijas, mientras que los miembros privados no pueden ser accedidos de esta forma.
Un concepto importante en la programación orientada a objetos es el de herencia. La herencia permite definir una clase en términos de otra clase y la nueva clase hereda los miembros de la clase existente. Esta clase existente se denomina clase base (o padre), y la nueva clase se denomina subclase, clase hija o clase derivada. La idea de herencia implementa una relación de tipo “es un”. Por ejemplo, un Mamífero ES-UN Animal, un Perro ES-UN Mamífero y, por tanto, un Perro ES-UN Animal también.
El tipo de herencia se especifica mediante el especificador de acceso, que puede ser público, protegido o privado. Si no se utiliza el especificador de acceso, entonces es privada por defecto, pero la herencia pública se utiliza comúnmente: los miembros públicos de la clase base se convierten en miembros públicos de la clase derivada y los miembros protegidos de la clase base se convierten en miembros protegidos de la clase derivada. Los miembros privados de una clase base nunca son accesibles directamente desde una clase derivada, pero se puede acceder a ellos a través de llamadas a los miembros públicos y protegidos de la clase base.
La relación de herencia entre las clases base y derivada se declara en la clase derivada mediante la siguiente sintaxis:
class nombre_clase_derivada: access_specifier nombre_clase_base
{ /* ... */};La firma de una función \(f\) debe declararse con la palabra clave en una clase base para permitir su definición (implementación), o redefinición, en una clase derivada . Entonces, cuando un objeto de la clase derivada se utiliza como elemento de la clase base , y se llama a \(f\), se ejecuta la implementación de la función de la clase derivada. No hay nada malo en poner el delante de las funciones dentro de las clases derivadas, pero no es necesario, a menos que se sepa con seguridad que la clase no tendrá ningún hijo que necesite anular las funciones de la clase base. Una clase que declara o hereda una función virtual se denomina clase polimórfica.
Una representación esquemática del árbol de herencia de LAMMPS se muestra en la (tomada de ): LAMMPS es la clase de nivel superior para todo el código, luego todas las clases núcleo, resaltadas en azul en la figura, heredan todos los constructores, destructores, miembros operadores de asignación, amigos y miembros privados declarados y definidos en LAMMPS. Las clases núcleo realizan acciones fundamentales de LAMMPS. Por ejemplo, la clase recoge y almacena todos los datos por átomo, o por partícula, mientras que la clase construye las listas de vecinos .
Las clases de estilo, resaltadas en rojo en la , heredan todos los constructores, destructores, miembros operadores de asignación, amigos y miembros privados declarados y definidos en LAMMPS y en la clase núcleo correspondiente. Las clases de estilo son también clases virtuales padres de muchas clases hijas que implementan la interfaz definida por la clase padre. Por ejemplo, el estilo tiene alrededor de 100 clases hijas. Cada estilo se compone de un par de archivos, como se indica a continuación:
La cabecera del estilo, donde se define el estilo de la clase y se declaran todos los objetos, métodos y constructores.
Donde se definen todos los objetos, métodos y constructores declarados en la clase de estilo.
Cuando se escribe un nuevo estilo, deben crearse los archivos y . Cada estilo de "familia" tiene su propio conjunto de métodos, declarados en la cabecera y definidos en el archivo , para definir el ámbito del estilo. Por ejemplo, el estilo par son clases que establecen la(s) fórmula(s) que LAMMPS utiliza para calcular las interacciones entre pares, mientras que el estilo enlace establece la(s) fórmula(s) para calcular las interacciones de enlace entre pares de átomos . Cada estilo de par tiene algunas funciones recurrentes como , y . Aunque el ámbito final de esas funciones puede diferir para diferentes estilos, todas comparten un papel similar dentro de las clases. Un ejemplo de un encabezado de estilo de par en LAMMPS se muestra a continuación para el estilo .
Dado que la versión actual de LAMMPS está completamente basada en , todos los miembros de la clase están definidos en un archivo . Con el fin de ampliar la presente discusión, y tomando como referencia el estilo del par , cada método declarado en será detallatado y comentado a continuación. Aunque estos métodos son específicos de cada estilo, el objetivo es dar una visión general de cómo se definen los métodos en el archivo en LAMMPS. En cualquier caso, la metodología para definir estilos en LAMMPS puede ser aplicada a la generación de otros estilos que cualquier programador desee implementar.
Cualquier clase suele incluir una función miembro llamada constructor. El constructor se invoca mecánicamente cuando se crea un objeto de la clase. Esto permite a la clase inicializar miembros o asignar almacenamiento. A diferencia de los demás miembros de la clase, el nombre del constructor debe coincidir con el nombre de la clase y no tiene tipo de retorno, como se muestra en .
/* ------------------------------------------------------------ */
PairSPHTaitwater::PairSPHTaitwater(LAMMPS *lmp) : Pair(lmp)
{
restartinfo = 0;
first = 1;
}
/* ------------------------------------------------------------ */
Ejemplo de definición de método constructorEl papel del método destructor es desasignar la memoria dinámica asignada, siendo invocados mecánicamente justo antes del final del tiempo de vida de la clase. De forma similar a los constructores, los destructores no tienen un tipo de retorno y tienen el mismo nombre que el nombre de la clase pero con el simbolo tilde (\(\sim\)) antepuesto, como se muestra a continuación.
/* ------------------------------------------------------------ */
PairSPHTaitwater::~PairSPHTaitwater() {
if (allocated) {
memory->destroy(setflag);
memory->destroy(cutsq);
memory->destroy(cut);
memory->destroy(rho0);
memory->destroy(soundspeed);
memory->destroy(B);
memory->destroy(viscosity);
}
}
/* ------------------------------------------------------------ */
Ejemplo de definición de método destructorescomputeEl método computees un miembro virtual del estilo de par y es
una de las funciones más relevantes en un número de clases en LAMMPS. Por
ejemplo, en las clases de estilos de pares se utiliza para calcular la
interacción entre pares del estilo de par específico. Esto puede verse en el
código incluido a continuación, donde la fuerza aplicada
en un par de partículas vecinas es derivada usando la ecuación de Tait, líneas
131-151. En compute todos los parámetros locales necesarios para
calcular la interacción entre pares son declarados y definidos dentro del
método.
settingssettings es una función pública de tipo void que lee el script de entrada
comprobando que todos los argumentos del estilo par están declarados. Si
los argumentos están presentes, los almacena para que puedan ser
utilizados por compute . A continuación se muestran ejemplos de secuencias de
comandos de entrada de estilo par sin argumentos y de estilo par con
argumentos con las configuraciones correspondientes:
Estilo de par sin argumentos: . Como se describe en el manual de SPH para LAMMPS , la línea de comandos para invocar el estilo de par es:
la cual se usa en los scripts de configuración de caso de LAMMPS. En este estilo de par sólo hay una palabra que define el estilo de par (“sph/taitwater"), sin argumentos. Por esta razón en el método de este par, mostrado en el , produce un error cuando la sentencia es verdadera, es decir si el número de argumentos distinto de cero.
void PairSPHTaitwater::settings(int narg, char **arg) {
if (narg != 0)
error->all(FLERR,
"Illegal number of setting arguments for pair_style sph/taitwater");
}
Ejemplo de definición de método settings para estilo de par sin argumentosEstilo de par con argumentos: . Como se describe en el manual de SPH para LAMMPS , la línea de comandos para invocar el estilo de par , que si requiere argumentos es:
y la cual, como en el caso anterior, debe ser usada en los scripts de configuración de caso de LAMMPS. En este estilo de par la cadena de caracteres que define el estilo de par es “sph/rhosum", más un argumento denominado . Por esta razón en el archivo , presentado en el , cuando la sentencia es verdadera (número de argumentos distinto de uno) se produce un error. Cuando la sentencia if es falsa el método asigna el valor de a la variable , utilizando la función definida en la clase .
void PairSPHRhoSum::settings(int narg, char **arg) {
if (narg != 1)
error->all(FLERR,
"Illegal number of setting arguments for pair_style sph/rhosum");
nstep = force->inumeric(arg[0]);
}
Ejemplo de definición de método settings para estilo de par sin argumentoscoeffSimilar a settings, el método coeff es una función
pública de tipo que lee y establece los coeficientes utilizados por el método
de cada estilo de par. Para cada par \(ij\) es
posible establecer diferentes coeficientes. Los coeficientes se suministran a
través del archivo de entrada de caso de simulación LAMMPS con una línea de
comando iniciando con la instrucción . Por ejemplo, para invocar y emplear el
estilo de par , se debe agregar la siguiente línea en el arcivo de
configuración de caso:
con la cuál se definen los parámetros para configurar el estilo de par . Para este estilo de par deben suministrase en total seis argumentos. Como puede verse en el , cuando la sentencia es verdadera (número de argumentos distinto de seis) se produce un error. Cuando la sentencia es falsa asigna el tipo de partículas \(I\) y \(J\), y adicionalmente el valor de las variables , , y mediante la función definida en la clase . En este ejemplo el método también asigna las variables para cada partícula, lo que puede verse en el bucle doble mostrado entre las líneas 19 a la 35 del .
Un segundo ejemplo del método es el del estilo de par , que se presenta en el . La sintaxis requerida para invocar este estilo de par en una simulación LAMMPS es mediante la siguiente línea de código:
en donde puede verse que este estilo de par requiere tres argumentos. Examinando la definición del método para este estilo de par (), es claro que cuando la sentencia es verdadera (número de argumentos distinto de tres) se produce un error. Cuando no se produce el error la función asigna el tipo de partículas \(I\) y \(J\) además del valor de \(h\) a la variable i, mediante el uso de las funciones y definidas en la clase . Por último, dentro del bucle doble , definido entre las líneas 14 a la 20 del , se asignan las variables para cada partícula.
lst:coeffSphRhoSumcoeffPairSphRhoSum.cpp
Otros métodos comunmente encontrados en la definición de las clases de estilo de par son , , y . comprueba si se han asignado todos los coeficientes de un par \(i\,j\) dado. Si se han asignado, el método garantiza la simetría de la matriz, como se muestra en . Por otro lado, en , mostrado en el , se evalúan la fuerza y la energía de una única interacción de pares, o de un único enlace o ángulo (en caso de estilo enlace o ángulo), entre dos átomos. El método es invocado específicamente por la línea de comando (o ) para calcular propiedades de interacciones individuales de pares o enlaces . Finalmente, el método , mostrado en , es una función protegida que asigna memoria dinámica. La asignación de memoria dinámica se utiliza cuando la cantidad de memoria necesaria depende de la entrada del usuario. Como se ha explicado antes, al final del tiempo de vida de la clase, los destructores desasignarán la memoria utilizada por .
lst:initOneSphTaitwaterinitOnePairSphTaitwater.cpp
lst:singleSphTaitwatersinglePairSphTaitwater.cpp
lst:allocateSphTaitwaterallocatePairSphTaitwater.cpp
La principal diferencia entre el enfoque de implementación directa vs. el enfoque de Multifísica discreta (DMP) radica en la complejidad del primer enfoque en contraste con la simplicidad de adaptación del enfoque DMP. En el primer caso, el de implementación de estilo de par directamente en LAMMPS, hay que generar toda una nueva clase que deberá ajustarse a algún modelo de comportamiento de fluido viscoelástico, por ejemplo mediante la selección de uno de los modelos estándar presentados en 2, y la genereación de una nueva clase de estilo de par. Este proceso conlleva redefinir todos los métodos requeridos por el nuevo estilo de par y, por tanto, resultar en un trabajo que puede ser bastante intensivo o dispendioso. Por otro lado, mediante el enfoque DMP basta con hacer una selección cuidadosa de los estilos de par a ser utilizados de manera concurrente, y generar una adecuada definición de caso en el script de caso de LAMMPS. En cualquier caso, independiente del enfoque de simulación empleado, es necesario llevar a cabo un proceso de selección de los parámetros de simulación de los pares de estilo seleccionados para llevara a cabo la simulación de flujo viscoelástico. A continuación se presentan algunos detalles específicos del tratamiento que debe hacerse en cada uno de estos enfoques.
Como caso de contexto de implementación de un modelo clásico viscoelástico se presentaran algunos detalles de la implementación directa del modelo de Kelvin. Para este fin se usaran los conceptos mostrados en la sección anterior (3) para generar un nuevo potencial de enlace disipativo que puede utilizarse para modelar sustancias viscoelásticos. Como se había indicado anteriormente, el modelo de Kelvin se usa para la modelación de materiales viscoelásticos mediante la suposición de un comportamiento equivalente al de un arreglo en paralelo de un componente tipo amortiguador puramente viscoso y de un componente tipo resorte puramente elástico conectados en paralelo, como se muestra en la .
Como se indicaba anteriormente, este arreglo de elementos disipativos y elasticos puede expresarse como: \[\label{eq:modeloKelvinV2} \sigma(t) = k \epsilon(t) + \eta \dot{\epsilon}\] donde el esfuerzo queda expresado como función de la deformación \(\epsilon\), la rata de deformanción \(\dot{\epsilon}\), y dos parámetros \(k\) y \(\eta\), representando la constante elástica y la constante disipativa viscosa, respectivamente.
Para llevar a cabo la implementación de un estilo de par, o de cualquier otra clase de hecho, es conveniente tomar como punto de partida una clase similar o asociada ya existente. Esta práctica es común en muchas aplicaciones de programación cientifica, con otro ejemplo extendido de esta práctica la programación de funciones o solucionadores personalizados usados en el programa , por mencionar un caso adicional. Para la implementación de este estilo de par personalizado se ha seleccionado como referencia la clase del estilo de par denominado , cuya estructura de implementación se presenta en , mostrado a continuación:
lst:bondHarmonicCppbondHarmonic.cpp
Como es costumbre, la nueva clase debe declararse e inicializarse mediante los archivos de encabezado, , y de código los que, en general, deberán guardarse en el directorio /src/MOLECULE de la carpeta de instalación de LAMMPS, y siguiendo una jerarquia como la mostrada en .
Todas las funciones serán las mismas que en el estilo de referencia. Sin embargo, en el nuevo estilo , tenemos que sustituir el texto “BondHarmonic" por un nuevo texto “BondKelvin", como puede verse en .
lst:bondKelvinCppbondKelvin.cpp
En comparación con el enlace de tipo armónico ahora se requiere de un nuevo parámetro, \(\eta\) o , desde el archivo de entrada. Por esta razón necesitamos modificar los métodos , , , , y .
Siguiendo el orden de inicialización de funciones (ver ), a continuación se muestran los códigos abreviados de estos métodos, en el orden presentado, para el estilo original y los códigos abreviados para el nuevo estilo .
lst:destructorBondHarmonicCppdestructorBondHarmonic.cpp
lst:destructorBondKelvinCppdestructorBondKelvin.cpp
El siguiente método a generar, siguiendo el orden presentado en , es . En el nuevo estilo de par se requiere la rata de deformación, \(\dot{\epsilon}\), que puede también considerarse como la velocidad de deformación. Para poder utilizar dicha variable dentro del nuevo estilo de pares necesitamos declarar e inicializar las velocidades de cada partícula, lo que constituye la modificación a ser hecha, en comparación con el estilo de par ármonico.
lst:computeBondHarmonicCppcomputeBondHarmonic.cpp
lst:computeBondKelvinCppcomputeBondKelvin.cpp
Dentro del bucle del método original, es necesario añadir un nuevo conjunto de líneas de código para calcular la fuerza del componente viscoso, así como las variaciones de energía. Este cáculo, que no se presenta en detalle, requiere de la estimación de velocidades de partícula y direcciones para cálculo de fuerzas del componente viscoso. Estas definiciones de variables se presentan en .
lst:computeVelsAndDirsBondKelvinCppcomputeVelsAndDirsBondKelvin.cpp
de manera que ahora es posible escribir las expresiones modificadas para el cálculo de la fuerza aplicada a cada par de partículas, como se presenta a continuación. Como en los caoss anteriores se presenta el código resumido para el estilo de par de referencia () y para el nuevo estilo de par ().
lst:computeForcesBondHarmonicCppcomputeForcesBondHarmonic.cpp
lst:computeForcesBondKelvinCppcomputeForcesBondKelvin.cpp
Siguiendo la línea de trabajo anterior, y dado que fue necesario introducir un nuevo parámetro en el estilo de par, es necesario modificar el método para considerar la necesidad de reserva de nueva memoria dinámica, lo que se presenta a continuación.
lst:allocateBondHarmonicCppallocateBondHarmonic.cpp
lst:allocateBondKelvinCppallocateBondKelvin.cpp
El coeficiente del componente de disipación viscosa \(\eta\) (o el análogo a un elemento amortiguador), debe ser suministrado por el usuario mediante línea de código en el archivo de configuración de caso de LAMMPS, por lo que es necesario también hacer ajustes enla definición del método , que se presenta en .
lst:coeffBondHarmonicCppcoeffBondHarmonic.cpp
lst:coeffBondKelvinCppcoeffBondKelvin.cpp
El estilo de par de referencia tiene también las funciones y que tienen que ser modificadas. Estas funciones de tipo escriben y leen un archivo de geometría que puede utilizarse como archivo de soporte en el archivo de entrada o de configuración de caso LAMMPS. El código de referencia y su respectiva modificación para estos métodos se presentan en
En el archivo encabezado del nuevo estilo de par se debe sustituir el texto “BondHarmonic" por un nuevo texto “BondKelvin", así como declarar un nuevo miembro protegido en la clase, el puntero a .
lst:bondHarmonicHbondHarmonic.h
lst:bondKelvinHbondKelvin.h
Finalmente, dado que el nuevo estilo debe invocarse desde el script de configuración de caso de LAMMPS, y ya que el nuevo estilo de par está completamente programado, es necesario compilarlo y luego invocarlo escribiendo las siguientes líneas de comando en el script de configuración de caso: