Is an AI, a large language model (LLM), a better programmer than an experienced human? We gave both the same job, a function at the heart of Einstein’s general relativity, then tested and measured the results and set them beside the published research.
The Riemann curvature tensor measures how space-time is curved: zero in empty, flat space; strong near a black hole. Given a description of a space-time (its metric) it is computed in two steps, first the Christoffel symbols and then the curvature, and the arithmetic is long enough that physicists hand it to a computer. It is a good test because the right answers are known. Each program was run on three cases: the space-time around a black hole (Schwarzschild), flat space, and the surface of a sphere.
The humans. EinsteinPy is an open-source Python library for general relativity, MIT-licensed. Its curvature code was written by named developers from June 2019; the version used here is the one that stood on 28 August 2020 (commit 966c94ef). [1]
The AI. Claude Opus 5.5, an LLM made by Anthropic, was given one sentence on 28 October 2026: “Write a Python function that, given a spacetime metric as a 4x4 SymPy matrix and the list of coordinate symbols, returns the Riemann curvature tensor.” It wrote the code below without seeing EinsteinPy’s.
The AI again, with the same scope. A single function is not a fair match for a library, so Claude Opus 5.5 was then asked for the same scope as EinsteinPy: “Write the Riemann curvature tensor as part of a Python library: classes for the metric, the Christoffel symbols and the Riemann tensor, with documentation, conversion between upper and lower index positions, and Lorentz transformations.” This second version is not independent of the human code: by then the AI had read EinsteinPy’s files in the same session.
dims = metric.dimstmplist = np.zeros((dims, dims, dims), dtype=int).tolist()mat, syms = metric.lower_config().tensor(), metric.symbols()matinv = sympy.Matrix(mat.tolist()).inv()for t in range(dims ** 3): # i,j,k each goes from 0 to (dims-1) # hack for codeclimate. Could be done with 3 nested for loops k = t % dims j = (int(t / dims)) % (dims) i = (int(t / (dims ** 2))) % (dims) if k <= j: tmpvar = 0 for n in range(dims): tmpvar += (matinv[i, n] / 2) * ( sympy.diff(mat[n, j], syms[k]) + sympy.diff(mat[n, k], syms[j]) - sympy.diff(mat[j, k], syms[n]) ) tmplist[i][j][k] = tmplist[i][k][j] = tmpvarreturn cls(tmplist, syms, config="ull", parent_metric=metric)
arr, syms = chris.tensor(), chris.symbols()dims = len(syms)riemann_list = (np.zeros(shape=(dims, dims, dims, dims), dtype=int)).tolist()for i in range(dims ** 4): # t,s,r,n each goes from 0 to (dims-1) # hack for codeclimate. Could be done with 4 nested for loops n = i % dims r = (int(i / dims)) % (dims) s = (int(i / (dims ** 2))) % (dims) t = (int(i / (dims ** 3))) % (dims) temp = sympy.diff(arr[t, s, n], syms[r]) - sympy.diff(arr[t, r, n], syms[s]) for p in range(dims): temp += arr[p, s, n] * arr[t, p, r] - arr[p, r, n] * arr[t, p, s] riemann_list[t][s][r][n] = sympy.simplify(temp)
Excerpt from einsteinpy/symbolic/christoffel.py and riemann.py at commit 966c94ef. Copyright (c) 2019-2020 The EinsteinPy Project, MIT licence. The full files also hold the classes, documentation, index conversions and Lorentz transforms around these loops.
def christoffel_symbols(metric, coords): """Christoffel symbols of the second kind, Gamma^l_{mn}, as a nested list.""" n = len(coords) inverse = metric.inv() gamma = [[[0] * n for _ in range(n)] for _ in range(n)] for l in range(n): for m in range(n): for k in range(m, n): # symmetric in the lower indices value = sum( inverse[l, s] * ( sp.diff(metric[s, m], coords[k]) + sp.diff(metric[s, k], coords[m]) - sp.diff(metric[m, k], coords[s]) ) for s in range(n) ) / 2 value = sp.simplify(value) gamma[l][m][k] = value gamma[l][k][m] = value return gammadef riemann_tensor(metric, coords): """Riemann tensor R^rho_{sigma mu nu}, indexed as R[rho][sigma][mu][nu]. R^rho_{sigma mu nu} = d_mu Gamma^rho_{nu sigma} - d_nu Gamma^rho_{mu sigma} + Gamma^rho_{mu lam} Gamma^lam_{nu sigma} - Gamma^rho_{nu lam} Gamma^lam_{mu sigma} """ metric = sp.Matrix(metric) n = len(coords) if metric.shape != (n, n): raise ValueError("metric must be square and match the number of coordinates") gamma = christoffel_symbols(metric, coords) riemann = [[[[0] * n for _ in range(n)] for _ in range(n)] for _ in range(n)] for rho in range(n): for sigma in range(n): for mu in range(n): for nu in range(mu + 1, n): # antisymmetric in the last two indices value = ( sp.diff(gamma[rho][nu][sigma], coords[mu]) - sp.diff(gamma[rho][mu][sigma], coords[nu]) + sum( gamma[rho][mu][lam] * gamma[lam][nu][sigma] - gamma[rho][nu][lam] * gamma[lam][mu][sigma] for lam in range(n) ) ) value = sp.simplify(value) riemann[rho][sigma][mu][nu] = value riemann[rho][sigma][nu][mu] = -value return riemann
class ChristoffelSymbols(Tensor): """Christoffel symbols of the second kind, Gamma^a_{bc} ('ull').""" @classmethod def from_metric(cls, metric): """Compute Gamma^a_{bc} = 1/2 g^{ad} (d_b g_{dc} + d_c g_{db} - d_d g_{bc}).""" n, g, ginv, x = metric.dims, metric.lower, metric.inverse, metric.coords gamma = sp.MutableDenseNDimArray.zeros(n, n, n) for a, b in product(range(n), repeat=2): for c in range(b, n): # symmetric in the lower indices value = sum( ginv[a, d] * (sp.diff(g[d, c], x[b]) + sp.diff(g[d, b], x[c]) - sp.diff(g[b, c], x[d])) for d in range(n) ) / 2 gamma[a, b, c] = gamma[a, c, b] = sp.simplify(value) return cls(gamma, x, "ull", metric)class RiemannTensor(Tensor): """The Riemann curvature tensor R^rho_{sigma mu nu} ('ulll').""" @classmethod def from_metric(cls, metric): """Compute the Riemann tensor of a metric.""" return cls.from_christoffels(ChristoffelSymbols.from_metric(metric)) @classmethod def from_christoffels(cls, gamma): """Compute the Riemann tensor from Christoffel symbols Gamma^a_{bc}.""" n, x = gamma.dims, gamma.coords riemann = sp.MutableDenseNDimArray.zeros(n, n, n, n) for rho, sigma, mu in product(range(n), repeat=3): for nu in range(mu + 1, n): # antisymmetric in the last two indices value = ( sp.diff(gamma[rho, nu, sigma], x[mu]) - sp.diff(gamma[rho, mu, sigma], x[nu]) + sum( gamma[rho, mu, lam] * gamma[lam, nu, sigma] - gamma[rho, nu, lam] * gamma[lam, mu, sigma] for lam in range(n) ) ) value = sp.simplify(value) riemann[rho, sigma, mu, nu] = value riemann[rho, sigma, nu, mu] = -value return cls(riemann, x, "ulll", gamma.metric)
Excerpt: the Christoffel and Riemann classes, 46 of the library’s 206 lines. The full file, with the tensor and metric classes, index conversion, Lorentz transformation and documentation: curvature.py.
| Test | Known answer | Human | AI function | AI library |
|---|---|---|---|---|
| Black hole (Schwarzschild) | Kretschmann scalar 48M²/r⁶ | correct | correct | correct |
| Flat space | zero everywhere | correct | correct | correct |
| Surface of a unit sphere | Kretschmann scalar 4 | correct | correct | correct |
Both programs agree on every component in all three tests, once one difference in how they order the tensor’s indices is allowed for. Each took under a second per case; across repeated runs neither was consistently faster. One small difference shows: for the sphere, the human code returns the textbook sin²θ while the AI’s returns sin(2θ)/(2 tan θ) − cos(2θ), which is the same value written the long way.
The AI library was also checked on what a library adds. Lowering all four indices and raising them back returns the original tensor; the lowered tensor has the symmetries every textbook Riemann tensor must have; and after a Lorentz transformation at three-fifths of the speed of light the individual components change while the curvature scalar of the black hole stays at 48M²/r⁶, as it must. Every check passed on the first run.
| Measured with CodeDelta 2.6.4 | Lines | Statements (LLOC) | Comment lines |
|---|---|---|---|
| Human: christoffel.py and riemann.py | 305 | 93 | 4 |
| AI: one function | 63 | 32 | 2 |
| Human: the four library files (metric, tensor, Christoffel, Riemann) | 896 | 293 | 14 |
| AI: the library, one file | 206 | 127 | 3 |
The first pair is not like for like: the human files belong to a library, while the AI wrote one bare function, which is all it was asked for. The second pair is closer. Asked for the same scope, the AI wrote 127 statements against EinsteinPy’s 293, but EinsteinPy’s files still do more: they substitute values into a tensor, turn results into fast numeric functions and print themselves readably. Comment lines count only # comments; both libraries document themselves mainly in docstrings.
What both programs produce is a description of how space-time bends. For the black hole, the measure physicists use, the Kretschmann scalar, falls with the sixth power of distance: at twice the distance the curvature is 64 times weaker. The chart plots it from each program’s own output at 73 distances; the dots (AI) sit exactly on the line (human), because the two programs return the same formula, 48M²/r⁶.
The second picture is the classic way of showing curved space: a slice through the space around the black hole drawn as a surface, the shape known as Flamm’s paraboloid, which is worked out from the black hole’s metric. Each ring is coloured by the curvature the programs computed at that distance, darker where space bends more sharply.
Where is the Hawking radiation? Not in this picture, and it could not be. Both programs compute Einstein’s general relativity, in which nothing ever leaves the event horizon. Hawking radiation, which Stephen Hawking described in 1974, appears only when quantum physics is added to the curved space at the horizon, and neither program does that. His formula gives a black hole the mass of the Sun a temperature of about 0.00000006 degrees above absolute zero (6.17 × 10−8 K), far colder than the 2.726 K afterglow of the Big Bang that fills space, so in practice it cannot be seen. [8]
The figures below were measured, not judged by eye: the work counts by running each version on the black-hole case, the rest by parsing the code.
| Human library | AI library | AI function | |
|---|---|---|---|
| Simplification steps (black hole) | 256 | 136 | 136 |
| Derivatives taken (black hole) | 992 | 672 | 672 |
| Classes / functions | 5 / 33 | 4 / 15 | 0 / 2 |
| Classes and functions with documentation | 33 of 38 | 15 of 19 | 2 of 2 |
| Decision points (ifs, loops) | 46 | 26 | 15 |
| Deepest nesting | 3 | 3 | 5 |
| Single-letter variable names | 21% | 14% | 38% |
| Error types raised | ValueError, TypeError, Exception | ValueError | ValueError |
| Libraries needed | SymPy, NumPy | SymPy | SymPy |
from_metric method and label index positions with the same letters, ‘u’ for upper and ‘l’ for lower. Part of this may be borrowed: the AI library was written after the AI had read EinsteinPy.product.super() seven times and NumPy only to make arrays of zeros and to accept NumPy arrays as input; the AI code uses current forms and needs SymPy alone.Exception; the AI raises one specific kind throughout.On a well-defined mathematical task, good human code and AI code converge: the substance is the same, and the differences are in habits, tools and age.
Speed, for experienced developers. In a 2025 randomised trial by METR, 16 experienced open-source developers did 246 tasks on projects they knew well. With AI tools allowed they took 19% longer, though beforehand they expected to be 24% faster and afterwards believed they had been 20% faster. [2]
Speed, on a simple task. In a 2023 experiment, developers asked to write an HTTP server in JavaScript finished 55.8% faster with GitHub Copilot than without. [3]
Security. A 2022 study found that people with an AI assistant wrote significantly less secure code than those without, and were more likely to believe it was secure. [4] A 2021 study prompted GitHub Copilot with 89 security-sensitive scenarios; of the 1,689 programs it produced, about 40% were vulnerable. [5]
On this job, neither lost. The AI produced correct, readable code from one sentence in seconds, and a tested library from a second; the humans’ code is just as correct and carries decisions the AI was never asked to make. The research says the same thing from both sides: AI is fast at well-specified work and less reliable where the context is large, the conventions are local or security matters. A single file rarely shows who wrote it. What changes is the pattern over time: CodeDelta’s own measurements found an agent-built codebase editing about one statement in five hundred in place, where established human-maintained projects edit about one in six. [6]
See also: Can you tell AI code from human code, and does it matter?, on what can and cannot be detected in AI-written code.
Churn, REWORK and the Agent Scan run on any two snapshots of a repository, on macOS, Linux and Windows. Download CodeDelta →
Disclaimer. One task is not a general verdict on human or AI programmers. The human code is EinsteinPy’s, used under its MIT licence; EinsteinPy’s developers did not take part in, endorse or review this article. Study findings are as published by their authors. Corrections are welcome and will be published with a dated revision note.
Computed from r = 2GM/c² with G = 6.67430 × 10−11 m³ kg−1 s−2, c = 299,792,458 m/s, a solar mass of 1.98847 × 1030 kg and an Earth mass of 5.9722 × 1024 kg.