GSoC 2026

Reduced Order Modeling with Neural Operators

GSoC 2026 Final Deliverables

Notes and progress from my GSoC 2026 project. The goal is to bring Neural Operators into the Gridap.jl ecosystem, specifically extending the GridapROMs.jl package to enable fast, nonlinear PDE simulations.


Devlog Timeline


Kicking off GSoC 2026: Bridging Julia, PDEs, and Neural Operators

Date: May 24, 2026

The Google Summer of Code 2026 has officially started! As summarized above, I’ll be working with the Gridap organization (supported by the NumFOCUS umbrella) to integrate Neural Operators into their ecosystem.

During this Community Bonding period, I had a couple of very productive meetings with the whole mentoring team—Nicholas Mueller, Eric Neiva, and Martina Gatti—to set up our communication schedule and define the overall scope of the project. On a day-to-day basis, I’ve been working mostly with Eric Neiva, who helped me navigate the PDE theory and the Gridap.jl ecosystem.

My background is mostly in computer science and systems programming, so my first priority during this phase was aligning on the mathematical formulations and getting comfortable with the Gridap.jl tools. I spent the last few weeks reading the documentation, configuring my local environment, and planning out the first coding tasks. We decided to start testing the models on simple equations to get a reliable baseline before moving to more complex physics.


The Exploratory Phase: Theory, Pluto Notebooks, and Model Trade-offs

Date: June 10, 2026

The coding phase is underway, starting with a strong focus on prototyping. To establish a solid mathematical baseline for Reduced Order Modeling within our setup, I built a series of Pluto notebooks.

The first 5 notebooks were dedicated to mastering Gridap.jl and GridapROMs.jl, focusing on standard linear ROMs applied to elliptic, parabolic, and hyperbolic PDEs. The remaining notebooks tested the integration of NeuralOperators.jl into the workflow.

We are currently evaluating several models to decide what will eventually be merged into GridapROMs.jl. I've been experimenting with FNO (Fourier Neural Operator), DeepONet, and NOMAD. Meanwhile, my mentors and I are also discussing more modern architectures like CNO, CNN, UNet, WaveletNet, GNN, and GNO.

This exploration has highlighted some clear trade-offs:

We are still finalizing the roster, but the current idea is to expose a few simple models (like DeepONet and FNO) and potentially offer a Graph-based model for users with access to HPC clusters.


Scaling Up: DrWatson, Caching, and Building an Orchestrator Dashboard

Date: June 25, 2026

As the complexity of the project grew, Pluto notebooks started to show their limits. I needed a more robust way to manage hyperparameters, physical variables, and model weights. I migrated the workflow to a dedicated suite of Julia scripts orchestrated by DrWatson.jl.

To prevent redundant and expensive FE computations, I built a custom caching mechanism called HashRegistry.jl. It computes a SHA-256 hash of the simulation parameters; if a pipeline step (data generation, model training, or evaluation) is already in the cache, it instantly loads the results. I also integrated Learning Rate Schedulers (only CosineAnnealing and ReduceLROnPlateau at the moment).

With so many parameters to tweak, interacting solely via the REPL became impractical. I decided to build an interactive web dashboard to act as a graphical orchestrator.

A key architectural decision here was to remove Node.js as a dependency for end-users. I configured the workflow so the React app is compiled into a static build (npm run build). The Oxygen.jl backend is set up to serve these static files directly. This means any user can launch the full interactive dashboard locally with only a Julia installation!

Now, I can configure the FEM generation, choose the model hyperparameters, and watch the training loop live, all from the browser.


Midterm Evaluation: Mini-Batches, Fine-Tuning, and XLA Compilation

Date: July 9, 2026

Tomorrow is the Midterm Evaluation deadline, and the last two weeks have been an intense coding marathon.

I’ve been heavily focused on refining the training loops. I successfully implemented mini-batch training, which revealed differences in how data must be managed and the resulting RAM consumption across the models:

I also added a Cloud Sync & Fine-Tuning feature. To make collaboration easier, I connected the dashboard to a Firebase backend (Firestore for metadata, Storage for plots and snapshot matrices/weights). Users can now browse a global catalog of shared experiments, pick a pretrained_model_hash from the cloud, and sync it to their local DrWatson workspace. Instead of starting from scratch with random weights, the Julia backend loads pre-trained weights (from database or local simulations) and begins fine-tuning on the newly selected data!

My biggest nemesis this week? XLA Compilation times. The first simulation on the server takes a long time to boot due to LLVM/XLA compilation via Reactant.jl. I've tried everything to shave off these 3-4 minutes—custom Julia sysimages, Level 1 .ji precompilation, and server warmup scripts. While the sysimage drastically improved library loading times, the XLA compilation lock during the first JIT pass remains a tough problem to solve. I've also fortified the Oxygen.jl server with thread mutexes and clean shutdown handling to manage active WebSocket simulations gracefully.

It's been a challenging but incredibly rewarding first half. Focusing now on Phase 2!


Phase 1 Wrap-up & The GridapROMs Blueprint

Date: July 23, 2026

The GSoC midterm evaluations are officially over, and it feels great to hit this milestone. Right up until the July 21st deadline, I was wrapping up the Phase 1 deliverables. The training loops and XLA compilation were the heavy lifters on the backend, but the interactive dashboard needed a lot of last-minute polish to actually be usable. I spent a few days wiring up strict input validations with Zod schemas so users can't accidentally crash the Julia backend with bad hyperparameter combinations. I also got a filtering system in place for the experiment catalog and added a dedicated "Docs" view to help people navigate the architecture.

With Phase 1 done, our July 22nd sync with my mentors became a real pivot point. We are moving away from standalone scripts and finally digging into the actual GridapROMs.jl source code.

The main objective here is to get Neural Operators integrated into the existing Reduced Basis ecosystem without breaking everything else. We sketched out the API during the meeting. The plan is to introduce a new NeuralOpSolver that sits in the solver hierarchy but bypasses the standard projection-based hyper-reduction steps.

Here are the initial stubs we came up with:

# The Neural Operator solver hangs off the GlobalRBSolver
const NeuralOpSolver{A,C<:NeuralOpReduction} = GlobalRBSolver{A,C,Nothing,Nothing}

function NeuralOpSolver(fesolver::GridapType, reduction::Reduction)
  # Notice the `nothing, nothing` — we don't need classical residual/jacobian reduction
  RBSolver(fesolver, GlobalContext(), reduction, nothing, nothing)
end

# The strategy defines the architecture and hyperparameters
struct DeepONetReduction <: NeuralOpReduction
  # ...
end

# The specialized Operator that holds the trained network instead of Galerkin matrices
struct NeuralRBOperator{O,T,A} <: RBOperator{O,T}  # A <: NeuralNetwork
  op::ParamOperator{O,T}
  model::A
end

It’s nice to see the neural models finally looking like native Gridap operators rather than floating scripts. The next step is actually filling these stubs with the training and inference logic.


Wiring the Pipeline: DeepONet and NeuralOpStrategy

Date: August 6, 2026

Over the last couple of weeks, the focus has been entirely on turning those stubs into compliant Gridap code. After our July 31st sync, we narrowed the scope: get DeepONet working first, and worry about the rest later.

Shoving a neural network stack (Lux.jl and Reactant.jl) into a finite element library involves a lot of boilerplate and glue code. By isolating DeepONet, I could lock down the API boundaries—specifically _extract_operator_data, train_neural_operator, and solve. Once this core skeleton is solid, plugging in new neural operators should be pretty straightforward.

Here are the core design decisions behind that first major commit:

The result? A fully functional pipeline. We can now generate high-fidelity snapshots, train a DeepONet, and use it as a surrogate solver entirely inside the Gridap ecosystem. Next on the list: handling transient problems and building a sampling system. I've noticed that feeding the network a function sampled at various points works much better than just passing a single scalar parameter.


Expanding the Scope: NOMAD, Transient Problems, and Fine-Tuning

Date: August 24, 2026

With the final deadline fast approaching, I spent the last couple of weeks sprinting to close out the remaining tasks. The DeepONet implementation provided a solid baseline, so extending the pipeline to support the NOMAD architecture was relatively straightforward. I also replaced the external NeuralOperators.jl dependencies with native Lux.jl implementations for both models to have tighter control over the forward passes and state management.

A major chunk of the work involved adapting the codebase for transient problems (RBTransient). Unlike steady-state problems, transient inference requires mapping the network outputs over a spatio-temporal grid. I implemented extraction methods to pull the t_grid and spatial coordinates, flattened them to feed the neural operators, and then reshaped the predictions back into the RBParamVector format expected by the GridapROMs solver.

Coordinate extraction itself required some compromises. I implemented a somewhat "hacky" workaround using get_coords_with_order to map algebraic degrees of freedom to physical coordinates. To prevent silent failures, I added a strict check that throws an error (or a warning) if the user does not wrap their test space in an OrderedFESpace. We will need a more robust extraction method post-GSoC, but this works reliably for the PR.

To fulfill the goal I set in early August, I also finalized the sampling system by introducing the branch_sampler inside the NeuralOpStrategy. Instead of feeding raw scalars, users can now dynamically unpack parameters into complex spatial sensors or apply transformations (like log-scaling) before the automatic Z-score normalization kicks in:

# Example: Mapping physical parameters to spatial sensors
x_sensors = range(0, 1, length=50)

branch_sampler_func = (p) -> begin
    sigma, mu = p[1], p[2]
    sensors_f1 = [ (1 / √(2 * π * sigma)) * exp(-x^2 / (2 * sigma)) for x in x_sensors ]
    sensors_f2 = [ sin(mu * x) for x in x_sensors ]
    
    # The Branch Net expects a single flat 1D vector per sample
    return vcat(sensors_f1, sensors_f2) 
end

strategy = NeuralOpStrategy(model = AutoDeepONet(), branch_sampler = branch_sampler_func)

I also shipped the fine-tuning APIs. The reduced_operator function now accepts a pretrained_op. I added an update_stats boolean flag: keeping it false inherits the old Z-score normalization statistics for continual learning, while setting it to true recomputes the statistics for transfer learning on new domains.

# Continual Learning: Inherits Z-scores (μ, σ) from the pre-trained domain
new_op = reduced_operator(solver, feop_ext, s_ext, pretrained_op; update_stats=false)

# Transfer Learning: Recomputes Z-scores for a drastically different physical scale
new_op = reduced_operator(solver, feop_ext, s_ext, pretrained_op; update_stats=true)

The last few days were all about codebase cleanup. I swapped out some messy Union types for cleaner AbstractDeepONet and AbstractNOMAD interfaces. I also finally got around to implementing that TrainingLog system we pushed down the priority list a few weeks ago, adjusting the verbosity levels to match how GridapSolvers.jl handles ConvergenceLogs. The last steps were writing the final test suites (focusing heavily on error handling) and drafting the documentation examples. The PR is ready for review.


The Final PR and a Change of Scenery

Date: August 28, 2026

I submitted the final pull request wrapping up my GSoC 2026 project. The PR aggregates all the work from the summer: DeepONet and NOMAD support for both steady and transient problems, the configurable NeuralOpStrategy, spatial and temporal subsampling via step_x and step_t, data normalization handling, and learning-rate schedulers like CosineAnnealing and ReduceLROnPlateau. It also includes the branch_sampler logic designed for multi-sensor inputs and algorithmic scaling.

# Define the strategy (Architecture, Hyperparameters, Schedulers)
strategy = NeuralOpStrategy(
    model = AutoNOMAD(width=64, depth=3),
    epochs = 2500,
    batch_size = 512,
    lr_scheduler = CosineAnnealing(lr_max=1f-3, lr_min=1f-6)
)

# Wrap the classical Gridap solver
neural_solver = NeuralOpSolver(LUSolver(), NOMADReduction(strategy))

# Offline Phase: Train the operator on snapshots
neural_rb_op = reduced_operator(neural_solver, feop, snapshots_train)

# Online Phase: Millisecond inference on new parameters
x_approx, stats = solve(neural_solver, neural_rb_op, param_test)

After submitting, I had a wrap-up meeting with the maintainers to discuss the integration. We ended up making an unexpected but logical decision regarding the codebase.

Instead of merging the neural operator support directly into GridapROMs.jl, the maintainers decided to move it into a separate, dedicated package called NonlinearROMs.jl. This new package is currently being built by the team specifically for deep learning-based reduced-order models, making it a much more natural fit for my work.

As a result, my PR on GridapROMs.jl will be closed, and the code will be ported over to NonlinearROMs.jl. The maintainer noted that while the core DeepONet/NOMAD architectures and the Lux/Reactant training loops I wrote will remain exactly as implemented, the surrounding glue code—like data preparation, sampling, and file layout—will be adapted to fit the new package's architecture.

Honestly, it’s a smart pivot. Mixing heavy deep learning dependencies with classical projection-based ROMs was bound to cause bloat, so keeping them separate makes total sense.

That officially closes the chapter on my GSoC summer. Over the next few weeks, I’ll be working with the team to help migrate the components over to NonlinearROMs.jl. It has been a demanding summer, but building a functional neural operator backend from scratch for the Gridap ecosystem was an incredible deep dive into scientific machine learning and software design.


Beyond GSoC: Graph Neural Operators and a Kernel-Based Framework

Date: September 9, 2026

Even though my GSoC timeline officially ended in August, I wasn't quite ready to leave the project. During our wrap-up meeting on August 28th, I pitched the idea of staying on as a core contributor to implement Graph Neural Operators (GNOs). The mentors were fully on board.

After taking a week off to recharge, we had our first kickoff meeting today to map out the implementation.

We spent the meeting digging into the literature, specifically looking at Zongyi Li's seminal blog post on GNOs and sections 3 and 4 of the Kovachki paper. While reviewing these references, a really interesting architectural pattern emerged: GNOs aren't just an isolated architecture. Both GNOs and FNOs fit into a broader mathematical category of "kernel-formulation" (or kernel-based) neural operators.

This completely shifted our immediate plan. Instead of just diving in and hardcoding a GNO, my first task is to design a generic infrastructure and the right abstract types to host this entire family of kernel-based operators. The goal is to build an abstract framework so that, once the GNO is implemented, plugging in any other kernel-based architecture in the future will be practically seamless.

On the software side, we evaluated how to actually build this in Julia. Since Graph Neural Networks (GNNs) basically act as an umbrella framework for generalizing CNNs on unstructured grids, we are looking closely at GraphNeuralNetworks.jl. More specifically, we are interested in GNNLux.jl, as its design is heavily inspired by PyTorch Geometric and integrates perfectly with the Lux backend I set up this summer.

It is a massive architectural challenge, but I'm excited to keep contributing to the ecosystem. I'll start drafting the abstract types for NonlinearROMs.jl later this week.


References & Further Reading

Throughout the project, I relied on several key papers and resources to understand and implement these architectures:


AI Usage Disclaimer: To accelerate development on the non-core aspects of this project (specifically the React frontend, server boilerplate, and English proofreading), I utilized Google Gemini as a conversational assistant. No autonomous AI coding agents were used; I retained full architectural control, and all the core Julia mechanics, mathematical implementations, and design decisions were made entirely by me.

"Google Summer of Code" and "GSoC" are trademarks of Google. NumFOCUS is a trademark of NumFOCUS. This project is an independent open-source contribution and is not officially endorsed by or affiliated with Google or NumFOCUS.

CC BY-SA 4.0 Isaia Zollo. Last modified: September 09, 2026. Website built with Franklin.jl and the Julia programming language.