Finding the largest jacobian in a finite element mesh
Welcome to the wonderful world of C programming where you have to do everything yourself.

The C programming language is basically a high level assembly language, there is no garbage collection and the programmer has to manage the memory themselves.

The Good

Before I start discussing what can be improved, I'll mention the good things:
 - The code is quite readable
 - No casts from malloc() to the receiving variable
 - The code follows the Single Responsibility and KISS principles, lots of nice small functions, although the function struct Mesh generate_mesh(double* coordinates, int num_nodes, int num_elements) could have 2 sub-functions to implement it, each of the for loops could be a function.
 - There is only one Magic Number, and that one is forgivable, the 5000000 used to initalize the variable num_elements

Error Checking

The allocation functions malloc(), calloc() and realloc() have been known to fail. This happens less often now than historically because computers generally have more memory than they used to, but there are still cases of limited memory such as in embedded programming. 

When an allocation function fails it returns NULL. Reference through a NULL pointer causes unknown behavior, and generally the program will terminate without an explanation. It is a good habit to test the results of the allocation before using the pointer in code:

    double* coordinates = malloc(num_nodes * sizeof(double));
    if (!coordinates)
    {
        fprintf(stderr, "Allocation of coordinates failed, exiting program\n");
        exit(EXIT_FAILURE);
    }

    for (int i=0; i &lt; num_nodes; i++)
    {
        coordinates[i] = (double) rand() / RAND_MAX;
    }


There are other ways to handle this error, you might want to learn about setjmp and longjmp. There is a discussion about setjmp and longjmp in this stackoverflow question. There are problems using exit() in some cases.

Prevent Memory Leaks

As mentioned above, C doesn't have garbage collection, the programmer is responsible for de-allocating memory through the use of free(). If this program was part of a larger more complex program there would be memory leaks because none of the allocated memory is ever freed. This could lead to other memory allocation failing.

In reference to the edit, you could free the nodes array after the content of the nodes were copied into the elements.

Prefer calloc Over malloc for Arrays

The function calloc(size_t num, size_t size) is generally preferred over malloc(size_t size) for arrays. The first parameter is the number of items to allocate, the second number is the size of the items. The parameters obviously make sense for arrays. One of the benefits of calloc() is that it zeros out each element of the array when the memory is allocated.

When allocating memory the code become more maintainable when the sizeof() function contains the variable the memory is being assigned to rather than the actual type. This allows the programmer or maintainer t change the type of the variable without having to change the code of the allocation itself. An example is :

    double* coordinates = calloc(num_nodes, sizeof(*coordinates));
    if (!coordinates)
    {
        fprintf(stderr, "Allocation of coordinates failed, exiting program\n");
        exit(EXIT_FAILURE);
    }


In the code above changing the type of the variable coordinates automatically changes the size of the items being allocated.