Tech & Projects

Flagship Project

Bare Metal Bard

A CUDA SGEMM written from scratch, taken from 1.2% of cuBLAS to 117% with tensor cores, then used to train a GPT — no PyTorch, cuBLAS or cuDNN anywhere in the training path.

CUDAC++Tensor Cores (WMMA)Nsight ComputeNext.js
Learn More
Bare Metal Bard screenshot

Environment

CORA

My hometown in China is very close to the coast. After learning more about global warming and its impact on coastal communities, I wanted to create something that allowed us to see the destructive consequences of sea level rise. My most notable data science project, CORA, is a tool that uses datasets and graph-based algorithms to simulate sea level rise floods, predict economic damage, and test adaptation strategies.

PythonPyQt6rasterioGeoPandasOSMnxShapelyNumPyMatplotlibReportLab
CORA screenshot 1
CORA screenshot 2
CORA screenshot 3

Finprint

Currently, I am working with a peer on Finprint, a web app that uses audio processing and machine learning to determine the species and type of a whale call. The identification of whale calls is important to understand whale social behavior, track where whales are, and how populations change. An AI-assisted approach will significantly improve the accuracy and even achieve previously impossible tasks such as classifying killer-whale acoustic signals.

0.979

Group accuracy

0.906

Species accuracy

0.965

Top-3 accuracy

32

Species classified

PyTorchlibrosaFastAPINumPySciPyscikit-learnDockerWatkins (WMMS)
Finprint screenshot 1
Finprint screenshot 2
Finprint screenshot 3

EcoVision

My passion for computer science stems from its ability to turn messy data into systems that improve our lives and anticipate what's to come, especially in solving ecological and environmental issues. My interest in this area originated in 2024, when at Hack the North, my teammates and I created EcoVision, a computer vision ML model that uses a phone's camera feed to determine whether an item is recyclable. This application showed me the potential of technology changing lifestyles and facilitating environmental initiatives.

SwiftSwiftUIYOLOv5PyTorchFlaskOpenCVTensorFlowTACO dataset
EcoVision screenshot 1

Robotics

I joined my high school’s robotics team in grade 9 as a programmer responsible for solving complex technical challenges, developing autonomous routines that consistently scored points in competition, contributing to our 1st Place Inspire Award in Dec 2025, and mentoring junior members in advanced programming techniques.

In June 2026, my friends and I created a new robotics team because the school team was too restrictive for our goals, where administrative tasks took way too long and business decisions were rigid. It involved recruiting members, purchasing parts, creating budgets, finding sponsors, organizing workspaces, and establishing team hierarchies. It is meaningful to me because it is a collaboration that started partly because of me. There is no greater organization upholding the team and it depends entirely on us to keep the structure.

The FTC robot on the practice field: Limelight vision camera above a REV Control Hub, with the intake and drivetrain exposed.
Rams Robotics 16488 in a qualification match at the FIRST Tech Challenge Ontario Provincial Championship.
Provincials graphic: team photographs above the FIRST Tech Challenge Inspire Award for 16488 Rams Robotics.

Used by Peers

Stroj

A self-hosted online judge, built so the coding club had somewhere to put its own problems rather than sending members off to someone else's site. A submission arrives against a problem's test data; the judge compiles it, runs it test by test under time, memory and output limits, compares what came out, and returns a verdict. Contests sit on top: a timed window, a problem set sealed until the clock starts, and either an ICPC or an IOI scoreboard.

Subtasks are what make a hard problem approachable. Tests grouped into weighted directories each become all-or-nothing, so a beginner who solves only the small cases still earns something toward the leaderboard rather than nothing. A hidden test belonging to no subtask is rejected at upload instead of sitting there silently unscored.

The sandbox is the half worth reading, and it is honest about the gap. Each submission runs in a throwaway directory as its own process group under rlimits, with a wall-clock watchdog that kills the whole group so nothing outlives its timeout by forking, and an active RSS sampler because macOS accepts RLIMIT_AS and then quietly ignores it. The judge reports the isolation actually in force rather than the one that was asked for, and says plainly that this suits a classroom or a team practice server, not hostile submissions off the open internet.

Python 3FastAPISQLiteVanilla JSDockerVercelpytest
Stroj screenshot 1
Stroj screenshot 2

orgchem

Type anything that names or describes an organic compound and see its structure. IUPAC names, common and trade names, SMILES and molecular formulas all resolve, but the part no other service handles is condensed structural formulas — the notation people actually write by hand — so that parser is the heart of it.

It is valence driven, which is what lets it settle the notation's ambiguities without guessing. Whether the bracket in CH₃CH(CH₃)CH₃ is a branch and the one in CH₃(CH₂)₃CH₃ a repeat unit falls out of which atoms have valence spare; whether an OH continues a chain or hangs off it falls out of the fact that OH cannot carry what follows. A formula written with no hydrogens at all is read as SMILES first, because CCO is ethanol one way and acetaldehyde the other and both draw perfectly well — which reading is asked for first is the whole answer.

Ambiguity is shown rather than resolved silently. A molecular formula does not name a structure, so C₅H₁₂ lists its isomers instead of picking one. Where a structure has exactly one stereogenic element both isomers are built, given 3D coordinates and drawn as ball-and-stick models that turn independently; orientation is held as a rotation matrix rather than a pair of angles, so the models turn freely instead of jamming at the poles.

The practice mode drills 283 structures in both directions, filtered by topic and level. Typed answers are marked by resolving them back into a structure and comparing it with the one shown rather than by matching a string, so 2-propanol, isopropyl alcohol and propan-2-ol all pass, and naming the right skeleton with the wrong configuration is reported as that rather than simply marked wrong. The answer stays on the server: an option is identified only by its position, and the server rebuilds the list from the question and a nonce, so nothing in what is sent distinguishes the right drawing from the other three.

Next.js 16React 19TypeScriptOpenChemLibTailwind CSS 4OPSINPubChem
orgchem screenshot 1
orgchem screenshot 2

Other

CoolRoute

Southern California heat falls hardest on elderly people, children and outdoor workers. Google Maps gives the shortest walk; CoolRoute gives the coolest one, and puts the trade in plain terms — 16% less sun for one minute of extra walking. Built for the ASPIRE hackathon.

The walking graph is 48,579 nodes and 56,415 edges lifted from OpenStreetMap: every footpath, sidewalk street and stairway in greater Claremont, out to the edges of La Verne, Pomona, Upland and Montclair, preprocessed into a static file that crosses the wire at about 0.7 MB.

Every edge carries static features — mapped trees, canopy priors by street type, 5,128 building footprints, parks, arterial heat islands. At request time the sun's altitude and azimuth are computed astronomically for the chosen departure, and each edge resolves to a shade fraction: canopy, plus building shadows that lengthen as the sun drops and fall across streets running perpendicular to it, plus park cooling.

Dijkstra then runs twice in the browser, in under 50 ms — once on distance, which reproduces the ordinary route, and once on distance scaled by sun exposure. Both draw at once, the cool one coloured segment by segment by how much sun it takes. Conditions come from the nearest NOAA station, with Open-Meteo behind it, and the source is named in the interface. The whole thing is static and client-side: no backend, no API keys.

TypeScriptOpenStreetMapDijkstraNOAA / NWSOpen-MeteoNominatimVercel

yanvpn

A personal VPN — one always-on server at home, an iPhone and a Linux laptop as clients — built for a network that filters DNS, firewalls outbound traffic, and blocks WireGuard outright.

WireGuard alone does not survive that, and the reason is worth stating precisely: every handshake initiation opens with the byte 0x01 followed by three zero bytes, in a packet that is always exactly 148 bytes long. One deep-packet-inspection rule catches that on any port, which is why moving it to 443 changes nothing — the giveaway is the content, not the port.

So three transports are installed, and it falls back between them. WireGuard itself is fastest and hides nothing. AmneziaWG keeps the same cryptography and the same speed while randomising those four fixed header bytes per deployment and padding the handshake with junk, so the size signature dies too. VLESS with REALITY proxies a genuine TLS handshake to a real public site, so a middlebox sees that site's real certificate with a valid chain and an active probe is handed the real site — at the cost of TCP's lower throughput and head-of-line blocking on lossy links.

Bringing it up tries them fastest-first and remembers what worked, and a doctor command probes all three and reports what the network is actually blocking rather than what it was assumed to block. DNS filtering is handled by construction instead of by rule: clients resolve through an address that exists only inside the tunnel, answered by dnsmasq at the house, so the local resolver never sees a query.

ShellWireGuardAmneziaWGVLESS / REALITYdnsmasqLinuxiOS

YanTasks

A task manager that answers the question the list never does — what should I do now. Tasks are weighted by how urgent they are, and the scheduler draws from them at random in proportion to those weights, once per 30-minute block, laying out the rest of the day rather than handing back a single answer.

It works signed out. Everything lives in the browser and survives a reload, and the account buttons say accounts are not configured on this server rather than blaming the network when no database is attached. Sign in and the same data lives in the account instead, so it follows you between browsers.

Accounts run on Neon Postgres over its HTTP driver, which is what makes them work on a serverless host: every query is a stateless request, so there is no connection pool to exhaust across instances and nothing is written to a filesystem that is read-only and discarded between requests anyway.

The security decisions are written down rather than assumed. Passwords are scrypt with a per-user salt, the parameters stored beside the hash so they can be raised later without a migration. Sessions are an httpOnly cookie holding a 256-bit token, of which only the digest is stored, so a stolen copy of the database cannot be replayed as a login. A wrong username and a wrong password give the same message, so the endpoint cannot be used to find out who has an account. Rate-limit counters live in Postgres rather than process memory, because otherwise every serverless instance would hand out an allowance of its own.

Moving from the device into an account is treated as a decision rather than a default: it offers to move the data or leave it, clears the local copy only once the server confirms it stored them, and asks on sign-in only when the account is completely empty — an account holding anything at all is left alone, because silently overwriting either copy is not a call to make on someone's behalf.

Next.jsTypeScriptNeon PostgresscryptVercel

yanpresence

Mirrors what you are playing in Apple Music into Discord, laid out the way Discord's own Spotify integration is: the song on the one-line status under your name, full-size album art, a live progress bar, and the song, artist and album each clickable through to Apple Music.

Three ways in, one pipeline. On macOS playback comes from the Music app over Apple Events. On Windows it comes from the System Media Transport Controls session — the record behind the flyout over the volume overlay — except for Apple TV, which publishes nothing at all and is read off its own UI Automation tree. On Linux it comes from the web player, over a companion browser extension or MPRIS on the session bus. Everything past that point is identical, because each source hands back the same shape of snapshot.

The detail that makes it read correctly is a single Discord field. `status_display_type` decides which value lands on the status line; Spotify sets it to the state, which is why its status reads as the artist. This sets it to the details, so the status reads as the song. The expanded card's header is separate and always comes from the application's name, which Discord does not allow to be set at presence time — which is why the first step of setup is naming the application Apple Music.

Artwork is pulled at 1024×1024, the size Discord's own documentation asks for, and where Apple has published motion artwork the full loop is transcoded from their HLS master to animated AVIF. On Windows the read runs through Windows PowerShell 5.1 rather than 7, because the media session is a WinRT API and pwsh cannot project WinRT types without the Windows SDK. It shows nothing while paused, and survives Discord restarts, Music restarts and sleep.

Node.jsApple EventsWinRT / SMTCMPRISDiscord IPCffmpegAVIF