Mariano Reingart: Aplicación libre para recuento de votos / escrutinio: recuento_web2py


Recientemente he liberado una aplicación web, usada originalmente para el Recuento definitivo de las elecciones provinciales de Rio Negro 2011:


En el Primer Hackaton de Datos Públicos,  que incluyó juegos de datos de elecciones pasadas, trabajé en dicha aplicación para armar y publicar el código de la misma, a modo de demostración que sirviera para procesar los resultados de distintas elecciones.

La aplicación es básica y simple, permite consultar/cargar datos de las planillas (actas / telegramas de cada mesa) y tiene una página de resultados totalizados. Está escrita en el lenguaje de programación Python (framework web2py) y utiliza PostgreSQL como base de datos.

El esquema de base de datos ha sido generalizado para que pueda ser utilizado en cualquier tipo de elección, ya que soporta diferentes cargos, listas internas / partidos, y un árbol jerárquico de ubicaciones (soportando clasificaciones variadas, por ej. en País, Provincia, Departamento, Circuito y Mesa).

Mi idea de publicar el código fuente, fue más que nada por una cuestión educativa / experimental, para poder compartirlo con alumnos y otros interesados, ya que quizás sea de utilidad para tomarlo como base para analizar resultados de elecciones, hacer pruebas de carga y rendimiento, optimización de bases de datos, etc.

Hasta el momento, se procesaron los datos públicos de dos elecciones nacionales:
Esta subido a un repositorio público bajo licencia AGPLv3+ (licencia de software libre similar a la de Linux pero con consideraciones adicionales para aplicaciones web): https://github.com/MSA-Argentina/recuento_web2py
Instalación Rápida:
  • Descargar web2py desde: http://www.web2py.com/ 
  • Descargar la aplicación en un archivo comprimido desde GitHub: master.zip
  • Descomprimir la aplicaión en la carpeta application de web2py (por ej. bajo el nombre recuento)
  • Crear una base de datos en PostgreSQL y ejecutar el esquema.sql para crear las tablas
  • Seguir los pasos siguientes de cada juego de datos (ver carga.sql y migracion.sql)
  • Configurar la cadena de conexión a la base de datos en el modelo app_settings.py

Juegos de Datos Públicos


Ultimamente trabajé en procesar los datos de las P.A.S.O. 2013 -elección primarias para senadores y diputados nacionales en las 24 provincias-, publicados en el Portal de Datos Públicos

Para más información ver carpeta 2013-primarias:
El juego de datos es bastante interesante ya que está a nivel de mesa (fuente de la información), por lo que incluye 88.463 mesas procesadas con 1.846.369 registros de resultado (votos por mesa, por cada lista para los cargos de diputados + sendadores).
Recordar que es un recuento provisorio por lo que los datos no son definitivos y no están todas las planillas cargadas (90.670 mesas habilitadas en total). El total de electores habilitado fue de 30.573.183.
Para ver los datos oficiales, dirigirse a http://www.elecciones.gov.ar/ (recuento definitivo) y http://www.resultados.gob.ar (recuento provisional).

Manejo de Imágenes de actas de mesa (telegramas / faxes):


La página de Consulta de Telegrama / Planilla permite revisar, cargar y corregir los votos por cargo por lista en cada mesa escrutada.
Para el procesamiento de las imágenes se desarrollaron dos programas auxiliares (scripts):
  • faxes.sh: para extraer las imágenes del PDF y convertirlas al formato TIFF optimizado (CCITT grupo 4) se utilizaron las herramientas pdfimages y ppm2tiff
  • faxes.py: para incorporar las imágenes a PostgreSQL (campo BYTEA), usando python, psycopg2 (adaptador BINARY) y una consulta para determinar la mesa según el nombre de archivo.
NOTA: si se utiliza una versión antigua de PIL (Python Imaging Library), es necesario convertir las imágenes a PNG, perdiendo un poco de eficiencia en la compresión, pero ganando compatibilidad.

Para mostrar las imágenes, se obtiene la misma desde la base de datos y se genera una miniatura con PIL (ver thumbnail en definitivo.py) para ser descargada por el navegador web.

El tamaño total de la base de datos, incluyendo las imágenes de las planillas, es de 6.4 GB

Consultas no triviales con el "ORM" de web2py


Gracias a características declarativas del framework web2py, se pueden realizar consultas relativamente complejas y arbitrarias, en este caso con agrupaciones, tablas anidadas y criterios de búsquedas particulares, de manera programada en Python, sin necesidad de recurrir a escribir SQL puro.
A continuación se extrae un ejemplo de código para totalizar los votos dada una ubicación (por la URL se especifican los identificadores de ubicación, cargo y estado de las planillas):

# constantes (jerarquía de ubicaciones)
CLASES = ['Pais', 'Provincia', 'Departamento', 'Circuito', 'Mesa']

# alias de tablas:       

p = msa.planillas
d = msa.planillas_det
l = msa.listas

# armo la consulta base con los criterios de búsqueda
query = p.id_planilla == d.id_planilla
query &= d.id_lista == l.id_lista
query &= p.id_estado == request.args[3]
query &= d.id_cargo == request.args[0]
query &= l.positivo == True

# armo la consulta "recursiva" (árbol) para tener las ubicaciones y planillas

# (up -> ubicación padre, uh -> ubicación hija)
# p/ el alias de cada tabla se usa el nombre de la clase (depto, mesa, etc.)
up = msa.ubicaciones.with_alias(ubicacion.clase)
query &= up.id_ubicacion == id_ubicacion
for clase in CLASES[CLASES.index(ubicacion.clase)+1:]: 
      uh = msa.ubicaciones.with_alias(clase)
      query &= uh.id_ubicacion_padre == up.id_ubicacion
      up = uh
query &= p.id_ubicacion == up.id_ubicacion
        
# campo suma total:
suma_votos = d.votos_definitivos.sum()
    
# ejecuto la consulta:
resultado = msa(query).select( 
              d.id_lista.with_alias("id_lista"),
              l.nro_lista.with_alias("nro_lista"), 
              l.descripcion.with_alias("descripcion"), 
              l.idx_fila.with_alias("idx_fila"), 
              l.descripcion_corta.with_alias("descripcion_corta"),
              l.color.with_alias("color"),
              suma_votos.with_alias("suma_votos"),
              groupby=(d.id_lista |
                        l.nro_lista | 
                        l.descripcion |
                        l.idx_fila |
                        l.descripcion_corta |
                        l.color),
              orderby= ~suma_votos | l.idx_fila
             )


La salida puede observarse en la Página de Resultados por Ubicación  que permite obtener la suma total de votos agrupada por lista y cargo, para un País / Dpto / Mesa):
Esta consulta en Python generará una consulta SQL similar a la siguiente:

SELECT planillas_det.id_lista, listas.nro_lista, listas.descripcion, 
       SUM(planillas_det.votos_definitivos) AS suma_votos 
FROM planillas, planillas_det, listas, 
     ubicaciones AS provincia, 
     ubicaciones AS departamento, 
     ubicaciones AS circuito, 
     ubicaciones AS mesa 
WHERE planillas.id_planilla = planillas_det.id_planilla
  AND planillas_det.id_lista = listas.id_lista 
  AND planillas.id_estado = 'Publicada' 
  AND planillas_det.id_cargo = 1
  AND listas.positivo = 'T' 
  AND provincia.id_ubicacion = 1 
  AND departamento.id_ubicacion_padre = provincia.id_ubicacion
  AND circuito.id_ubicacion_padre = departamento.id_ubicacion 
  AND mesa.id_ubicacion_padre = circuito.id_ubicacion 
  AND planillas.id_ubicacion = mesa.id_ubicacion
GROUP BY planillas_det.id_lista, listas.nro_lista, listas.descripcion 
ORDER BY SUM(planillas_det.votos_definitivos) DESC, listas.nro_lista;

Consultas Recursivas con PostgreSQL:


La consulta anterior, podría escribirse de manera más flexible en PostgreSQL utilizando Expresiones de Tablas Comunes (CTE) para consultas recursivas (clausula WITH).
En el siguiente ejemplo, se utiliza un termino no recursivo para obtener la ubicación base (id_ubicacion =1), unido luego con el termino recursivo que busca las ubicaciones hijas de cada padre:

WITH RECURSIVE U(id_ubicacion, descripcion, clase) AS (
    SELECT UP.id_ubicacion, UP.descripcion, UP.clase 
      FROM ubicaciones UP 
     WHERE UP.id_ubicacion = 1
  UNION ALL
    SELECT UH.id_ubicacion, UH.descripcion, UH.clase
      FROM U, ubicaciones UH 
     WHERE UH.id_ubicacion_padre = U.id_ubicacion 
  )
SELECT L.id_lista, P.id_partido, L.nro_lista, L.descripcion, P.descripcion,
       SUM(PD.votos_definitivos) AS suma_lista
FROM U, planillas PL, planillas_det PD, listas L, partidos P
WHERE PL.id_ubicacion = U.id_ubicacion
  AND PL.id_planilla = PD.id_planilla
  AND PD.id_lista = L.id_lista 
  AND L.id_partido = P.id_partido
  AND PD.id_cargo = 2
GROUP BY P.id_partido, L.id_lista, L.nro_lista, L.descripcion, P.descripcion 
ORDER BY 6 DESC;

Si bien en este caso no cambia el rendimiento ni el resultado, podría ser útil para otras elecciones en que las jerarquías de ubicaciones no tengan los mismos niveles de agrupación (por ej., hay provincias que podrían estar divididas por secciones electorales, municipios, comunas, departamentos, con mayor o menor de detalle, generalmente hasta el establecimiento y mesa)

Es interesante ver con el comando EXPLAIN el análisis del plan de ejecución de la consulta para detectar posibles optimizaciones , como podría ser agregar el siguiente indice:

CREATE INDEX planillas_det_id_cargo_id_idx ON planillas_det(id_cargo, id_planilla)


Funciones de Ventana con PostgreSQL:


Otra característica de PostgreSQL que puede ser útil en estos casos, son las funciones que calculan en base a una partición de los datos (similar a un agrupamiento).
Por ejemplo, para obtener la cantidad de votos por cada partido (además de la suma de cada lista interna), se puede agregar la expresión SUM(SUM(PD.votos_definitivos)) OVER (PARTITION BY L.id_partido) AS suma_partido  a la consulta anterior:

WITH RECURSIVE U(id_ubicacion, descripcion, clase) AS (
    SELECT UP.id_ubicacion, UP.descripcion, UP.clase 
      FROM ubicaciones UP 
     WHERE UP.id_ubicacion = 1
  UNION ALL
    SELECT UH.id_ubicacion, UH.descripcion, UH.clase
      FROM U, ubicaciones UH 
     WHERE UH.id_ubicacion_padre = U.id_ubicacion 
  )
SELECT L.id_lista, P.id_partido, L.nro_lista, L.descripcion, P.descripcion,
       SUM(PD.votos_definitivos) AS suma_lista,
       SUM(SUM(PD.votos_definitivos)) OVER (PARTITION BY L.id_partido) AS suma_partido
FROM U, planillas PL, planillas_det PD, listas L, partidos P
WHERE PL.id_ubicacion = U.id_ubicacion
  AND PL.id_planilla = PD.id_planilla
  AND PD.id_lista = L.id_lista 
  AND L.id_partido = P.id_partido
  AND PD.id_cargo = 2
GROUP BY P.id_partido, L.id_lista, L.nro_lista, L.descripcion, P.descripcion 
ORDER BY 7 DESC, 6 DESC;

Cuyo resultado es:


Agradecimientos y trabajo a futuro


Esta aplicación fue posible gracias a la colaboración de la empresa MSA y al grupo de trabajo de datos públicos / software libre del foro de la Agenda Digital, por haber hecho posible la publicación del código fuente y el acceso a los datasets respectivamente.

En el futuro posiblemente publicaremos un análisis más exhaustivo, extendiendo el apunte sobre optimización de consultas y estadísticas en el sitio del Grupo de Usuarios de PostgreSQL Argentina: http://www.postgresql.org.ar/trac/wiki/OptimizarRendimiento

Seguramente se presentará el trabajo en la próxima jornada PostgreSQL: PgDay Argentina 2013, donde se preparará un taller para experimentar con los datos, analizar el rendimiento y proponer mejoras.

Hernán Grecco: Make your functions units-aware with Pint 0.3

Pint is Python package to define, operate and manipulate physical quantities: the product of a numerical value and a unit of measurement. It allows arithmetic operations between them and conversions from and to different units.

>>> from pint import UnitRegistry
>>> ureg = UnitRegistry()
>>> q1 = 24. * ureg.meter
>>> q2 = 10. * ureg.centimeters
>>> q1 + q2
<Quantity(24.1, 'meter')>
It provides a comprehensive and extensive list of physical units, prefixes and constants defined in a standalone text file. The registry can parse prefixed and pluralized forms of units resulting in a much shorter and maintainable unit definition list.

It also provides great NumPy integration, with implicit unit conversion and an emphasis on correctness.
>>> import numpy as np
>>> np.cos([0, 45] * ureg.degrees)
<Quantity([ 1. 0.70710678], 'dimensionless')>
>>> np.cos([0, 45] * ureg.meter)
Traceback (most recent call last):
...
pint.unit.DimensionalityError: Cannot convert from 'meter' to 'radian'

Wrapping your functions with Pint

Scientific code usually needs to interface with services, libraries or data structures in which physical parameters must be given in specific units. It is common to specify the units in the docstrings like this:
>>> def somefun(length, force):
... """
... :param length: length in meters.
... :param force: applied force in Newton.
... :return: elapsed time in seconds.
... """
... # Do something
Later in the code, you will have to remember the required units and make sure that you are doing the right conversions. This is very inconvenient and error prone. Pint 0.3 helps you to write units-aware functions:
>>> from pint import UnitRegistry
>>> ureg = UnitRegistry()
>>> @ureg.wraps('second', ('meter', 'newton'))
... def somefun(length, force):
... # Do something
and you will use it like this:
>>> somefun(200 * ureg.cm, 8e+5 ureg.dyne)
8 second
The first argument in the decorator factory indicates the return units and the second argument indicates the input units. The decorator will convert input quantities to the required units and then call the wrapped function using their magnitudes as arguments. The return value will be used as the magnitude of a new quantity with the specified output units.
This is very convenient to wrap functions from other packages. For example, the Polymode package provides the index of refraction as a function of the wavelength (given in micrometers) for a variety of optical materials.
>>> from Polymode import Material
>>> nbk7 = Material.BK7().index
>>> nbk7(.5) # The index of refraction for a wavelength given in micrometer
1.5214144757734767
Instead of remembering the input units, you can just wrap it using Pint:
>>> nbk7 = ureg.wraps(None, ['micrometer', ])(Material.BK7().index)
>>> nbk7(500 * ureg.nanometer)
1.5214144757734767

Calling the wrapped function with an argument with incompatible units (e.g. in this case, seconds) will raise a `DimensionalityError`. There is more information in the documentation.

IPython integration

Pint now integrates better with IPython providing pretty printing and autocomplete.

In the qtconsole:



and in the notebook:


Thanks to the people that contributed bug reports, suggestions and patches in this release. In particular to: Eric Lebigot, Nate Bogdanowicz, Daniel Sokolowski, 

Interested? Install it and give it a try!

Submit your bug reports, comments and suggestions in the Issue Tracker. There are already some ideas for version 0.4. Check them out, comment and add yours.

Read the docs: https://pint.readthedocs.org/
or fork the code: https://github.com/hgrecco/pint

alecu: Selección de charlas para PyCon

Este año tenemos solamente dos salas para PyCon Argentina 2013, asi que desde el comité de selección de charlas tuvimos que descartar charlas a lo loco. Otros años pudimos incluir el 60 o 70% de las propuestas, pero este año va a quedar menos del 50%.

Elegir las charlas es un proceso exhaustivo, y desde el comité revisamos cada una con respecto a la experiencia de los oradores, relevancia del tema y claridad de la propuesta. Y luego tuvimos algunas reuniones donde terminamos de decidir la grilla.

El criterio que usamos para evaluar a los oradores que nunca habían presentado charla en algún PyDay o en PyCon o que no conocíamos fue ver si tenían alguna charla grabada en internet. A los oradores que están empezando, nuestra recomendación es dar una Charla Relámpago durante el evento, y luego proponer la charla durante algún PyDay donde la revisión suele ser más permisiva.

También tuvimos que rechazar varias charlas que hablaban sobre algún framework o biblioteca, sobre todo si dicho código estaba publicado hace poco, significando que tenía pocos o inexistentes usuarios; y en algunas charlas también, si el código no estaba publicado. En ambos casos tratamos de priorizar lo que es general y útil a la mayoría de la audiencia, versus lo que es muy nuevo y poco usado. Estos temas recomendamos también presentarlos como Charla Relámpago durante la conferencia.

Y también rechazamos charlas de temas repetidos, y tratamos de minimizar la cantidad de charlas de cada orador. Salvo en casos muy puntales, cada orador aparece en una sóla charla.

Como siempre, es probable que nos hayamos equivocado, y muy probable también es que haya cosas para criticar. Los invito a hacerlo en los comentarios de este blog, pero sobre todo los invito también a participar en la organización de esta y las siguientes conferencias para que las cosas salgan más a su gusto.

De lo que si estamos seguros es que las charlas que elegimos van a armar una conferencia buenísima, asi que los esperamos en menos de dos meses en Rosario!

Alejandro Santos: Ping por Packet AX25

Así me está funcionando el ping por AX25, nada mal!
# ping -i 15 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_req=1 ttl=64 time=4002 ms
64 bytes from 10.0.0.1: icmp_req=2 ttl=64 time=2689 ms
64 bytes from 10.0.0.1: icmp_req=4 ttl=64 time=2851 ms
64 bytes from 10.0.0.1: icmp_req=5 ttl=64 time=3382 ms
64 bytes from 10.0.0.1: icmp_req=7 ttl=64 time=3258 ms
64 bytes from 10.0.0.1: icmp_req=9 ttl=64 time=2753 ms
64 bytes from 10.0.0.1: icmp_req=12 ttl=64 time=4070 ms
64 bytes from 10.0.0.1: icmp_req=13 ttl=64 time=3859 ms
64 bytes from 10.0.0.1: icmp_req=15 ttl=64 time=4178 ms
64 bytes from 10.0.0.1: icmp_req=17 ttl=64 time=4264 ms
64 bytes from 10.0.0.1: icmp_req=18 ttl=64 time=3541 ms
64 bytes from 10.0.0.1: icmp_req=20 ttl=64 time=4856 ms
64 bytes from 10.0.0.1: icmp_req=22 ttl=64 time=4018 ms
64 bytes from 10.0.0.1: icmp_req=23 ttl=64 time=5313 ms
64 bytes from 10.0.0.1: icmp_req=25 ttl=64 time=3723 ms
64 bytes from 10.0.0.1: icmp_req=27 ttl=64 time=2809 ms
64 bytes from 10.0.0.1: icmp_req=28 ttl=64 time=4391 ms
64 bytes from 10.0.0.1: icmp_req=29 ttl=64 time=6268 ms
64 bytes from 10.0.0.1: icmp_req=30 ttl=64 time=6058 ms
64 bytes from 10.0.0.1: icmp_req=31 ttl=64 time=4279 ms
64 bytes from 10.0.0.1: icmp_req=32 ttl=64 time=5625 ms
64 bytes from 10.0.0.1: icmp_req=33 ttl=64 time=3763 ms
64 bytes from 10.0.0.1: icmp_req=34 ttl=64 time=2911 ms
64 bytes from 10.0.0.1: icmp_req=35 ttl=64 time=2853 ms
64 bytes from 10.0.0.1: icmp_req=36 ttl=64 time=2718 ms
64 bytes from 10.0.0.1: icmp_req=37 ttl=64 time=2789 ms
64 bytes from 10.0.0.1: icmp_req=38 ttl=64 time=2944 ms
64 bytes from 10.0.0.1: icmp_req=39 ttl=64 time=3699 ms
64 bytes from 10.0.0.1: icmp_req=41 ttl=64 time=3798 ms
64 bytes from 10.0.0.1: icmp_req=44 ttl=64 time=2918 ms
^C
--- 10.0.0.1 ping statistics ---
44 packets transmitted, 30 received, 31% packet loss, time 645337ms
rtt min/avg/max/mdev = 2689.509/3819.856/6268.794/986.186 ms

Alejandro Santos: Medidor de ROE digital con Arduino

Arduino SWR Meter

Tal como adelanté en mi anterior entrada, una mejora al puente de ROE de XQ2FOD que le hice fue la de reemplazar los medidores analógicos por un display LCD controlado con un Arduino (ATmega328p).

Arduino SWR Meter

El circuito es demasiado simple como para dibujarlo, la idea es usar la librería LiquidCristal de Arduino y reemplazar en el circuito de XQ2FOD la entrada a los medidores analógicos por las entradas Analog0 y Analog1 en el Arduino. Se pueden ver más fotos en mi galería de Flickr.

El código fuente del programa cargado en el Arduino se puede encontrar acá. En programa se encarga de leer constantemente las entradas 0 y 1 analógicas, y hacer los cálculos de SWR (sacados del capítulo de Lineas de Transmisión del ARRL Handbook 2010).

p = sqrt(Pr/Pf);
SWR = (1+p) / (1-p);

Además como se puede ver en el codigo fuente, se calcula el promedio de los datos leídos para evitar que el display LCD parpadee cuando los datos de entrada fluctúen en el tiempo.

LU4EXT



Diego Sarmentero: Documentor 0.1

And for the third project of this week: Documentor!
This is the first Screencast of the 0.1 version of Documentor.
Documentor scan all the source code of your project and generate a documentation site with the collected information of your project using Nikola.

Diego Sarmentero: Tabu Party Game (Web + Ubuntu Client)

These last 2 days i've been working in this app: Tabu Party Game for the Ubuntu App Showdown Contest. You can create Languages and Cards for each Languages from the web, and Play it from the web (not ready yet) and from the Ubuntu Client for Mobile and Desktop.
This is a video with a complete demonstration of the UI, the web, how to create cards, etc, etc:

Alejandro Santos: La historia del puente de ROE XQ2FOD


Hace algunos años buscando cómo armar mi propio medidor de ROE me encontré este excelente circuito de XQ2FOD (link acá), donde el autor asegura que funciona en todas las bandas populares de Radio Aficionados, desde 80 metros hasta 1.3 Ghz. Sucede que para que funcione en frecuencias altas (y no tan altas también) debe estar bien construido, ya que cualquier error mecánico afecta la lectura del instrumento.

DSC01047b

Este fue la primer versión que armé, hace casi 20 meses, ignorando toda advertencia de XQ2FOD en el artículo inicial. El primer comentario que tuve fue que debía armarlo con los alambres lo más cortitos que se puedan, hasta el punto de que se le podían limar la pintura a las resistencias para no tener los alambres. En efecto, este medidor no servía y marcaba cualquier cosa en el instrumento.

Carga Fantasma 50 Ohm

Luego de armar una versión un poco más compacta (no mucho más), decidí que necesitaba una carga fantasma para poder medir el propio instrumento y ver si efectivamente marcaba 1:1 de ROE con 50 Ohm de carga. La anterior foto fue uno de los intentos de carga, donde la aguja se disparaba marcando bastante ROE. En efecto, la carga estaba muy mal hecha.

Carga Fantasma 50 Ohm - N

Luego de varios intentos, terminé con una carga como la que se puede ver en la foto. Ya con esta carga, la actual versión del puente de ROE hacía que el instrumento marque 1.0 de ROE, ¡todo un avance! Hace algunos meses encontré el blog de LU1AR donde describe una carga fantasma de baja potencia, de muy fácil construcción, que bien armada puede llegar hasta 1.3 Ghz sin problemas (ver acá). Tengo pendiente armar una de estas ya que no tengo una buena carga fantasma.

Puente de ROE XQ2FOD versión 2

Siguiendo con la idea de mantener las distancias de los alambres lo más cortas posible, terminé con un puente como en la imagen anterior. En este caso, el puente marcaba menos de 1.1 de ROE con mi carga artesanal en VHF.

Puente de ROE XQ2FOD versión 2

Además, la siguiente mejora fue, por un lado, reemplazar los diodos 1N34 originales por diodos 1N5711 (azules en la foto), cortesía de un colega Radio Aficionado. En teoría, estos dioditos deberían funcionar bien y sin problemas hasta UHF.

Arduino SWR Meter

Por el otro, otra mejora que le hice al circuito fue reemplazar los medidores de aguja analógicos originales por un microcontrolador (Arduino Atmega328), mostrando el valor del ROE (SWR en inglés) en un display de LCD. Pueden ver detalles de este proyecto acá.

 Esta última fue la versión que tuve armada durante casi un año, hasta que hace unos días decidí armar una mejor, que pueda llegar hasta microondas.

Puente de ROE XQ2FOD versión 3

Tal como comenta XQ2FOD en su artículo, utilizando componentes SMT se puede lograr un puente de ROE que permita medir hasta muy altas frecuencias. Es por esto que decidí armarlo de esta manera, llegando al primer prototipo que se puede ver en la imagen anterior. Usé los mismos diodos de siempre, y las resistencias son seis de 100 Ohm SMT apareadas en paralelo, obtenidas de un viejo circuito de impresora rota. La placa es Epoxy de 1.5mm, y del otro lado (donde no se ven en la foto) es puro cobre de plano a tierra, conectado también al chasis de los conectores.

En la foto se puede ver una linea de transmisión de casi 4 cm, esto fue la primer prueba, me sobró bastante circuito ya que los componentes son pocos, y decidí dejarla ya que me pareció que no iba a molestar. En la práctica esto no es cierto, el circuito debe ser del menor tamaño posible, y a menos que esté muy bien construido recién ahí la línea no debería molestar. Este no fue mi caso, no está bien construido, por lo que el siguiente paso fue reducir el tamaño del circuito. Midiéndolo con un analizador de antenas MFJ prestado de otro colega, el anterior puente marcaba 1.5 de ROE en VHF, un valor bastante alto para lo que debería ser el puente. Es por esto que decidí construirlo nuevamente utilizando la porción del circuito libre.

Puente de ROE XQ2FOD versión 4

En esta nueva versión usé resistencias nuevas (sin reciclar de otro aparato), donde además las resistencias son de mayor tamaño mecánico, permitiendo disipar más calor. Además, en vez de soldarle una carga fantasma de dudoso valor (en la foto anterior se pueden ver dos resistencias normales de 100 Ohm en paralelo en la punta), decidí usar una carga de 50 Ohm profesional. Esta carga de 50 Ohm que me prestaron fue medida profesionalmente y que mostró llegar con un ROE (casi) plano hasta 3 Ghz. Con la carga fantasma profesional, y el medidor de antenas MFJ de siempre, pude ver que mi puente de ROE tiene 1.1 de ROE en VHF. Un dato importante es que al medir el ROE de la carga directamente, el MFJ también marcaba 1.1 de ROE en VHF, por lo que es muy posible y probable que cualquier error que marque el MFJ en mi puente pueda también deberse a la falta de precisión del MFJ en altas frecuencias. Para estar seguro debo conseguir un segundo medidor que me permita medir en UHF, ya que esta versión del medidor MFJ solo llega hasta VHF. Además, le tengo infinita más confianza a la carga de 50 Ohm profesional que al MFJ.

Puente de ROE XQ2FOD versión 4.1

Arriba se puede ver la última versión del puente, donde el único cambio es el recorte del PCB al mínimo tamaño posible.

Puente de ROE XQ2FOD versión 4

Un experimento que decidí hacer fue conectar un acoplador direccional al puente de ROE. En teoría, el acoplador debería generar por la línea acoplada una señal en caso de que haya ROE en lo que esté conectado (el puente en este caso). Mediante mi osciloscopio decidí medir qué llegaba desde esta línea acoplada, y hasta donde pude ver, la pantalla del osciloscopio no se movía, dando a entender que el puente debería estar bien construido. Con el acoplador también se puede construir un medidor de ROE en una antena, sin embargo esto queda para otro momento, ya que a mi me interesaba tener el puente de XQ2FOD funcionando. Si quieren ver la historia completa de construccion del puente de ROE de XQ2FOD, se puede ver la colección de fotos en mi cuenta de Flickr, acá.

73 LU4EXT, Alejandro.