diff --git a/build/Site.hs b/build/Site.hs index f061984..6c54a7c 100644 --- a/build/Site.hs +++ b/build/Site.hs @@ -29,6 +29,7 @@ import Compilers (essayCompiler, postCompiler, pageCompiler, poetryCompiler, fi import Catalog (musicCatalogCtx) import Commonplace (commonplaceCtx) import Now (nowCtx) +import Vita (vitaCtx) import Contexts (siteCtx, essayCtx, postCtx, pageCtx, poetryCtx, fictionCtx, compositionCtx, contentKindField, recentFirstByDisplay, tagLinksFieldExcludingTopSegment, isProvedConfidence) @@ -270,6 +271,13 @@ rules = do -- /current.html. Re-compiles current.html when the YAML changes. match "data/now.yaml" $ compile getResourceBody + -- CV/résumé YAML — the same files the PDF pipeline builds from + -- (yaml-source/data/). Loaded by Vita.vitaCtx so /about.html renders + -- education, publications, presentations, and experience from one + -- source instead of a hand-typed copy. Matching them here is what + -- makes about.html recompile when the CV data changes. + match "yaml-source/data/*.yml" $ compile getResourceBody + -- Per-build stamp — written by Main.main before Hakyll starts, so it -- always exists and always differs from the previous run. Matched -- (not routed) purely so the telemetry pages can `load` it as a @@ -352,8 +360,21 @@ rules = do >>= loadAndApplyTemplate "templates/default.html" essayCtx >>= relativizeUrls + -- Vita — the academic-homepage view, driven by yaml-source/data/*.yml. + -- Prose (research interests, pointers, contact) stays in the markdown + -- body; education, publications, presentations, and experience are + -- generated by Vita.vitaCtx and assembled by templates/vita.html. + match "content/about.md" $ do + route $ stripPrefixRoute "content/" + `composeRoutes` setExtension "html" + compile $ pageCompiler + >>= loadAndApplyTemplate "templates/vita.html" vitaCtx + >>= loadAndApplyTemplate "templates/default.html" vitaCtx + >>= relativizeUrls + match ("content/*.md" .&&. complement "content/index.md" + .&&. complement "content/about.md" .&&. complement "content/commonplace.md" .&&. complement "content/colophon.md" .&&. complement "content/current.md" diff --git a/build/Vita.hs b/build/Vita.hs new file mode 100644 index 0000000..18653e3 --- /dev/null +++ b/build/Vita.hs @@ -0,0 +1,543 @@ +{-# LANGUAGE GHC2021 #-} +{-# LANGUAGE OverloadedStrings #-} +-- | Vita page: renders education, publications, presentations, and +-- experience for @/about.html@ from @yaml-source/data/*.yml@ — the same +-- files that drive the CV and résumé PDFs via the Jinja/xelatex pipeline. +-- +-- The point is single-sourcing. Before this module the vita page carried a +-- hand-typed copy of all four sections, and it had drifted from the PDFs in +-- roughly fifteen places (stale roles, a superseded line count, a talk still +-- listed as forthcoming after it was given). Anything rendered here is read +-- from the same YAML the PDFs are built from, so the two cannot disagree. +-- +-- Sections the site already owns are deliberately absent: in-progress work +-- belongs to @/current@ (data\/now.yaml, see "Now"), the engineering index to +-- @/cv/projects/@, and the personal narrative to @/me/@. This module renders +-- only what nothing else did. +-- +-- The YAML is LaTeX-flavoured, because its first consumer is xelatex. Values +-- may contain @\\textbf{}@, @\\href{}{}@, @$\\times$@, @~@, @--@ and friends, +-- so every value goes through 'latexToHtml' on the way out. See that function +-- for the full supported set and why escaping happens before conversion. +module Vita + ( vitaCtx + ) where + +import Data.Aeson (FromJSON (..), Object, Value (..), withObject, (.:), (.:?), (.!=)) +import Data.Aeson.Types (Parser, typeMismatch) +import Data.List (isPrefixOf, sortOn) +import Data.Maybe (mapMaybe) +import Data.Scientific (isInteger, toRealFloat) +import qualified Data.Aeson.Key as K +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Yaml as Y +import Hakyll hiding (escapeHtml) +import Contexts (siteCtx) +import Utils (escapeHtml) + +-- --------------------------------------------------------------------------- +-- Loose scalars +-- --------------------------------------------------------------------------- + +-- | A YAML scalar that may be written as either a string or a number for the +-- same logical field. @publications.yml@ has both @year: 2026@ (parsed as a +-- number) and @year: "2026--2027"@ (a string); @experience.yml@ has +-- @start: "2025"@ next to @start: July 2026@. Accept either and normalise +-- to 'String' rather than forcing the YAML to be quoted consistently — the +-- PDF pipeline does not care, and this module should not make it care. +newtype Loose = Loose { unLoose :: String } + +instance FromJSON Loose where + parseJSON (String t) = pure (Loose (T.unpack t)) + parseJSON (Number n) + | isInteger n = pure (Loose (show (truncate (toRealFloat n :: Double) :: Integer))) + | otherwise = pure (Loose (show (toRealFloat n :: Double))) + parseJSON (Bool b) = pure (Loose (if b then "true" else "false")) + parseJSON v = typeMismatch "string or number" v + +reqLoose :: Object -> String -> Parser String +reqLoose o k = unLoose <$> o .: K.fromString k + +optLoose :: Object -> String -> Parser (Maybe String) +optLoose o k = fmap unLoose <$> o .:? K.fromString k + +-- --------------------------------------------------------------------------- +-- Entry types +-- --------------------------------------------------------------------------- + +data Link = Link + { lkLabel :: String + , lkHref :: String + } + +instance FromJSON Link where + parseJSON = withObject "Link" $ \o -> Link + <$> o .: "label" + <*> o .: "href" + +data Edu = Edu + { edInstitution :: String + , edLocation :: Maybe String + , edDegree :: String + , edStart :: String + , edEnd :: Maybe String + , edGpa :: Maybe String + , edNotes :: Maybe String + , edVisible :: Bool + } + +instance FromJSON Edu where + parseJSON = withObject "Edu" $ \o -> Edu + <$> o .: "institution" + <*> optLoose o "location" + <*> reqLoose o "degree" + <*> reqLoose o "start" + <*> optLoose o "end" + <*> optLoose o "gpa" + <*> optLoose o "notes_cv" + <*> o .:? "cv_visible" .!= True + +newtype EduDoc = EduDoc { unEduDoc :: [Edu] } + +instance FromJSON EduDoc where + parseJSON = withObject "EduDoc" $ \o -> EduDoc <$> o .: "education" + +data Pub = Pub + { pbAuthors :: String + , pbTitle :: Maybe String + , pbVenue :: String + , pbYear :: String + , pbMonth :: Maybe String + , pbTarget :: Maybe String + , pbLinks :: [Link] + , pbNote :: Maybe String + , pbVisible :: Bool + } + +instance FromJSON Pub where + parseJSON = withObject "Pub" $ \o -> Pub + <$> reqLoose o "authors" + <*> optLoose o "title" + <*> reqLoose o "venue" + <*> reqLoose o "year" + <*> optLoose o "month" + <*> optLoose o "target" + <*> o .:? "links" .!= [] + <*> optLoose o "equal_contrib_note" + <*> o .:? "cv_visible" .!= True + +newtype PubDoc = PubDoc { unPubDoc :: [Pub] } + +instance FromJSON PubDoc where + parseJSON = withObject "PubDoc" $ \o -> PubDoc <$> o .: "publications" + +data Pres = Pres + { prAuthors :: String + , prTitle :: String + , prVenue :: String + , prKind :: Maybe String + , prYear :: String + , prMonth :: Maybe String + , prStatus :: Maybe String + , prVisible :: Bool + } + +instance FromJSON Pres where + parseJSON = withObject "Pres" $ \o -> Pres + <$> reqLoose o "authors" + <*> reqLoose o "title" + <*> reqLoose o "venue" + <*> optLoose o "kind" + <*> reqLoose o "year" + <*> optLoose o "month" + <*> optLoose o "status" + <*> o .:? "cv_visible" .!= True + +newtype PresDoc = PresDoc { unPresDoc :: [Pres] } + +instance FromJSON PresDoc where + parseJSON = withObject "PresDoc" $ \o -> PresDoc <$> o .: "presentations" + +data Exp = Exp + { exOrg :: String + , exRole :: Maybe String + , exLocation :: Maybe String + , exLocUrl :: Maybe String + , exStart :: String + , exEnd :: Maybe String + , exSection :: Maybe String + , exOrder :: Int + , exPreamble :: Maybe String + , exBullets :: [String] + , exVisible :: Bool + } + +instance FromJSON Exp where + parseJSON = withObject "Exp" $ \o -> Exp + <$> reqLoose o "organization" + <*> optLoose o "role" + <*> optLoose o "location" + <*> optLoose o "location_url" + <*> reqLoose o "start" + <*> optLoose o "end" + <*> optLoose o "cv_section" + <*> o .:? "cv_order" .!= 99 + <*> optLoose o "cv_preamble" + <*> o .:? "bullets" .!= [] + <*> o .:? "cv_visible" .!= True + +newtype ExpDoc = ExpDoc { unExpDoc :: [Exp] } + +instance FromJSON ExpDoc where + parseJSON = withObject "ExpDoc" $ \o -> ExpDoc <$> o .: "experience" + +-- | @personal.yml@ also carries a @display@ string per link (the value the +-- CV prints in full, since paper cannot be clicked). It is deliberately +-- not read here — see 'renderContact'. +data ProfileLink = ProfileLink + { plLabel :: String + , plHref :: String + , plVisible :: Bool + } + +instance FromJSON ProfileLink where + parseJSON = withObject "ProfileLink" $ \o -> ProfileLink + <$> reqLoose o "label" + <*> reqLoose o "href" + <*> o .:? "cv_visible" .!= True + +-- | Contact details from @personal.yml@. The phone number is deliberately +-- not parsed: it is printed on the CV PDF, which is a document handed to +-- a chosen reader, whereas this page is crawled. Nothing here should hand +-- a scraper a phone number it did not already have to go looking for. +data Person = Person + { pnEmail :: String + , pnLinks :: [ProfileLink] + } + +instance FromJSON Person where + parseJSON = withObject "Person" $ \o -> Person + <$> reqLoose o "email" + <*> o .:? "links" .!= [] + +-- --------------------------------------------------------------------------- +-- LaTeX → HTML +-- --------------------------------------------------------------------------- + +-- | Convert the LaTeX subset that actually appears in the CV YAML into HTML. +-- +-- Callers must escape HTML /before/ calling this, never after: this +-- function emits real tags, so escaping afterwards would turn them into +-- visible @<strong>@. Escaping first is safe because none of the +-- LaTeX constructs contain @<@, @>@ or @&@ — and an @&@ inside an +-- @\\href@ URL becomes @&@, which is what an HTML attribute wants +-- anyway. +-- +-- The supported set is deliberately closed and matches what the YAML +-- contains today (@\\textbf@, @\\textit@, @\\texttt@, @\\href@, +-- @$\\times$@, @$\\delta$@, @\\#@, @{,}@, @~@, @--@, @---@). An unhandled +-- command passes through verbatim and is therefore visible on the page — +-- the intended failure mode, since a silently swallowed @\\emph@ would +-- drop its argument's text. +latexToHtml :: String -> String +latexToHtml = + substAll "---" "—" + . substAll "--" "–" + . substAll "$\\times$" "×" + . substAll "$\\delta$" "δ" + . substAll "\\#" "#" + . substAll "{,}" "," + . substAll "~" " " + . rewriteCmd2 "href" (\u t -> "" ++ t ++ "") + . rewriteCmd1 "textbf" (\x -> "" ++ x ++ "") + . rewriteCmd1 "textit" (\x -> "" ++ x ++ "") + . rewriteCmd1 "texttt" (\x -> "" ++ x ++ "") + +-- | Escape, then convert. The one-step form every renderer should use. +tex :: String -> String +tex = latexToHtml . escapeHtml + +substAll :: String -> String -> String -> String +substAll _ _ [] = [] +substAll pat rep s@(c:cs) + | pat `isPrefixOf` s = rep ++ substAll pat rep (drop (length pat) s) + | otherwise = c : substAll pat rep cs + +-- | Split a leading @{...}@ group, tracking brace depth so nested groups +-- survive. Returns the group's contents and whatever follows it. +takeGroup :: String -> Maybe (String, String) +takeGroup ('{':rest) = go (0 :: Int) "" rest + where + go _ _ [] = Nothing + go d acc ('}':cs) + | d == 0 = Just (reverse acc, cs) + | otherwise = go (d - 1) ('}':acc) cs + go d acc ('{':cs) = go (d + 1) ('{':acc) cs + go d acc (c:cs) = go d (c:acc) cs +takeGroup _ = Nothing + +-- | Rewrite every @\\cmd{arg}@ with a function of its argument. +rewriteCmd1 :: String -> (String -> String) -> String -> String +rewriteCmd1 name f = go + where + marker = '\\' : name + go [] = [] + go s@(c:cs) + | marker `isPrefixOf` s + , Just (arg, rest) <- takeGroup (drop (length marker) s) + = f (go arg) ++ go rest + | otherwise = c : go cs + +-- | Rewrite every @\\cmd{a}{b}@ with a function of both arguments. +rewriteCmd2 :: String -> (String -> String -> String) -> String -> String +rewriteCmd2 name f = go + where + marker = '\\' : name + go [] = [] + go s@(c:cs) + | marker `isPrefixOf` s + , Just (a, rest1) <- takeGroup (drop (length marker) s) + , Just (b, rest2) <- takeGroup rest1 + = f a (go b) ++ go rest2 + | otherwise = c : go cs + +-- --------------------------------------------------------------------------- +-- Shared rendering pieces +-- --------------------------------------------------------------------------- + +-- | @start – end@, or just @start@ when the entry has no end. +dateRange :: String -> Maybe String -> String +dateRange s me = tex s ++ maybe "" (\e -> " – " ++ tex e) me + +-- | The grey line under an entry heading: dates, then location if present. +metaLine :: String -> Maybe String -> Maybe String -> String +metaLine dates mloc murl = concat + [ "

" + , "", dates, "" + , case mloc of + Nothing -> "" + Just loc -> concat + [ " · " + , case murl of + Just u -> "" ++ tex loc ++ "" + Nothing -> "" ++ tex loc ++ "" + ] + , "

" + ] + +-- | Link chips. The affordance the print CV cannot offer: every artifact one +-- click away, rather than a bracketed label the reader has to retype. +renderLinks :: [Link] -> String +renderLinks [] = "" +renderLinks ls = concat + [ "

" + , concatMap one ls + , "

" + ] + where + one l = concat + [ "" + , tex (lkLabel l) + , "" + ] + +renderBullets :: [String] -> String +renderBullets [] = "" +renderBullets bs = concat + [ "" + ] + +section :: String -> String -> String -> String +section slug heading inner = concat + [ "
" + , "

", heading, "

" + , inner + , "
" + ] + +-- --------------------------------------------------------------------------- +-- Section renderers +-- --------------------------------------------------------------------------- + +renderEducation :: [Edu] -> String +renderEducation es + | null visible = "" + | otherwise = section "education" "Education" $ concat + [ "" + ] + where + visible = filter edVisible es + one e = concat + [ "
  • " + , "
    " + , "

    ", tex (edInstitution e), "

    " + , "

    ", tex (edDegree e) + , maybe "" (\g -> " · GPA " ++ tex g ++ "") (edGpa e) + , "

    " + , metaLine (dateRange (edStart e) (edEnd e)) (edLocation e) Nothing + , maybe "" (\n -> "

    " ++ tex n ++ "

    ") (edNotes e) + , "
    " + , "
  • " + ] + +renderPublications :: [Pub] -> String +renderPublications ps + | null visible = "" + | otherwise = section "publications" "Publications and Preprints" $ concat + [ "" + , footnote + ] + where + visible = filter pbVisible ps + -- The dagger legend lives on whichever entry declares it, but reads as a + -- section-level note, so it is rendered once at the foot of the list. + footnote = case mapMaybe pbNote visible of + (n:_) -> "

    " ++ tex n ++ "

    " + [] -> "" + dateOf p = tex (pbYear p) ++ maybe "" (\m -> ", " ++ tex m) (pbMonth p) + one p = concat + [ "
  • " + , "
    " + , case pbTitle p of + -- Entries without a title (work in preparation) put the label in + -- the authors field; the CV template makes the same distinction. + Nothing -> concat + [ "

    ", tex (pbAuthors p), "

    " + , "

    ", tex (pbVenue p) + , maybe "" (\t -> " " ++ tex t) (pbTarget p) + , "

    " + ] + Just t -> concat + [ "

    ", tex t, "

    " + , "

    ", tex (pbAuthors p), "

    " + , "

    ", tex (pbVenue p), "

    " + ] + , "

    ", dateOf p, "

    " + , renderLinks (pbLinks p) + , "
    " + , "
  • " + ] + +renderPresentations :: [Pres] -> String +renderPresentations ps + | null visible = "" + | otherwise = section "presentations" "Presentations" $ concat + [ "" + ] + where + visible = filter prVisible ps + dateOf p = maybe "" (\m -> tex m ++ " ") (prMonth p) ++ tex (prYear p) + one p = concat + [ "
  • " + , "
    " + , "

    ", tex (prTitle p), "

    " + , "

    ", tex (prAuthors p), "

    " + , "

    " + , maybe "" (\k -> tex k ++ ", ") (prKind p) + , tex (prVenue p) + , "

    " + , "

    ", dateOf p, "" + , maybe "" (\s -> "" ++ tex s ++ "") (prStatus p) + , "

    " + , "
    " + , "
  • " + ] + +-- | Experience keeps the CV's research/industry split — that division is +-- editorial, lives in @cv_section@, and a homepage that merged the two +-- would say something the PDF does not. +renderExperience :: [Exp] -> String +renderExperience xs = research ++ industry + where + visible = sortOn exOrder (filter exVisible xs) + isRes e = exSection e == Just "research" + research = group "experience-research" "Research Experience" (filter isRes visible) + industry = group "experience-industry" "Industry Experience" (filter (not . isRes) visible) + group slug heading es + | null es = "" + | otherwise = section slug heading $ concat + [ "" + ] + one e = concat + [ "
  • " + , "
    " + , "

    ", tex (exOrg e), "

    " + , maybe "" (\r -> "

    " ++ tex r ++ "

    ") (exRole e) + , metaLine (dateRange (exStart e) (exEnd e)) (exLocation e) (exLocUrl e) + , maybe "" (\p -> "

    " ++ tex p ++ "

    ") (exPreamble e) + , renderBullets (exBullets e) + , "
    " + , "
  • " + ] + +renderContact :: Person -> String +renderContact p = section "contact" "Contact" $ concat + [ "

    " + , "" + , escapeHtml (pnEmail p) + , "" + , concatMap one (filter plVisible (pnLinks p)) + , "

    " + ] + where + -- Chips carry the label, not personal.yml's `display` value. The CV + -- prints "ORCID: 0009-0002-0162-3587" because a printed page cannot be + -- clicked; a chip reading "0009-0002-0162-3587" alone identifies + -- nothing, and "github.com/levineuwirth" is a URL doing a label's job. + one l = concat + [ "" + , tex (plLabel l) + , "" + ] + +-- --------------------------------------------------------------------------- +-- Load +-- --------------------------------------------------------------------------- + +-- | Same UTF-8 round-trip as "Now": Hakyll hands back a 'String' of Unicode +-- codepoints and the yaml library wants a UTF-8 'ByteString'. +-- 'Data.ByteString.Char8.pack' would truncate every 'Char' to 8 bits and +-- silently mangle the em-dashes and daggers this data is full of. +loadYaml :: FromJSON a => FilePath -> Compiler a +loadYaml path = do + raw <- load (fromFilePath path) :: Compiler (Item String) + case Y.decodeEither' (TE.encodeUtf8 (T.pack (itemBody raw))) of + Left err -> fail (path ++ ": " ++ show err) + Right doc -> return doc + +-- | Render a section, or drop the field entirely when it comes out empty so +-- the template's @$if(...)$@ guards behave. +sectionField :: String -> Compiler String -> Context String +sectionField name gen = field name $ \_ -> do + html <- gen + if null html then noResult (name ++ ": empty") else return html + +-- --------------------------------------------------------------------------- +-- Context +-- --------------------------------------------------------------------------- + +vitaCtx :: Context String +vitaCtx = + constField "vita" "true" + <> sectionField "vita-education-html" + (renderEducation . unEduDoc <$> loadYaml "yaml-source/data/education.yml") + <> sectionField "vita-publications-html" + (renderPublications . unPubDoc <$> loadYaml "yaml-source/data/publications.yml") + <> sectionField "vita-presentations-html" + (renderPresentations . unPresDoc <$> loadYaml "yaml-source/data/presentations.yml") + <> sectionField "vita-experience-html" + (renderExperience . unExpDoc <$> loadYaml "yaml-source/data/experience.yml") + <> sectionField "vita-contact-html" + (renderContact <$> loadYaml "yaml-source/data/personal.yml") + <> siteCtx diff --git a/content/about.md b/content/about.md index a85f662..03bcecf 100644 --- a/content/about.md +++ b/content/about.md @@ -3,72 +3,23 @@ title: Levi Neuwirth — Vita tags: meta --- -For a less formal, more detailed introduction to who I am, see [[Me]]. +The formal record. For a less formal, more detailed introduction to who I am, see [[Me]]; for what I am actively working on this month, see [Current](/current.html). ## Documents -These are probably what you're looking for. A summary of the key points follows below! -- **[Curriculum Vitae (PDF)](/cv.pdf)** -- **[Resume (PDF)](/resume.pdf)** +- **[Curriculum Vitae (PDF)](/cv.pdf)** — the complete record, including grants, affiliations, languages, and technical skills. +- **[Resume (PDF)](/resume.pdf)** — one page, engineering-facing. +- **[Project index](/cv/projects/)** — engineering artifacts in depth, with links to writeups and source. -## Education - -- **Technical University of Denmark (DTU)** — MSc in Computer Science and Engineering. September 2026 – expected 2028. Expecting PhD studies after. -- **Brown University** — Sc.B. in Computer Science and Mathematics. August 2022 – May 2026 -- **DIS Copenhagen / Københavns Universitet** — Semester abroad. Fall 2024 +The sections below are generated from the same data as the two PDFs, so they cannot fall out of step with them. ## Research Interests My work clusters into four threads: -- **AI safety and applied AI** — zero-knowledge proofs for cryptographic verification of large language models, as a MARS V fellow with the [Cambridge AI Safety Hub](https://caish.org/mars) (mentored by James Petrie, Future of Life Institute); a Magic: The Gathering reinforcement-learning project on OSCAR; agentic-systems work at xAI on `grok-code-fast-1`; and reasoning, evaluation, and red-teaming research contracts. -- **Mathematics** — graph theory, number theory, and theoretical computer science. Public results so far are graph-theory-centered: static coverage and persistence in tree-ball geometry ([preprint](/essays/branch-based-local-capture-in-tree-balls/)); more is in progress. +- **AI safety and applied AI** — zero-knowledge proofs for cryptographic verification of large language models, as a MARS V fellow with the [Cambridge AI Safety Hub](https://caish.org/mars) (mentored by James Petrie, Future of Life Institute); a Magic: The Gathering reinforcement-learning project; and reasoning, evaluation, and red-teaming research contracts. +- **Mathematics** — graph theory, number theory, and theoretical computer science. Public results so far are graph-theory-centered: static coverage and persistence in tree-ball geometry ([preprint](/essays/branch-based-local-capture-in-tree-balls/)), and the annealed critical window for growing-radius domination in random regular graphs ([preprint](/essays/near-critical-growing-radius-domination.html)). - **Computer systems and high-performance computing** — the Weenix kernel, a TCP/IP networking stack from scratch in Go, and micro-architectural performance work (SIMD, hardware counters via PAPI, RAPL energy, cross-ISA ports across AVX2 / ARM NEON-SVE / RISC-V V) on Brown's OSCAR HPC cluster. - **Machine learning** — order-invariant ICD-10-CM embeddings (under review at *JAMIA*, deployed calculator), the NeuroPose 3D-kinematics system in Liqi Shu's lab at Brown Neurology, and ongoing research engineering at [NeuroAI](https://neuroai.health). Undergraduate work has been clinically focused; graduate study broadens the scope. Computer vision and security thread through all four but do not stand on their own. - -## Research - -### Published / In Submission - -- **Neuwirth L.** *Branch-Tube Persistence and Static Coverage in Tree-Ball Geometry.* Preprint, July 2026. [Preprint](/essays/branch-based-local-capture-in-tree-balls/) -- **Neuwirth L.** *Where Does SIMD Help Post-Quantum Cryptography? A Micro-Architectural Study of ML-KEM on x86 AVX2.* Technical report, Brown University Department of Computer Science, April 2026. [Report](/essays/where-does-simd-help-post-quantum-cryptography/) · [Artifact](https://git.levineuwirth.org/neuwirth/where-simd-helps) -- **Shu L, Neuwirth L†, Wang X†, Zheng H†.** *Beyond Comorbidity Indices: An Order-Invariant ICD-10-CM Embedding for Readmission and Mortality Prediction.* Under review at the *Journal of the American Medical Informatics Association* (JAMIA), 2026. [Preprint](/essays/beyond-comorbidity-indices/) · [Calculator](https://levineuwirth.github.io/icd_embeddings/) · [Code](https://github.com/levineuwirth/icd_embeddings) - -### In Preparation / In Progress - -- **Zero-knowledge proofs for LLM verification.** MARS V fellowship, [Cambridge AI Safety Hub](https://caish.org/mars), July–October 2026; cryptographic verification of claims about model training, inference, and deployment. Public write-up expected October 2026. -- **[NeuroPose](/essays/neuropose/) clinical-implications manuscript.** In preparation; target submission 2026–2027. -- **SIMD / PQC Phase 2 & Phase 3.** Hardware performance counters (PAPI), RAPL energy, and cross-ISA ports (ARM NEON/SVE, RISC-V V). -- **Semantic-embeddings citation project.** Early-stage work with [NeuroAI](https://neuroai.health), preprint expected summer 2026. -- **Magic: The Gathering reinforcement learning project.** Early-stage work through Brown's HPC, expected late 2026. - -### Presentations - -- **Early Detection of Neurological Disorders through Video-Captured Kinematic Analysis.** Ma J, Arms S, Kaneira L, Lall M, Chen K, Cabral W, Man D, Neuwirth L, Shu L. Poster, Brown / Rhode Island Hospital Neurology Summer UTRA Symposium, August 2025. -- **"Order-Invariant ICD-10-CM Embedding for Readmission and Mortality Prediction: Toward Multimodal Generative Patient Models"**Shu L, Neuwirth L†, Wang X†, Zheng H†. IEEE/ACM Conference on Connected Health: Applications, Systems and Engineering Technologies (CHASE), August 2026, accepted. -- **Neuwirth L, Dasher AS.** "Proving What a Datacenter Did: Verified Inference Between Adversaries." Talk, MARS V, Cambridge AI Safety Hub, July 2026. Accepted. - -†Equal-contribution undergraduate authors. - -## Experience -See [resume (PDF)](/resume.pdf). - -- **Cambridge AI Safety Hub** — *MARS V Fellow.* July – October 2026, Cambridge, UK. Selected for [MARS V](https://caish.org/mars), a competitive part-time AI-safety research fellowship; working under James Petrie (Future of Life Institute) on zero-knowledge proofs for cryptographic verification of large language models. -- **Shu Laboratory, Brown Department of Neurology** — *Undergraduate Researcher and Technical Lead.* October 2023 – Present. Technical lead on [NeuroPose](/essays/neuropose/); co-lead developer on the [ICD-10-CM embedding model](/essays/beyond-comorbidity-indices/). -- **NeuroAI** Present. [neuroai.health](https://neuroai.health). Early-stage venture of academics and clinicians integrating deep learning, reinforcement learning, and generative AI into clinical and research workflows; leading research-engineering across model development, deployment infrastructure, and system design. -- **xAI** Summer 2025, remote. Contributed to the training of `grok-code-fast-1`, xAI's agentic coding model; built LLM integrations into autonomous agent frameworks and resolved 50+ agentic tool-execution failures. -- **Independent Research Contracting** — *Various AI laboratories.* 2025 – Present. Expert reasoning contributions in code and mathematics for agentic workflows, agentic task design and evaluation, AI safety, and red-teaming. - -## Selected Projects - -- **[Weenix](/essays/weenix/)** — Unix-like kernel in ~7,000 lines of C: virtual memory, VFS, system calls, threading, device drivers, interrupt handlers, and file systems. Custom linker support for running userspace x86-64 ELF binaries; extended with pipes and userspace preemption. -- **[Networking Stack from Scratch](/essays/networking-stack/)** — TCP/IP, RIP, UDP, and DNS in Go, supporting file transmission of up to 1 GB across 8-node networks. Extended with a fully RFC-compliant SSH implementation (2,000+ additional lines) supporting sustained sessions of arbitrary length. -- **[SIMD / PQC Performance Study](/essays/where-does-simd-help-post-quantum-cryptography/)** — Hand-written AVX2 assembly for ML-KEM / Kyber. 35×–56× speedup over compiler-optimized C for core NTT arithmetic; 5.4×–7.1× end-to-end KEM speedup, with a full statistical-analysis pipeline on Brown's OSCAR cluster. - -For the complete index — additional artifacts, deployed ML, and smaller tools — see [/cv/projects/](/cv/projects/). - -## Contact - -[ln@levineuwirth.org](mailto:ln@levineuwirth.org) · [ORCID 0009-0002-0162-3587](https://orcid.org/0009-0002-0162-3587) · [GitHub](https://github.com/levineuwirth) · [Forgejo](https://git.levineuwirth.org/neuwirth) diff --git a/levineuwirth.cabal b/levineuwirth.cabal index 240db82..1a0e9b7 100644 --- a/levineuwirth.cabal +++ b/levineuwirth.cabal @@ -19,6 +19,7 @@ executable site Catalog Commonplace Now + Vita Backlinks Dingbat SimilarLinks diff --git a/static/css/vita.css b/static/css/vita.css new file mode 100644 index 0000000..abdbc21 --- /dev/null +++ b/static/css/vita.css @@ -0,0 +1,304 @@ +/* vita.css — /about.html. + * + * The vita is a record surface. Where Now answers "what is moving right + * now" with a status chip per item, the vita answers "what is on file" + * and carries no temporality beyond the dates themselves — so it borrows + * item-card.css's card rhythm and now.css's section headings, but drops + * the badge column entirely. Entries here have no state to report. + * + * The one affordance this page has that the CV PDF cannot: every artifact + * is one click away. Link chips are therefore the only element given any + * interaction polish; everything else stays deliberately quiet so the + * chips read as the page's active surface. + */ + +/* ============================================================ + INTRO PROSE + Body content from about.md — the pointers and the four research + threads — set above the generated sections. Mirrors .now-intro + so the two data-driven pages open in the same register. + ============================================================ */ + +.vita-intro { + margin: 0 0 3rem; + font-family: var(--font-serif); + font-size: 1rem; + color: var(--text-muted); + line-height: 1.6; +} + +.vita-intro h2 { + font-family: var(--font-serif); + font-size: 1.15rem; + font-variant: all-small-caps; + font-feature-settings: "smcp" 1; + letter-spacing: 0.09em; + color: var(--text-muted); + font-weight: 400; + margin: 2rem 0 0.6rem; +} + +.vita-intro p:last-child { + margin-bottom: 0; +} + +/* ============================================================ + SECTIONS + Identical treatment to .now-section — same size, tracking, and + ink — because the two pages are siblings and a reader moving + between them should not have to re-learn the hierarchy. + ============================================================ */ + +.vita-section { + margin-bottom: 2.75rem; +} + +.vita-section-heading { + font-family: var(--font-serif); + font-size: 1.15rem; + font-variant: all-small-caps; + font-feature-settings: "smcp" 1; + letter-spacing: 0.09em; + color: var(--text-muted); + text-transform: none; + font-weight: 400; + margin: 0 0 0.85rem 0; +} + +/* ============================================================ + ENTRIES + .vita-card composes on .item-card, which is a flex row sized + for a badge column. The vita emits no badge, so the single + .item-card-main child simply takes the full width. + ============================================================ */ + +.vita-list { + margin: 0; +} + +.vita-entry-title { + font-family: var(--font-serif); + font-size: 1rem; + font-weight: 600; + color: var(--text); + line-height: 1.35; + margin: 0; +} + +/* Degree, role, position. Serif italic — the scholarly convention + for a title held, and it separates the role from the institution + above it without another weight or size step. */ +.vita-role { + font-family: var(--font-serif); + font-size: 0.95rem; + font-style: italic; + color: var(--text-muted); + margin: 0.15rem 0 0; + line-height: 1.4; +} + +.vita-gpa { + font-style: normal; + color: var(--text-faint); +} + +/* Author lists run long and are reference material rather than + reading material — sans, small, muted, tight. */ +.vita-authors { + font-family: var(--font-sans); + font-size: 0.82rem; + color: var(--text-muted); + margin: 0.2rem 0 0; + line-height: 1.45; +} + +/* Venue: journal, conference, or publication state. Italic serif is + the citation convention and distinguishes it from the author line + directly above without adding a color step. */ +.vita-venue { + font-family: var(--font-serif); + font-size: 0.92rem; + font-style: italic; + color: var(--text-muted); + margin: 0.2rem 0 0; + line-height: 1.45; +} + +/* Dates and location. Matches .item-card-date's register exactly — + tabular numerals so date columns stay optically aligned down the + list even though they are inline here. */ +.vita-meta { + font-family: var(--font-sans); + font-size: 0.72rem; + color: var(--text-faint); + margin: 0.25rem 0 0; + font-variant-numeric: tabular-nums; + line-height: 1.5; +} + +.vita-sep { + user-select: none; +} + +.vita-location { + color: var(--text-faint); +} + +a.vita-location { + text-decoration: underline; + text-decoration-color: var(--border); + text-decoration-thickness: 0.08em; + text-underline-offset: 0.2em; + transition: color var(--transition-fast), text-decoration-color var(--transition-fast); +} + +a.vita-location:hover { + color: var(--text-muted); + text-decoration-color: var(--border-muted); +} + +/* Presentation state ("Presented", "Accepted"). Sits on the meta + line rather than in a badge column, so it is set as small-caps + text with a leading separator instead of a bordered chip — a + frame here would compete with the link chips below it. */ +.vita-status { + font-family: var(--font-sans); + font-variant: all-small-caps; + letter-spacing: 0.07em; + color: var(--text-muted); +} + +.vita-status::before { + content: " · "; + color: var(--text-faint); +} + +/* Preamble / note lines — the CV's connective prose. Same register + as .item-card-abstract.is-full, unclamped. */ +.vita-note { + font-family: var(--font-sans); + font-size: var(--text-size-small); + color: var(--text-muted); + margin: 0.4rem 0 0; + line-height: 1.55; +} + +.vita-bullets { + margin: 0.4rem 0 0; + padding-left: 1.05rem; + list-style: none; +} + +.vita-bullets li { + font-family: var(--font-sans); + font-size: var(--text-size-small); + color: var(--text-muted); + line-height: 1.55; + margin: 0.3rem 0 0; + position: relative; +} + +/* Hairline dash rather than a bullet glyph: the entries are claims, + not an enumerated sequence, and a disc would assert more structure + than the content has. */ +.vita-bullets li::before { + content: ""; + position: absolute; + left: -1.05rem; + top: 0.72em; + width: 0.5rem; + height: 1px; + background: var(--border-muted); +} + +/* ============================================================ + LINK CHIPS + The page's one interactive surface, and the reason a vita on the + web beats a vita on paper. Geometry is lifted from .now-status so + the chip shape is already familiar from /current; the difference + is that these are links and therefore respond. + ============================================================ */ + +.vita-links { + margin: 0.5rem 0 0; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.vita-chip { + display: inline-block; + font-family: var(--font-sans); + font-size: 0.66rem; + font-variant: all-small-caps; + letter-spacing: 0.08em; + line-height: 1; + padding: 0.32em 0.55em 0.28em; + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-muted); + background: transparent; + white-space: nowrap; + text-decoration: none; + transition: + color var(--transition-fast), + border-color var(--transition-fast), + background-color var(--transition-fast); +} + +.vita-chip:hover { + color: var(--text); + border-color: var(--text-muted); + background: var(--surface-raised, transparent); +} + +.vita-chip:focus-visible { + outline: 2px solid var(--text-muted); + outline-offset: 2px; +} + +/* The equal-contribution legend. Belongs to the section, not to any + one entry, so it is set below the list in the faintest register + the page uses. */ +.vita-footnote { + font-family: var(--font-serif); + font-size: 0.85rem; + font-style: italic; + color: var(--text-faint); + margin: 0.9rem 0 0; +} + +/* ============================================================ + MOBILE (≤540px) + item-card.css already stacks its header at this breakpoint. The + vita's entries are single-column by construction, so only the + type steps down. + ============================================================ */ + +@media (max-width: 540px) { + .vita-entry-title { + font-size: 0.95rem; + overflow-wrap: anywhere; + } + + .vita-role, + .vita-venue { + font-size: 0.9rem; + } + + .vita-meta { + font-size: 0.68rem; + } + + .vita-chip { + font-size: 0.62rem; + padding: 0.28em 0.45em 0.24em; + } +} + +@media (prefers-reduced-motion: reduce) { + .vita-chip, + a.vita-location { + transition: none; + } +} diff --git a/templates/partials/head.html b/templates/partials/head.html index 8a03421..d5e7d26 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -48,6 +48,8 @@ $if(catalog)$$endif$ $if(commonplace)$$endif$ $if(now)$$endif$ $if(now)$$endif$ +$if(vita)$$endif$ +$if(vita)$$endif$ $if(build)$$endif$ $if(reading)$$endif$ $if(composition)$$endif$ diff --git a/templates/vita.html b/templates/vita.html new file mode 100644 index 0000000..ab380fc --- /dev/null +++ b/templates/vita.html @@ -0,0 +1,11 @@ +
    +

    $title$

    + +
    $body$
    + + $vita-education-html$ + $vita-experience-html$ + $vita-publications-html$ + $vita-presentations-html$ + $vita-contact-html$ +