LLM 101 Introducción

Vamos a hacer una serie de preguntas rápidas para ayudar a los alumnos a empezar a trabajar con modelos grandes del lenguaje o LLM (Large Language Model)

¿Que es un modelo grande del lenguaje o LLM del inglés Large Language Model?

Es una red neuronal profunda (con muchas capas) diseñadas para procesar lenguaje natural (NLP del inglés natural language processing). Esto es, entender y generar lenguaje natural escrito. Como todas las redes neuronales, hay que entrenarlas. Una LLM se entrena con mucho texto y «aprende» patrones complejos del lenguaje.

¿Por qué se han puesto de moda?

Hace unos años, unas arquitecturas llamadas Transformer y sobre todo, el entrenamiento masivo con toda la información que hay disponible en Internet ha hecho que los LLM tengan unas prestaciones increíbles en tareas concretas, mas allá de las que originalmente fueron diseñadas. chatGPT (Generative Pre-trained Transformer) de OpenAI y BERT (Bidrectional Encoder Representations form Transformer) de google son las arquitecturas mas famosas. Una descripción de la arquitectura transformer puedes verla aquí

¿Pero un LLM es un programa que ejecuto en mi ordenador?

No exactamente, es un archivo binario que contiene la configuración de la red neuronal después del entrenamiento, esto es, los «pesos» de cada neurona, de cada capa, calculados en el proceso de aprendizaje.
Los modelos pueden tener distintos formatos, generalmente indicados en el nombre. Aunque no hay un estándar de nombrado, se detectan patrones en el nombrado de los modelos. Por ejemplo, SynthIA-7B-v2.0-16k-AWQ es un modelo entrenado con 7B, siete billones de parámetros, en su versión 2.0 y con un tamaño de contexto de 16K. La AWQ indica el formato y la precisión en el cual se han almacenado los «pesos» de cada neurona.

¿Qué es el tamaño del contexto?

Una cosa es el modelo LLM y otra cosa son sus parámetros. El tamaño del contexto es el número máximo de tokens (palabras) que le puedes pasar al LLM. Lo define la arquitectura del LLM y generalmente, a mayor tamaño del contexto, mejor se puede especificar la tarea que le pides al LLM por lo que generará cosas mas precisas. También es cierto que generalmente, implica mas memoria. Es el prompt que le metes a chatGPT para que te responda.

Vale, pero ¿Cómo ejecuto un LLM en mi máquina?

Bien, tienes que cargar el modelo binario en memoria para poder introducirles contexto (el famoso prompt) y obtener lo que genera. Los modelos pueden ser muy grandes por lo que los requisitos hardware son exigentes. Dicho esto, hay librerías en varios lenguajes de programación que te permiten cargar en memoria e interactuar con LLMs mediante un sencillo programa.
Uno de los mejores recursos para trabajar en este campo y en IA en general es la web https:/huggingface.co , que tiene librerías, modelos, datasets, papers, etc. la guía de uso de esta página es un buen primer comienzo.

¿Me puedes poner un ejemplo?

El código python de mas abajo carga un modelo un modelo LLM de código abierto modesto, que se baja de la web https://huggingface.co/ e interacciona con él introduciéndoles prompts (el contexto que hablábamos antes) y obteniendo las respuestas. Realmente trata de predecir la siguiente palabra al prompt.

import time
import torch
import transformers
import accelerate

from transformers import AutoTokenizer, AutoModelForCausalLM

print("Loading model...")
print("transformer version: ", transformers.__version__)
print("accelerate version: ", accelerate.__version__)
timeStart = time.time()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device:", device)

hugginfacemodelname = "openchat/openchat-3.5-1210"
tokenizer = AutoTokenizer.from_pretrained(hugginfacemodelname)
model = AutoModelForCausalLM.from_pretrained(
    hugginfacemodelname,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
).to(device)

print("Load model time: ", -timeStart + time.time())

while True:
    input_str = input("Enter: ")
    input_token_length = input("Enter length: ")
    if input_str == "exit":
        break
    timeStart = time.time()
    inputs = tokenizer.encode(input_str, return_tensors="pt").to(device)
    outputs = model.generate(
        inputs,
        max_new_tokens=int(input_token_length),
    )
    output_str = tokenizer.decode(outputs[0])
    print(output_str)
    print("Time taken: ", -timeStart + time.time())

El código utiliza la librería python transformers y torch. En primer lugar se indica el modelo LLM a cargar, openchat/openchat-3.5-1210 y se genera el tokenizer que no es sino una herramienta que convierte el texto en tokens (palabras). Para el modelo que hemos especificado, la función «from_pretained» busca y carga el tokenizador correspondiente. Si se lo tiene que bajar, esto puede tardar. A continuación, con AutoModelForCausalLM.from_pretained.., se carga el modelo de lenguaje causal, un tipo de modelo que predice la siguiente palabra en una secuencia.

¿Hay mas tipos de modelos?

Si, en esa librería hay modelos específicos para pregunta-respuesta, para clasificación de imágenes, multimodales, etc. al cual le tienes que indicar qué modelo quieres entrenar.

¿El prompt o la entrada del LLM cómo se articula?

Asociado al concepto de LLM aparece el concepto de prompt engineering o ingeniería de prompts. Es importante destacar que un buen prompt o cadena de prompts puede mejorar mucho la salida de una determinada LLM en una tarea específica. En los últimos años aparece una gran cantidad de métodos para elaborar prompts:
Referencias: https://dl.acm.org/doi/pdf/10.1145/3560815

¿ Puedo entrenar un LLM en lo que yo quiera?

En principio si. Entrenar un LLM desde cero requiere de muchos recursos. La mayoría de los ingenieros hacen un proceso denominado finetunning que no es otra cosa que coger un modelo LLM ya entrado de forma genérica y aplicarles un proceso de entrenamiento con un conjunto de datos específico de la tarea donde quiero usar mi LLM. El resultado es un modelo LLM que tiene el conocimiento general y se ha especializado mediante lo que ha aprendido con los patrones del dataset con el cual has hecho el finetunning. Un ejemplo muy bueno que aparece en los vídeos de formación de google relacionados con LLM, es ver el finetunning similar al proceso de enseñarle a una mascota, por ejemplo un perro, una serie de tareas específicas (buscar droga, personas, órdenes, etc.). Tienes el perro, siendo en este simil, la LLM generalista, y mediante un proceso de entrenamiento (finetunning), obtienes una LLM que es mejor que la generalista en un proceso específico.

Otra técnica muy adecuada es el Retrieval Augmented Generation o RAG, que no es sino mejorar las respuestas de LLM en un dominio específico proporcionándole información específica de ese dominio. Lo que se hace es recuperar información relevante (de fuentes internas o externas) para la pregunta e introducirla en el contexto/prompt junto con la pregunta para obtener mejores respuestas. Realmente todo este proceso de recuperación lo que hace es mejorar el prompt.

Finetunning

En este enlace podemos ver los principios básicos de finetunning:
hay tres aproximaciones al finetuning

  1. Self-supervised learning
  2. Supervised learning
  3. Reinforcement learning

¿Por qué ocupa tanto una LLM?

Una red neuronal almacena pesos calculados en el proceso de entrenamiento, como toda
variable, esos pesos pueden almacenarse en diverso formatos (punto flotante vs entero) y con determinada precisión (64,32,16 bits). La precisión es importante por que es lo que almacena los patrones aprendidos.

¿Qué puedo hacer para reducir el tamaño de una LLM?

Hay un proceso, denominado cuantización, que pasa de una determinada precisión a otra. Si pasas de una precisión en punto flotante de 16 bits a un entero de 8 bits, esto hace que el modelo ocupe menos en el disco duro a costa de perder información/patrones. Es decir, el modelo es algo peor. Hay dos tipos de cuantización, simétrica y asimétrica Mas info: https://myscale.com/blog/es/quantization-for-llms-finetuning/

En el siguiente ejemplo podemos ver un ejemplo básico de finetunning para evaluar sentimiento en comentarios de películas. El ejemplo es muy sencillo pero completo, en primer lugar se evalúa con un pequeño testbench la LLM escogida, una de las mas sencillas, se entrena con un dataset de comentarios etiquetados y se vuelve a evaluar para ver si ha «aprendido». https://towardsdatascience.com/fine-tuning-large-language-models-llms-23473d763b91

¿Cómo evaluamos un LLM?

Cómo sabemos si una LLM es buena o malo y qué podemos probar para ver si hemos mejorado. Bueno, aquí entra en juego la evaluación de las redes neuronales en general. Es decir, evaluar las prestaciones en base a una tarea específica pasa inherentemente por poder evaluar cómo de bueno son los resultados obtenidos ante prompts especificos.

Dos paradigmas populares, para la evaluación genérica de LLMs:

  1. El tradicional NLP Benchmark approach usando, por ejemplo, MosaicML’s Eval Gauntlet.
  2. Otra técnica muy utilizada es , precisamente, utilizar una LLM como juez.

¿Qué LLM es mejor?

Con la aparición de múltiples LLMs han aparecido numerosas métricas y rankings en diferentes testbench. Aquí puedes ver el ranking de hugging face: link

Herramientas y recursos

https://pinokio.computer/ una WEBUI para instalar y lidiar con modelos de AI.
https://huggingface.co/TheBloke Distintos modelos y parámetros para poder ser ejecutados en máquinas mas modestas
https://github.com/jmorganca/ollama herramienta para ejecutar LLM de forma local y hacerle un wrapper python que te permita adaptar los prompts/salidas.

Terminología:

Cuantización: Reduce la precisión de los números utilizados para representar los pesos en el modelo, pierdes precisión pero el tamaño y la inferencia se acelera sin necesitar tantos recursos.
GPTQ : formato de cuantización
-AWQ : formato de cuantización
-GGUF : formato de modelos que reemplaza a GGML.
Tamaño del contexto: la cantidad de tokens (normalmente palabras ) que un modelo puede considerar en un prompt de una vez cuando genera una respuesta o predicción.
Formatos de los modelos models in open AI book
– Destilación (Distillation) es el proceso de entrenar un modelo pequeño (llamado estudiante) para emular las respuestas de un modelo mas grande (profesor).
-Pruning es el proceso de elminar las salidas no críticas del modelo pequeño o estudiante.
-Quantization es el proceso de reducir la precisión de los pesos del modelo y las activaciones.

Bibliografía

Open source LLM
XGen-7B Technical Report un LLM open source con un prompt de hasta 8K

Emulación en NS3

El proceso de simulación en el simulador NS3 es el más ampliamente utilizado. Ya lo hemos visto en entradas anteriores. Una característica menos conocida y menos utilizada que te permite NS3 es la emulación. Efectivamente, NS3 te permite crear un escenario (nodos, pila de protocolos y aplicaciones) y que el tráfico generado por ese escenario «salga» al mundo real. Por ejemplo, podemos crear un sensor en ns3 que genere tráfico de forma que el servidor que recoge la información no distinga si es un sensor real o no. Las aplicaciones de la emulación son muy numerosas, por ejemplo, para testear servicios software reales , probar configuraciones, crear perfiles de tráfico, etc.

Vamos a ver un ejemplo de cómo crear este tipo de escenarios, y luego iremos viendo cosas concretas. Nuestro objetivo es generar tráfico LoraWan de un nodo a una pasarela en NS3 y que ese tráfico salga por una interfaz virtual que se crea en el ordenador que ejecuta la simulación.

La parte de la simulación LoraWan ya la hemos visto y tenemos un ejemplo en los repositorios de este blog. Sigue las instrucciones para hacer el ejemplo si no lo has hecho todavía.

Vamos a partir de ese ejemplo de LoraWan básico que constaba de un nodo lora representando un sensor y una pasarela o hub que recibía ese tráfico.

Bien, vamos a coger ese ejemplo y vamos a modificarlo para que ese tráfico lora salga por una interfaz de red virtual que se crea en el ordenador que ejecuta la simulación. Para ello necesitamos dos cosas, una es añadir un nodo que denominaremos «fantasma» que recoja el tráfico de la red que llega a la pasarela LoraWan y la saque por la interfaz virtual. El otro elemento que necesitamos es una aplicación que reciba el tráfico de la interfaz LoRa y lo reenvíe por una interfaz CSMA (Ethernet). Esa aplicación la instalaremos en la pasarela LoraWan y le añadiremos una interfaz CSMA. Al otro extremo de la interfaz CSMA conectaremos el nodo ghost que será al que tendremos que conectar la interfaz virtual del tipo «TapBridge».

Para ello, creamos una interfaz tapBridge, con su asistente:

TapBridgeHelper tapBridge;
tapBridge.SetAttribute ("Mode", StringValue ("ConfigureLocal"));
tapBridge.SetAttribute ("DeviceName", StringValue ("virtualtwin"));

Ahora vamos a crear un asistente para crear una red Ethernet que conecte el Ghostnode, y el hub que recoge el tráfico LoraWan, éste ghostnode será el que cree la interfaz virtual. Podríamos hacer que el hub que recoge el tráfico LoraWan creara la interfaz virtual, pero realmente los gateways/hub realmente recogen el tráfico y lo reenvian a una interfaz Ethernet y queríamos modelar también ese paso:


NodeContainer ghostNode;
ghostNode.Create(1);
ghostNode.Add(hub.Get(0));
CsmaHelper csma;
csma.SetChannelAttribute ("DataRate", StringValue ("100Mbps"));
csma.SetChannelAttribute ("Delay", TimeValue (NanoSeconds (6560)));

NetDeviceContainer hubDevice = csma.Install(ghostNode);

Básicamente se añade el hub (que ya lo habíamos creado previamente) al nuevo contenedor de nodos y se conectan todos a un canal csma. La expresión hub.Get(0) devuelve el nodo en la posición 0 del asistente de creador de nodos o contenedor de nodos, hub. Ese contenedor sólo tenía un nodo.

Asignamos la dirección IP en ese canal:

InternetStackHelper stack;
stack.Install (ghostNode);
Ipv4AddressHelper address;
address.SetBase ("10.0.0.0", "255.255.255.0");
Ipv4InterfaceContainer p2pInterfaces;
p2pInterfaces = address.Assign(hubDevice);

tapBridge.Install(hub.Get(0),hubDevice.Get(0));

y ya solo nos falta instalar las aplicaciones pertinentes, la primera es la aplicación que simulará el tráfico del sensor, ya la hemos visto:

PeriodicSenderHelper periodicSenderHelper;
periodicSenderHelper.SetPeriod (Seconds (12));
periodicSenderHelper.Install (nodes);

También necesitamos una aplicación que recoja toda la información que le llega a la interfaz lorawan del hub y la reenvíe por la interfaz csma que le hemos añadido. Por defecto, esa aplicación no existe, por lo que la hemos tenido que crear junto con su asistente para instalarlo. En otra entrada de blog la analizaremos, por el momento vamos a ver que se «instala» en el hub como otra aplicación cualquiera:

ForwardercdmaHelper ForwardercdmaHelper;
ForwardercdmaHelper.Install (hub);

Hemos incluido el código de esa aplicación (ForwaredercdmaHelper) en el ejemplo de esta entrada.

Al ejecutar la simulación podemos ver cómo se genera una interfaz «virtualtwin» mediante el comando ifconfig.

Created with GIMP

En esa interfaz sale todo el tráfico que mandemos al nodo ghost, entre ellos, el tráfico lorawan que hemos reenviado en el hub.

El ejemplo completo lo podemos encontrar en el directorio emulación del repositorio github

Creando un nuevo módulo en NS3

[Actualizado a cmake, versión 3.36.1 (documentación oficial ns3.36)]
Crear un nuevo protocolo o algoritmo en NS3 es un paso que implica la creación de un nuevo módulo. Obviamente, a partir del tipo de protocolo o algoritmo a implementar, los pasos a seguir y los protocolos que nos servirán como referencia cambiarán. En cualquier caso, siempre necesitaremos generar un nuevo módulo e incluirlo en la compilación de NS3 para poder hacer simulaciones con posterioridad.

Para crear un nuevo módulo, NS3 nos proporciona un script python, create-module.py, dentro de la carpeta src de nuestra instalación de ns3.
Si por ejemplo queremos crear un nuevo protocolo inalámbrico, pongamos el protocolo weightlessp, si ejecutamos:

$utils/create-module.py weightless
Creating module /home/felix/tools/ns-3-dev-git/contrib/weightless
Successfully created new modules
Run './ns3 configure' to include them in the build

Se nos crea la plantilla dentro de src:

$cd contrib/weightless/
felix@homer-esi:~/tools/ns-3-dev-git/contrib/weightless$ ls
CMakeLists.txt doc examples helper model test

donde vemos las carpetas habituales para meter la documentación, los ejemplos, los asistentes, el modelo propiamente dicho y los test. También me genera el archivo CMakeList.txt que utiliza ns3 para compilar los módulos. Para ver que se incluye correctamente en el proceso de compilación:

$./ns3 configure --enable-example --enable-test
$./ns3 build
$./test.py -s weightlessp
.
PASS: TestSuite weightlessp
1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
.

Ahora falta, obviamente, lo difícil, que es programar el modelo propiamente dicho identificando cada una de las partes. Lo más recomendable es estudiar cuidadosamente un protocolo existente de la misma capa que el que queremos programar y copiar su estructura adaptando el comportamiento a la funcionalidad deseada.

Simulando consumo y recolección de energía con LoraWan en NS3 (y III)

Vamos a ver un ejemplo completo de simulación de consumo y recolección de energía con LoraWan en NS3. La idea es simular un nodo LoraWan al cual le añadiremos un módulo recolector de energía (BasicEnergyHarvesterHelper). El ejemplo que usaremos en esta entrada se encuentra en lorawan-energy.cc. Haremos especial énfasis en la parte de la energía. No obstante, vamos a ir describiendo paso a paso, cada bloque en el ejemplo.
Lo primero que tenemos es la creación de los nodos. Crearemos un dispositivo final en LoraWan y una pasarela. El ejemplo está preparado para ampliar sus capacidades por lo que podríamos poner el número de nodos finales que queramos como argumento. Tal y como vimos en la entrada relativa a los argumentos de entrada.
Volviendo a la creación de nodos:

NodeContainer endDevices;
endDevices.Create(numendDevices);
MobilityHelper scenario = createscenario(radio);
scenario.Install (endDevices);
//put the hub in 0.0 0.0 0.0 center
NodeContainer hub;
hub.Create(1);
MobilityHelper hubposition = createstarcenter();
hubposition.Install (hub);

Se crea un contenedor de nodos con numendDevices, por defecto igual a uno, y se posicionan con un radio igual a 20. Utilizamos para ello la función createscenario. A continuación creamos otro contenedor de nodos para los nodos que hacen las funciones de pasarela o gateway/hub. En este caso, también uno, y lo posicionamos en el centro. La función createstarcenter y la función createscenario se encuentran al final del archivo. Ambas devuelven un MobilityHelper que, utilizando los contenedores, instalamos en los nodos finales endDevices y en los hub.
A continuación debemos poner un interfaz LoraWan en cada dispositivo final y en cada Hub o pasarela.
Para ello, creamos un canal LoraWan de acuerdo a la documentación del módulo:

Ptr <logdistancepropagationlossmodel> loss = CreateObject<logdistancepropagationlossmodel> ();
loss->SetPathLossExponent (3.76);
loss->SetReference (1, 7.7);
Ptr<propagationdelaymodel> delay = CreateObject<constantspeedpropagationdelaymodel> ();
Ptr<lorachannel> channel = CreateObject<lorachannel> (loss, delay);

Estos parámetros de retardo y pérdida simulan la propagación de un canal usando la codificación LoRa. Ahora hay que crear las interfaces Lora, conectadas al canal y las añadiremos a los nodos ya creados. Primero la interfaz LoRa para los dispositivos finales usando los asistentes:

LoraPhyHelper phyHelper = LoraPhyHelper ();
phyHelper.SetChannel (channel);
LorawanMacHelper macHelper = LorawanMacHelper ();
LoraHelper helper = LoraHelper ();
helper.EnablePacketTracking();
phyHelper.SetDeviceType(LoraPhyHelper::ED);
macHelper.SetDeviceType(LorawanMacHelper::ED_A);
NetDeviceContainer endDevicesNetDevices = helper.Install(phyHelper, macHelper, endDevices);

Como se puede observar, creamos la capa física y MAC, indicándole que es un dispositivo final mediante ED y ED_A respectivamente. Adicionalmente, hacemos lo mismo con la pasarela.

LoraHelper helperHub = LoraHelper ();
phyHelper.SetDeviceType (LoraPhyHelper::GW);
macHelper.SetDeviceType (LorawanMacHelper::GW);
helperHub.Install (phyHelper, macHelper, hub);
macHelper.SetSpreadingFactorsUp (endDevices, hub, channel);

Ahora indicando que es una pasarela.
Bien, el siguiente paso es configurar una aplicación, que instalaremos en los nodos finales y que mandará un paquete cada 5 segundos de un tamaño de 12 bytes:

PeriodicSenderHelper periodicSenderHelper;
periodicSenderHelper.SetPeriod (Seconds (5));
periodicSenderHelper.SetPacketSize (12);
ApplicationContainer appContainer = periodicSenderHelper.Install (endDevices);
double simulationTime = 3600;
Time appStopTime = Seconds (simulationTime);
appContainer.Start (Seconds (0));
appContainer.Stop (appStopTime);

Como podemos ver, configuramos el periodo y el tamaño de paquete para instalarlo en los dispositivos finales. A continuación indicamos que inicie en el segundo 0 y termine a la hora de comienzo mediante una variable, appStopTime que utilizaremos para configurar la simulación.
El siguiente paso que vamos añadir a nuestro dispositivo final es una fuente de energía, una pila de 200 mAh y luego configuraremos el consumo de acuerdo a un circuito final. Primero la pila:

BasicEnergySourceHelper basicSourceHelper;
LoraRadioEnergyModelHelper radioEnergyHelper;


// Bateria PD2032 200 mAh 3.7V
basicSourceHelper.Set ("BasicEnergySourceInitialEnergyJ", DoubleValue (2664)); // Energy in J
basicSourceHelper.Set ("BasicEnergySupplyVoltageV", DoubleValue (3.7));

Como podemos ver, 2664 julios a 3.7 voltios.
El consumo lo obtenemos de un modem semtech sx1276:

radioEnergyHelper.Set ("StandbyCurrentA", DoubleValue (0.0016));
radioEnergyHelper.Set ("TxCurrentA", DoubleValue (0.120)); //20 dbm
radioEnergyHelper.Set ("SleepCurrentA", DoubleValue (0.0000002));
radioEnergyHelper.Set ("RxCurrentA", DoubleValue (0.0115));
radioEnergyHelper.SetTxCurrentModel ("ns3::ConstantLoraTxCurrentModel","TxCurrent", DoubleValue (0.12)); //+20dbm
EnergySourceContainer sources = basicSourceHelper.Install (endDevices);
DeviceEnergyModelContainer deviceModels = radioEnergyHelper.Install
(endDevicesNetDevices, sources);

Con esta configuración, nuestro dispositivo transmitiría hasta que terminara la simulación o hasta que la batería no suministrara suficiente energía.
Añadimos un simulador de un recolector de energía, que de forma periódica recolecta un valor aleatorio de energía:

BasicEnergyHarvesterHelper basicHarvesterHelper;
basicHarvesterHelper.Set ("PeriodicHarvestedPowerUpdateInterval", TimeValue (Seconds (1.0)));
basicHarvesterHelper.Set ("HarvestablePower", StringValue ("ns3::UniformRandomVariable[Min=0.0|Max=0.009]"));
EnergyHarvesterContainer harvesters = basicHarvesterHelper.Install (sources);

Podemos ver, que se configura una recolección periódica de un segundo con un valor aleatorio entre 0 y 0.09 julios.
Finalmente configuramos el navegador como en cualquier otra simulación:

Simulator::Stop (appStopTime);
Simulator::Run ();
Simulator::Destroy ();

El ejemplo tiene mas código relacionado con extraer resultados por línea de comando y por gnuplot, esta parte la veremos en otra entrada. Si ejecutamos el ejemplo, vemos que hay una serie de resultados relacionados con la energía restante en la pila.

$ ./waf --run lorawan-energy

Una vez ejecutada la simulación, tenemos un archivo gnuplot-energy-example.sh que se ha generado junto con los datos de la simulación, le damos permisos de ejecución, lo ejecutamos y nos genera una gráfica con la evolución de la energía restante en el nodo final durante la hora de simulación:
Energía restante con recolector de energía
Donde podemos ver cómo la recolección de energía mitiga el gasto energético asociado al envío de paquetes. Si vemos cómo sería la gráfica sin el recolector de energía, basta con comentar esa parte, volver a simular y obtener la gráfica:

Energía restante con recolector de energía

Donde podemos ver cómo la energía restante no se repone en ningún momento.
Es una buena técnica jugar con los valores para observar su influencia en la energía restante en la pila del dispositivo final.

Simulando consumo de energía en NS3 (II)

Seguimos explicando el modelo de simulación del consumo de energía. Ya explicamos en la primera entrada relativa a la energía los conceptos básicos modelados en NS3. Seguimos trabajando con el ejemplo básico examples/energy/energy-model-example.cc

Si observamos el ejemplo, existen dos funciones añadidas justo antes de la función principal, RemainingEnergy y TotalEnergy:

/// Trace function for remaining energy at node.
void
RemainingEnergy (double oldValue, double remainingEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Current remaining energy = " << remainingEnergy << "J");
}

/// Trace function for total energy consumption at node.
void
TotalEnergy (double oldValue, double totalEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Total energy consumed by radio = " << totalEnergy << "J");
}

Estas funciones, como podemos ver, imprimen la energía que queda en la fuente de energía y el total de energía consumida por la radio.

Para invocar estas funciones cuando existe un cambio en la energía restante y/o energía consumida, debemos enlazar las funciones anteriores a los cambios de estado de la fuente de energía y del modelo de energía consumido respectivamente. De esta forma, obtenemos primero una referencia a la fuente de energía con ID 1. A continuación, mediante la función TraceConnectWithoutContext, vinculamos el cambio de estado en RemainingEnergy a una llamada a la misma función, mediante MakeCallBack y un puntero a la función deseada, en este caso RemainingEnergy también:

Ptr basicSourcePtr = DynamicCast (sources.Get (1));
basicSourcePtr->TraceConnectWithoutContext ("RemainingEnergy", MakeCallback (&RemainingEnergy));

A continuación se sigue el mismo proceso obteniendo el modelo de consumo de energía de un dispositivo:

// device energy model
Ptr basicRadioModelPtr =
basicSourcePtr->FindDeviceEnergyModels ("ns3::WifiRadioEnergyModel").Get (0);
NS_ASSERT (basicRadioModelPtr != NULL);
basicRadioModelPtr->TraceConnectWithoutContext ("TotalEnergyConsumption", MakeCallback (&TotalEnergy));

De esta forma se tracean los cambios de estado en las fuentes de energía, que simulan baterías, modelos de energía, que simulan el consumo de las tarjetas de red, y los recolectores de energía que simularían paneles solares, viento, etc.

La función TraceconnectWithoutContext conecta una fuente de trazas (es decir, algo que representa un estado dentro de NS3 y que puede cambiar) con una función. Identificaremos estas fuentes de trazas por que son variables de la plantilla TracedValue, por ejemplo, puedes ver en la definición BasicEnergySource tiene una variable TracedValue m_remainingEnergyJ; que es susceptible de ser traceada mediante una función asociada con TraceconnectWithoutContext asocíandole una función que tenga como argumentos el valor antiguo y nuevo. El tipo de argumentos en este caso será double. La documentación de cada clase proporciona qué TracedSources tiene para usar este mecanismo (e.g. BasicEnergySource).

Argumentos de entrada en simulaciones de NS3

Para ahorrarte trabajo, puedes configurar mediante argumentos de entrada muchos parámetros o atributos de tu simulación, tanto de los módulos que usas de NS3, como definir tus propios parámetros. Por ejemplo, si vas a simular el tráfico en una red lorawan con topología en estrella, con la pasarela en el medio, puedes poner como parámetro configurable el número de nodos o el radio de la estrella. Este ejemplo es el que vamos a utilizar para explicar cómo gestionar los argumentos de entrada. El archivo que usaremos es el lorawan.cc del repositorio. Necesitarás instalar el módulo de lorawan como indicamos aquí

Si copiamos el archivo lorawan.cc al directorio scratch de nuestra instalación NS3 podemos ver qué argumentos podemos definir por la linea de comandos mediante la opción –PrintHelp, esto es:

$ ./waf --run "lorawan --PrintHelp"
lorawan [Program Options] [General Arguments]
[...]
Program Options:
--radio: Radio of the disc where random put the nodes [20]
--numnodes: Num. nodes in the grid for simulating [20]


General Arguments:
--PrintGlobals: Print the list of globals.
--PrintGroups: Print the list of groups.
--PrintGroup=[group]: Print all TypeIds of group.
--PrintTypeIds: Print all TypeIds.
--PrintAttributes=[typeid]: Print all attributes of typeid.
--PrintHelp: Print this help message.

Como podemos ver, después de los mensajes de compilación (sustituidos por […]), podemos ver que el programa lorawan tiene definidos dos opciones, el radio y el numnodes, indicando el radio de la estrella y el número de nodos respectivamente. El número por defecto en ambos casos y si no indicamos esas opciones es 20. Si queremos simular 3 nodos en una topología en estrella con un radio de 10 metros, solo tendremos que indicarlo en línea de argumentos:
$ ./waf --run "lorawan --numnodes=3 --radio=10"
[...]

Si analizamos el archivo lorawan.cc, la definición de argumentos es relativamente sencilla, en primer lugar se indica que vamos a parsear la línea de comandos en busca de atributos y se definen las variables que almacenarán los valores que se pasen por línea de comandos:

#include "ns3/command-line.h"
[...]
double radio = 20.0;
int numnodes = 20;

A continuación se definen los valores esperados en línea de argumentos, en nuestro ejemplo, lo hacemos mediante una función que definimos antes del main y añadimos al final. En esta función creamos la variable que almacenará y parseará los argumentos y añadimos nuestros dos argumentos esperados mediante AddValue:

CommandLine setupcmd(){
CommandLine cmd;
cmd.AddValue ("radio", "Radio of the disc where random put the nodes", radio);
cmd.AddValue ("numnodes", "Num. nodes in the grid for simulating",numnodes);
return cmd;
}

como puedes ver indicamos el nombre, una pequeña ayuda y las variables que van a almacenar los valores.
Finalmente, parseamos la línea de comandos:

[..]
CommandLine cmd =setupcmd();
cmd.Parse (argc, argv);
[..]

Con lo que en las variables definidas a tal efecto, guardaremos los valores que le pasemos.

Aparte de poder definir nuestros propios argumentos de entrada para configurar nuestras simulaciones, muchos módulos que usamos tienen sus propios atributos configurables por líneas de comandos. Por ejemplo, si queremos ver un listado de todos los módulos disponibles, podemos verlos con la opción –PrintTypeIds

./waf --run "lorawan --PrintTypeIds"
Waf: Entering directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.112s)
Registered TypeIds:
ns3::A2A4RsrqHandoverAlgorithm
ns3::A3RsrpHandoverAlgorithm
[..]
ns3::LoraChannel
ns3::LoraInterferenceHelper
ns3::LoraNetDevice
ns3::LoraPhy
ns3::LoraRadioEnergyModel
ns3::LoraTag
ns3::LoraTxCurrentModel
[...]

En esa larga lista, podemos explorar qué atributos son configurables mediante la opción –PrintAttributes y el identificador o nombre del módulo. Por ejemplo, si queremos saber qué atributos puedo especificar mediante línea de comandos del módulo de ns3 LoraRadioEnergyModel, podemos ejecutar:

$ ./waf --run "lorawan --PrintAttributes=ns3::LoraRadioEnergyModel"
Waf: Entering directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.063s)
Attributes for TypeId ns3::LoraRadioEnergyModel
--ns3::LoraRadioEnergyModel::RxCurrentA=[0.0112]
The radio Rx current in Ampere.
--ns3::LoraRadioEnergyModel::SleepCurrentA=[1.5e-06]
The radio Sleep current in Ampere.
--ns3::LoraRadioEnergyModel::StandbyCurrentA=[0.0014]
The default radio Standby current in Ampere.
--ns3::LoraRadioEnergyModel::TxCurrentA=[0.028]
The radio Tx current in Ampere.
--ns3::LoraRadioEnergyModel::TxCurrentModel=[0]
A pointer to the attached tx current model.



Donde podemos ver que podemos definir la corriente de envío, recepción, dormir, etc.

Permisos de archivos en GNU/Linux

Los permisos de acceso a los archivos en GNU/Linux limitan quién puede acceder a un archivo y qué puede hacer con ese archivo. Se define el perfil de propietario del archivo (el usuario que lo creó), el grupo al cual pertenece dicho archivo (que por defecto es al grupo del usuario que lo creó), y el resto de usuarios que no son ni el propietario, ni pertenecen al grupo al cual pertenece el archivo. Para cada uno de esos perfiles se define si el usuario puede acceder para leer el archivo, escribir en él o ejecutarlo.
¿Cómo saber qué permisos tiene un archivo?, bien, la forma más fácil es con el comando ls, indicando que te dé todos archivos, incluso los que empiezan por «.» con la opción -a. En la figura podemos ver un ejemplo con la opción l para que liste los archivos, y la h, si quieres que el tamaño salga en múltiplos cómodos de leer (si no, sale un número en bytes). En la figura podemos ver cómo interpretar toda la salida del comando ls -alh en un directorio con un único archivo (hola.txt).

Atendiendo a la información que muestra el comando leída de izquierda a derecha, podemos ver que el archivo hola.txt es un archivo, el propietario tiene permisos de lectura y escritura, el grupo y el resto de usuarios solo pueden leerlo, tiene un único enlace a dicho archivo (el 1 sin leyenda), lo creó el usuario alumno, pertenece al grupo de alumno, tiene 5 bytes, se creó el 18 de enero a las 11:56 y se llama hola.txt.
¿Cómo puedo cambiar los permisos de un archivo?, con el comando chmod, por ejemplo, si pudiéramos ejecutar el archivo hola.txt y quisiéramos darle permisos de ejecución, bastaría con chmod +x hola.txt

Comandos, opciones y argumentos en la consola GNU/Linux

Vamos a repasar los conceptos de comando, opciones y argumentos mientras continuamos gestionando archivos. Si tienes claro estos conceptos avanzarás más rápido en tu dominio de la consola.
¿Qué es un comando? es una orden que se ejecuta en el terminal y que ejecuta un programa con una funcionalidad determinada. Por ejemplo, la orden ls ejecuta el programa ls que lista por pantalla los archivos de un directorio.
¿Qué es una opción? es una indicación, que se pone a continuación del comando, para el programa que se ejecuta para que haga o muestre algo diferente. Generalmente, en su forma corta es una letra precedida por un guion, por ejemplo -l. En el comando ls, la opción -l lista los archivos en forma de lista con sus permisos, usuario, grupo, tamaño, fecha de creación y nombre. De esta forma, si queremos visualizar los archivos de esta forma, debemos añadir la opción -l a la orden ls: ls -l. Algunas opciones pueden tener un formato largo que es una palabra, precedida por dos guiones, ej. – -help. La opción – -help en la mayoría de los comandos imprime una ayuda básica con las opciones disponibles.
¿Qué es un argumento? es de nuevo una indicación, que generalmente se pone a continuación del comando y las opciones, sobre qué directorio o archivo hay que ejecutar el programa indicado. Por ejemplo, si queremos listar los archivos de un directorio concreto, lo tenemos que indicar al final de la orden. ls -l /home/usuario/directorio listará en forma de lista, los archivos del directorio /home/usuario/directorio.

Si no indicas ninguna opción y/o argumento, el programa tiene habilitadas unas opciones por defecto que no necesitan indicarse. El comando ls por defecto lista los archivos del directorio donde se ejecuta.
La estructura de archivos y directorios tiene forma de árbol donde el «tronco» sería el elemento raiz o directorio inicial / y luego está formado por subdirectorios del sistema operativo y, dentro del directorio «home», cada usuario tiene un directorio para sus archivos personales.
En la figura de arriba fíjate que hay dos formas equivalentes de indicar que liste los subdirectorios y archivos del directorio uclm.
El comando man es el comando de ayuda del terminal y si le proporcionas como argumento el comando que sea, te mostrará la ayuda de ese comando (si tiene ayuda).

Comandos básicos de la consola, gestión de archivos (Debian 10: Bash)

Entre los comandos más habituales que ejecutamos en la consola nos encontramos todo lo que tiene que ver con la gestión de archivos y directorios. El directorio inicial o raiz viene representada por la barra / y es equivalente al directorio C:\ de windows. A partir de esa ruta inicial se crea una estructura de directorios y archivos con sus características asociadas.

Con el comando ls vemos los archivos y directorios del directorio que le indiquemos. Si no ponemos nada, por defecto, lista los directorios y archivos del directorio donde se ejecuta.

Se utiliza la barra / para separar cada directorio, de esta forma, /home/alumno/ejemplo/ es un directorio home que se encuentra en el directorio raíz y dentro del cual hay un directorio llamado alumno que a su vez alberga un directorio ejemplo. En bash, el punto «.» representa el directorio actual donde se encuentra la consola, los dos puntos «..» representa el directorio anterior en la ruta al cual está la consola, y el símbolo de la ñ «~» representa el directorio de usuario actual, por ejemplo, si el usuario es alumno, la ~ representaría la ruta /home/alumnno
Con el comando «cd DIRECTORIO» nos vamos al directorio que le indiquemos a continuación como argumento.
De esta forma, si nos encontramos en el directorio /home/alumno, y queremos ir al directorio ejemplo que se encuentra en dicho directorio, podemos especificar la ruta de varias formas:

  1. La ruta relativa al directorio donde se ejecuta el comando: cd ejemplo
  2. La ruta absoluta: cd /home/alumno/ejemplo
  3. La ruta relativa al directorio actual indicado explícitamente: cd ./ejemplo
  4. La ruta relativa al directorio padre del actual: cd ../alumno/ejemplo

Con el comando «mkdir DIRECTORIO» creamos un directorio o directorios si no existen. Por defecto lo creamos en el directorio actual pero se pueden especificar rutas completas. Creemos el directorio ejemplo2, pondremos varias formas para afianzar el concepto de ruta:

  1. mkdir ejemplo2
  2. mkdir /home/alumno/ejemplo2
  3. mkdir ~/ejemplo2

En la figura de abajo podemos ver un ejemplo creando un directorio uclm con la facultad esi dentro de ella y el edificio fermin-caballero, a su vez, dentro de la esi. Finalmente, con el comando cd, nos vamos al último directorio creado.