Project structure#
app/prismo/ host pipeline (JAX), optimizer, figures, CLI
main.py `prismo run | validate-gradient | probe-objective | animate`
pipeline.py θ → objective, composed adjoint (tesseract-jax); container start-up; mesh authoring
optimizer.py move-limited NLopt MMA that survives a failed solve
density_filter.py Andreassen density filter (H matrix)
soref_bennett.py carriers → Δε, Δα
mesh_transfer.py nodal field → design-cell restriction
waveguide_mesh.py local (non-container) rib mesh author
outputs.py figures, gradient validation, line scan, animation
app/tests/ unit tests; solver doubles injected through `components=`
components/shared_code/ prismo_shared: Pydantic schemas shared by both Tesseracts and the app
components/tesseracts/
chargetransport/ Python 3.12 + Julia 1.10 worker: ChargeTransport.jl forward + discrete-adjoint VJP
tesseract_api.py apply (solve | reset), vector_jacobian_product; owns the worker process
scripts/worker.jl JSON-lines worker loop; ct_common.jl (system, continuation), ct_adjoint.jl (VJP)
Dockerfile.julia-base precompiled Julia depot (`make julia-base chargetransport`)
julia_env/ pinned Project.toml / Manifest.toml
gyptis/ conda FEniCS: shared-mesh author, eigenmode forward + eigen-adjoint VJP
tesseract_api.py apply (solve | write_mesh | design_cell_centroids | mode_field), VJP
tesseract_environment.yaml conda env (gyptis, legacy dolfin, Python 3.10)
<component>/test_cases/ JSON regression cases run by `make test <component>`
docs/ this documentation; docs/figures/ holds the README figures
scripts/ benchmark + the standalone eigen-adjoint prototype
Makefile the entry point: install, build, test, run, validate, figures, docs
Components#
Tesseract |
Image |
Solves |
Inputs → outputs |
Differentiation |
|---|---|---|---|---|
|
Python 3.12 + Julia 1.10 |
drift-diffusion on the silicon subdomain |
|
discrete adjoint: \(J_F^\top\) solve in the Julia worker |
|
conda, legacy FEniCS, Python 3.10 |
vector eigenmode on the full domain |
|
Hellmann–Feynman eigen-adjoint, one left/right eigenpair |
Both images install prismo_shared — the Pydantic schemas (MeshRef,
SorefBennettCoefficients, carrier fields, solver sessions) that define the
exchange format. The host app never imports a solver: it talks to the two
served containers through tesseract_core.Tesseract.
A run, step by step#
sequenceDiagram
autonumber
participant CT as ChargeTransport Tesseract
participant App as prismo (host, JAX)
participant GY as gyptis Tesseract
App->>GY: serve · write_mesh
GY-->>App: shared .msh, design-cell vertices
App->>CT: serve (mesh dir mounted)
App->>GY: apply + VJP at uniform background
GY-->>App: mode-overlap weights w_c
loop each MMA evaluation
App->>CT: apply(doping, 0 V) · apply(doping, −5 V)
CT-->>App: n, p per node
App->>GY: apply(design_epsilon)
GY-->>App: n_eff²
App->>GY: vector_jacobian_product
GY-->>App: ∂J/∂ε per design cell
App->>CT: vector_jacobian_product (0 V, −5 V)
CT-->>App: ∂J/∂N per node
App->>App: MMA step · checkpoint
end
App->>CT: reset · cold re-solve of the best design
App->>GY: mode_field
App->>App: figures, animation
init_tesseract_containersstarts both images (Tesseract.from_image(...).serve()), forwarding mesh size / contact offset / domain width to the gyptis author and the solve budget to the Julia worker; the output directory is bind-mounted into the ChargeTransport container for the mesh file.write_meshon gyptis authors the shared.msh; the host reads the design cell vertices and assembles the mesh-transfer matrix and the silicon design nodes; the density filter matrix is built on the design-node coordinates.read_mode_overlapruns one eigensolve + one adjoint at the uniform background to get the frozen overlap weights for the loss.The optimizer loop:
pipeline_with_terms(θ)evaluates the objective — filter, doping map, two ChargeTransport solves (0 V, −5 V), Soref–Bennett, transfer, eigensolve — andjax.gradof it pulls the two drift-diffusion adjoints and the eigen-adjoint. NLopt MMA proposes the next θ inside the move-limit box.checkpoint.jsonand a doping frame are written after every evaluation.Afterwards: mode field and swept-carrier figures at the optimum (warm), then a worker
resetand a cold re-solve of the best design; the headline is computed from the cold value. Figures and the animation are written tooutputs/;make figuresrasterizes them intodocs/figures/.
validate-gradient and probe-objective start from the same
build_pipeline_inputs so they solve on exactly the same mesh, filter,
transfer and seed as run.
Developer loop#
PRISMO_DEV_MOUNTS=1 make run-containersbind-mounts the hosttesseract_api.pyandprismo_sharedover both images, read-only — a Python component edit costs a container restart instead of an image rebuild. The CLI prints a banner whenever the mounts are active.PRISMO_CT_SCRIPTS_DIR=components/tesseracts/chargetransport/scriptsmounts the Julia sources over/tesseract/scripts; the sysimage still supplies the precompiled packages.make imagesshows whether each image is older than the last commit touching its sources. A dependency change (tesseract_requirements.txt,tesseract_environment.yaml,julia_env/*.toml) always needs a rebuild.PRISMO_CT_SOLVE_BUDGET_S/PRISMO_CT_JULIA_TIMEOUT_Sstretch the Julia solve budget and the request timeout for refined meshes.make testruns each component’s JSON regression cases against its image (tesseract run <image> test @case.json) and then the host unit tests (pytest app).make gen-tests <component> FILE=payload.jsoncaptures a new regression case from a liveapply.Tesseract’s own tools apply:
tesseract run ... --profiling --tracing,tesseract serve --debug,tesseract-runtimeagainst a baretesseract_api.pyfor a no-container debugging loop.