API Reference#
End-to-end differentiable pipeline: signed design field theta -> objective.
Composes density filter -> doping mapping -> ChargeTransport.jl (0V, -5V) -> Soref-Bennett coupling -> gyptis -> delta_n_eff into a single JAX-differentiable function, optionally minus a weighted first-order modal free-carrier loss.
- class prismo.pipeline.BiasPoint(bias_v, delta_neff, modal_loss_db_cm, vpi_lpi_v_cm, fom_v_db)[source]#
Bases:
NamedTupleOne operating point of the reported figures of merit.
delta_neffis measured against the same 0 V reference as the objective;modal_loss_db_cmis the modal free-carrier loss of the device at this bias (depletion removes carriers, so it falls as the bias deepens, unlike the fixed 0 V loss the objective reports).vpi_lpi_v_cmandfom_v_dbareinfwhere Δneff is zero, which the 0 V point always is.- Parameters:
- class prismo.pipeline.DesignNodes(indices: ndarray, n_mesh_nodes: int)[source]#
Bases:
objectWhich shared-mesh nodes carry a design variable, and how to place them back.
The design field is defined on the silicon nodes (
slab+rib_silicon) rather than on every node of the shared mesh. Only silicon nodes have physics attached to them: ChargeTransport gathers doping on the silicon subgrid and scatters carriers back from it, and every gyptis design cell is a rib triangle whose three vertices are silicon nodes. A variable on an oxide, substrate, clad or PML node therefore has an exactly-zero gradient unless the density filter happens to reach a silicon node, in which case it dopes silicon from outside the device – a degree of freedom with no physical referent either way.Restricting the design set drops those variables from the MMA problem and shrinks the dense filter matrix quadratically (it is
(n_design, n_design)). Downstream contracts are unchanged:scatter()places the filtered field back into full gmsh node order before the doping map, somesh_ref’s node ordering and the(n_design_cells, n_nodes)transfer still see a full-length nodal field. Non-design nodes scatter totheta = 0, i.e. net-intrinsic, which is what the oxide already meant.- Variables:
indices –
(n_design,)node indices into full gmsh node order.n_mesh_nodes – Number of nodes in the full shared mesh.
- Parameters:
indices (
ndarray)n_mesh_nodes (
int)
- classmethod all_nodes(n_mesh_nodes: int)[source]#
Every mesh node is a design node (the no-silicon-groups fallback).
- Parameters:
n_mesh_nodes (
int)
- indices: ndarray#
- class prismo.pipeline.PipelineComponents(chargetransport: Callable[[...], Any], gyptis: Callable[[...], Any], gyptis_background: Callable[[...], Any], design_cell_centroids: Callable[[], ndarray] | None = None, design_cell_vertices: Callable[[], ndarray] | None = None, write_mesh: Callable[[str | Path], ndarray] | None = None, mode_field: Callable[[ndarray, float], tuple[ndarray, ndarray]] | None = None, reset_chargetransport: Callable[[], None] | None = None, closers: tuple[Callable[[], None], ...] = ())[source]#
Bases:
objectThe differentiable components one
pipeline()call composes.Carries all backend state – container handles or local api modules and the per-lifecycle background eigenmode cache – so
pipeline()reads no module globals. The container lifecycle builds one on startup and releases it viaclose().design_cell_centroidsis the static geometry query bound to the same gyptis backend as the solve components: a zero-arg reader returning the(n_design, 2)centroids indesign_epsilonorder, so the pipeline setup can build the mesh-transfer operator through the seam without reaching for a raw container handle.mode_fieldis the matching read-only query for the tracked mode’s|E|profile, used by the headline mode figure. Both areNonewhen no gyptis backend is bound.- Parameters:
design_cell_centroids (
Callable[[],ndarray] |None, default:None)design_cell_vertices (
Callable[[],ndarray] |None, default:None)write_mesh (
Callable[[str|Path],ndarray] |None, default:None)mode_field (
Callable[[ndarray,float],tuple[ndarray,ndarray]] |None, default:None)reset_chargetransport (
Callable[[],None] |None, default:None)
- class prismo.pipeline.PipelineTerms(delta_neff, modal_loss_db_cm)[source]#
Bases:
NamedTupleThe two physical terms one pipeline evaluation yields.
delta_neffis the signed effective-index shift the optimizer has always maximized;modal_loss_db_cmis the first-order modal free-carrier loss of the unbiased device (nanwhen no mode-overlap weights were given). Both are JAX scalars and differentiable;pipeline()combines them.- Parameters:
delta_neff (
Array)modal_loss_db_cm (
Array)
- delta_neff: Array#
Alias for field number 0
- modal_loss_db_cm: Array#
Alias for field number 1
- prismo.pipeline.bias_sweep(theta: Array, bias_voltages: Sequence[float], H: Array | None = None, H_sum: Array | None = None, mesh_ref: MeshRef | None = None, background_epsilon: float | None = None, design_transfer: Array | None = None, design_nodes: DesignNodes | None = None, components: PipelineComponents | None = None, mode_overlap: Array | ndarray | None = None)[source]#
The reported figures of merit of one design over a range of biases.
A post-hoc characterization, not part of the objective: the optimizer works at the single operating point
REVERSE_BIAS_V, and this reruns the same physics – one ChargeTransport solve and one eigensolve per bias, against the shared 0 V equilibrium – to show how Δneff, the modal loss and VπLπ·alpha develop as the reverse bias deepens. Not differentiated.The 0 V equilibrium is solved once and reused as both the Soref-Bennett reference and, where
0.0is inbias_voltages, that point’s own carrier state: at zero bias Δneff is exactly zero and VπLπ is infinite, which is the honest reading of “no bias, no phase shift”.bias_voltagesare the biases to characterize [V], e.g.0to-5, andmode_overlapthe frozen weights fromread_mode_overlap()(without them the loss and the figure of merit readnan). The remaining arguments matchpipeline(). Returns oneBiasPointper bias, in the order given.- Parameters:
theta (
Array)H (
Array|None, default:None)H_sum (
Array|None, default:None)mesh_ref (
MeshRef|None, default:None)design_transfer (
Array|None, default:None)design_nodes (
DesignNodes|None, default:None)components (
PipelineComponents|None, default:None)mode_overlap (
Array|ndarray|None, default:None)
- prismo.pipeline.build_chargetransport_component(tesseract: Tesseract | None = None, *, rewrite_mesh_path: bool = False)[source]#
Build the ChargeTransport component bound to a served Tesseract.
tesseractis a served handle – a container image or an in-processtesseract_apiserved viaTesseract.from_tesseract_api().tesseract_jax.apply_tesseract()composes itsapply/vector_jacobian_productendpoints into the JAX program, so the returned callable differentiates straight through the drift-diffusion solve. With no handle, calling it raises – there is no physics-free identity fallback.rewrite_mesh_pathrewrites a hostmesh_refpath to the container’s read-only mount (see_CT_MESH_MOUNT); the in-process handle reads the host path directly, so it leaves the path alone.
- prismo.pipeline.build_default_components(mode_index: int = 0)[source]#
Build the default in-process components from the local tesseract apis.
Serves each component’s
tesseract_apiin-process viaTesseract.from_tesseract_api()when its module imports. There is no physics-free stub: a component whose tesseract_api has no live solver (no Julia, no gyptis/FEniCS) raises when called rather than fabricating carriers or an effective index. Used whenpipeline()is called without an explicit bundle (no containers running).mode_indexis the guided mode the gyptis solves target (seebuild_gyptis_components()); the shared module-level bundle is built for the fundamental.- Parameters:
mode_index (
int, default:0)
- prismo.pipeline.build_design_transfer(components: PipelineComponents, node_coords: ndarray, design_cell_vertices: ndarray | None = None)[source]#
Assemble the dense mesh-transfer matrix for the container gyptis path.
Both solvers share one gmsh geometry, so the transfer is an exact local restriction: each gyptis design cell is a triangle of the shared mesh whose three vertices are shared-mesh nodes. Reads the design-cell vertices from the gyptis backend (in
design_epsilonorder) and matches them tonode_coords(the same shared-mesh nodes the ChargeTransport solve and the density filter use), returning the(n_design_cells, n_nodes)matrix that feedspipeline(design_transfer=...)directly.- Parameters:
components (
PipelineComponents)node_coords (
ndarray)design_cell_vertices (
ndarray|None, default:None)
- prismo.pipeline.build_gyptis_components(tesseract: Tesseract | None = None, mode_index: int = 0)[source]#
Build the perturbed and background gyptis components for a served handle.
tesseractis a served handle – a container image or an in-processtesseract_apiserved viaTesseract.from_tesseract_api(). The perturbed component composes itsapply/vector_jacobian_productendpoints throughtesseract_jax.apply_tesseract(), soneff_sqdifferentiates straight through the eigensolve. With no handle, calling either component raises – there is no physics-free fallback.The background solve is rho-independent (its field is a uniform
backgroundof the design length), so it must contribute an exact zero design gradient and run only once per geometry. It therefore runs forward-only behind apure_callbackguarded bystop_gradientand a per-lifecycle cache, rather than throughapply_tesseract– no eigen-adjoint is ever assembled for it.mode_indexselects the guided mode every solve of this bundle targets:0the fundamental (largest neff in the guided window),kthek-th guided mode in descending neff. The component ranks the modes on the first solve of a geometry and tracks the chosen branch by nearest eigenvalue thereafter, so Δneff is the shift of that mode throughout an optimization. The index is baked into the bundle rather than threaded throughpipeline(): one run optimizes one mode.
- prismo.pipeline.carrier_fields(theta: Array, H: Array | None = None, H_sum: Array | None = None, mesh_ref: MeshRef | None = None, design_nodes: DesignNodes | None = None, components: PipelineComponents | None = None)[source]#
The carrier densities behind one design:
(n0, p0, n1, p1)[cm^-3].Filter -> doping -> ChargeTransport at 0 V and at the reverse bias, in full node order, for the headline depletion figure (swept carriers under the mode). Not differentiated; the same two solves the pipeline runs.
- Parameters:
theta (
Array)H (
Array|None, default:None)H_sum (
Array|None, default:None)mesh_ref (
MeshRef|None, default:None)design_nodes (
DesignNodes|None, default:None)components (
PipelineComponents|None, default:None)
- prismo.pipeline.default_components()[source]#
The shared in-process components
pipeline()uses without a bundle.Built on first use, not at import: the gyptis tesseract_api reads its geometry knobs (
PRISMO_GYPTIS_*, seegyptis_author_env()) when it loads, so the CLI can set them for an in-process run before the bundle exists.
- prismo.pipeline.design_epsilon_from_theta(theta: Array, H: Array | None = None, H_sum: Array | None = None, mesh_ref: MeshRef | None = None, background_epsilon: float | None = None, design_transfer: Array | None = None, design_nodes: DesignNodes | None = None, components: PipelineComponents | None = None)[source]#
Everything
pipeline()does up to the eigensolves.Runs filter -> signed doping -> both ChargeTransport solves -> Soref-Bennett -> mesh transfer, and returns the
(epsilon_bg, epsilon_pert)design-cell permittivity fields the two gyptis solves consume. Split out ofpipeline()so the same permittivity the objective was evaluated on can be handed to the mode-field query for the headline figure, rather than reconstructing the chain a second way.Arguments match
pipeline().- Parameters:
theta (
Array)H (
Array|None, default:None)H_sum (
Array|None, default:None)mesh_ref (
MeshRef|None, default:None)design_transfer (
Array|None, default:None)design_nodes (
DesignNodes|None, default:None)components (
PipelineComponents|None, default:None)
- prismo.pipeline.dev_mounts_requested()[source]#
PRISMO_DEV_MOUNTSis set to a truthy value (1/true/yes).
- prismo.pipeline.free_carrier_absorption_cm(electrons_cm3: Array, holes_cm3: Array, coeffs: SorefBennettCoefficients | None = None)[source]#
Absolute Soref-Bennett free-carrier absorption per node [cm^-1].
alpha = C_e * N_e^D_e + C_h * N_h^D_hfrom the absolute carrier densities (cm^-3), not the equilibrium-subtracted perturbation the permittivity uses: the insertion loss of a doped waveguide is set by every carrier the mode sees. At 1e19 cm^-3 this is ~85 cm^-1 (electrons) and ~60 cm^-1 (holes), i.e. hundreds of dB/cm.- Parameters:
electrons_cm3 (
Array)holes_cm3 (
Array)coeffs (
SorefBennettCoefficients|None, default:None)
- prismo.pipeline.gyptis_author_env(mesh_size: float | None = None, contact_offset: float | None = None, domain_width: float | None = None)[source]#
Environment knobs the gyptis mesh author reads, for the values given.
The gyptis tesseract_api sizes the shared mesh from
PRISMO_GYPTIS_MESH_SIZE/PRISMO_GYPTIS_CONTACT_OFFSET/PRISMO_GYPTIS_WIDTHwhen it loads. The container path passes them to the image; the in-process path exports them before the module is first imported.Nonekeeps the author’s default.
- prismo.pipeline.init_tesseract_containers(mesh_dir: str | Path | None = None, mesh_size: float | None = None, mode_index: int = 0, contact_offset: float | None = None, domain_width: float | None = None)[source]#
Start tesseract Docker containers and bundle the live components.
Returns a
PipelineComponentsthe caller owns and passes topipeline(). Containers stay running until the bundle’sclose()is called (seeteardown_containers). Startup is all-or-nothing so a container run can never silently use local component stubs.- Parameters:
mesh_dir (
str|Path|None, default:None) – Host directory holding the shared.mshfile. It is bind-mounted read-only into the ChargeTransport container at_CT_MESH_MOUNTso its Julia solver can load the real 2D grid instead of the 1D fallback. The bind mount is live, so a mesh written after the container starts is still visible.mesh_size (
float|None, default:None) – Characteristic element size of the silicon (rib + slab) in micrometres, passed to the gyptis container asPRISMO_GYPTIS_MESH_SIZE. gyptis authors the shared mesh, so this one knob sets the resolution both solvers see.Nonekeeps the component’s own default (0.04 µm).mode_index (
int, default:0) – Guided mode the gyptis solves target and track –0the fundamental,kthek-th guided mode in descending neff (seebuild_gyptis_components()).contact_offset (
float|None, default:None) – Gap from the rib edge to the near contact edge [µm], passed to the gyptis mesh author asPRISMO_GYPTIS_CONTACT_OFFSET.Nonekeeps the component’s default (0.2 µm).domain_width (
float|None, default:None) – Physical box width [µm] (the slab spans it; the PML lies outside), passed asPRISMO_GYPTIS_WIDTH.Nonekeeps the default (2.0 µm).
- prismo.pipeline.loss_figure_of_merit_v_db(delta_neff: float | Array, modal_loss_db_cm_value: float | Array, bias_v: float = -5.0)[source]#
VπLπ x alpha[V·dB]: the literature’s efficiency-loss figure of merit.Smaller is better; good depletion modulators reach ~10-30 V·dB. Like VπLπ it is reported, not optimized, and inherits VπLπ’s sign.
- prismo.pipeline.modal_loss_db_cm(alpha_cells_cm: Array, mode_overlap: Array, neff_background: Array | float, background_index: float = 3.4757)[source]#
Modal free-carrier loss [dB/cm] from the per-cell absorption, first order.
With
w_cell = d(neff^2)/d(eps_cell)the mode’s sensitivity to the design-cell permittivity (the Hellmann-Feynman adjoint gyptis already computes), an imaginary permittivityIm(eps) = n_si*alpha*lambda/(2*pi)in each cell shiftsIm(neff^2)bysum(w*Im(eps)), and the modal power loss2*k0*Im(neff)is(n_si/neff) * sum(w_cell * alpha_cell); the wavelength cancels. For a uniform core this is the textbook confinement-weighted lossGamma * alpha * n_si/neff. The weights are those of the unperturbed (background) mode, frozen: the carrier-induced permittivity shift is ~1e-3 and does not reshape the mode.
- prismo.pipeline.pipeline(theta: Array, H: Array | None = None, H_sum: Array | None = None, mesh_ref: MeshRef | None = None, background_epsilon: float | None = None, design_transfer: Array | None = None, design_nodes: DesignNodes | None = None, components: PipelineComponents | None = None, loss_weight: float = 0.0, mode_overlap: Array | ndarray | None = None)[source]#
Signed design field theta -> objective (Δneff, optionally loss-penalized).
- Parameters:
theta (
Array) – Signed design field per design node in [-1, 1], shape(n_design,)– the silicon nodes whendesign_nodesis given, otherwise every mesh node. Its sign is the free P/N polarity (junction = zero-crossing).H (
Array|None, default:None) – Dense filter matrix, shape(n_design, n_design). Skip filter ifNone.H_sum (
Array|None, default:None) – Pre-computed row sums ofH.mesh_ref (
MeshRef|None, default:None) –MeshRefforwarded to ChargeTransport calls.background_epsilon (
float|None, default:None) – Background Si relative permittivity (default:n_si^2 = 3.4757^2).design_transfer (
Array|None, default:None) – Dense(n_design_cells, n_nodes)mesh-transfer matrix carrying the nodal perturbation onto the gyptis design cells.Nonemaps the perturbation node-for-node (identity).design_nodes (
DesignNodes|None, default:None) – Which shared-mesh nodesthetaaddresses. The filtered field is scattered back to full node order before the doping map, so everything downstream still sees(n_nodes,).Nonemeansthetaalready spans every mesh node.components (
PipelineComponents|None, default:None) – Live differentiable components to compose. Defaults to the in-process components built from the local tesseract apis.loss_weight (
float, default:0.0) – Weightwof the modal free-carrier loss in the objectiveΔneff - w * alpha_mode[neff per dB/cm].0(default) optimizes Δneff alone.mode_overlap (
Array|ndarray|None, default:None) –(n_design_cells,)mode-overlap weights fromread_mode_overlap(). Required whenloss_weight > 0; without them the loss is not evaluated.
- Returns:
Signed effective-index shift
Δneff = Re[neff(-5V)] - Re[neff(0)], minusloss_weighttimes the modal loss in dB/cm. Smooth and differentiable through zero; depletion (the physical bias response) makes Δneff positive. ReportVπLπfrom it viavpi_lpi_v_cm()and the two terms viapipeline_with_terms().- Return type:
Array
- prismo.pipeline.pipeline_with_terms(theta: Array, H: Array | None = None, H_sum: Array | None = None, mesh_ref: MeshRef | None = None, background_epsilon: float | None = None, design_transfer: Array | None = None, design_nodes: DesignNodes | None = None, components: PipelineComponents | None = None, loss_weight: float = 0.0, mode_overlap: Array | ndarray | None = None)[source]#
pipeline()returning(objective, terms)forhas_auxcallers.The objective is
delta_neff - loss_weight * modal_loss_db_cm; withloss_weight == 0it is exactlydelta_neffand the loss is only reported (nanwhen nomode_overlapis bound). Seepipeline()for the arguments.- Parameters:
theta (
Array)H (
Array|None, default:None)H_sum (
Array|None, default:None)mesh_ref (
MeshRef|None, default:None)design_transfer (
Array|None, default:None)design_nodes (
DesignNodes|None, default:None)components (
PipelineComponents|None, default:None)loss_weight (
float, default:0.0)mode_overlap (
Array|ndarray|None, default:None)
- prismo.pipeline.read_gyptis_design_cell_centroids(*, tesseract: Tesseract | None = None)[source]#
Read gyptis design-cell centroids in
design_epsilonfield order.- Parameters:
tesseract (
Tesseract|None, default:None)
- prismo.pipeline.read_gyptis_design_cell_vertices(*, tesseract: Tesseract | None = None)[source]#
Read the
(n_design, 3, 2)design-cell vertex coordinates.Each gyptis design cell is a triangle of the shared unified mesh; its three vertices are shared-mesh nodes. The host matches these coordinates to node indices to assemble the exact node->DG0-cell restriction operator. Exposed through the
write_meshoperation.- Parameters:
tesseract (
Tesseract|None, default:None)
- prismo.pipeline.read_gyptis_mode_field(design_epsilon: ndarray, core_epsilon: float = 12.080490489999999, *, tesseract: Tesseract | None = None, mode_index: int = 0)[source]#
Read the tracked mode’s
|E|profile at the gyptis mesh vertices.Solves once at
design_epsilonand returns(abs_e, coords)– the peak-normalized magnitude per vertex and the matching(n_vertices, 2)vertex coordinates in micrometres. This is the headline optical-mode figure’s data source; it is a read-only query, so it does not advance the component’s tracked mode branch.core_epsilonmust be the same background the solve components were given: it is part of the geometry key the component tracks the mode branch by, so a mismatched value would both solve a different device and miss the tracked branch, falling back to the untracked mode selection. Likewisemode_indexmust match the solve components’ target: it is part of the same branch key, so the figure shows the mode that was optimized.
- prismo.pipeline.read_mode_overlap(components: PipelineComponents, n_design_cells: int, background_epsilon: float = 12.080490489999999)[source]#
The mode-overlap weights
d(neff^2)/d(eps_cell)at the uniform background.One eigensolve plus one eigen-adjoint of the bound gyptis component on the rho-independent background permittivity. The result feeds
pipeline(mode_overlap=...)as a constant: the loss term is first order in the carrier perturbation, so the weights are frozen at the unperturbed mode rather than re-derived (which would need second-order eigen- derivatives the adjoint does not provide).- Parameters:
components (
PipelineComponents)n_design_cells (
int)background_epsilon (
float, default:12.080490489999999)
- prismo.pipeline.reset_chargetransport_worker(*, tesseract: Tesseract | None = None)[source]#
Drop the ChargeTransport worker’s warm solutions.
The next solve is then a function of the doping alone – the cold continuation from near-intrinsic equilibrium and the bias ramp – rather than of the Newton starting points the previous designs left behind. Carried as the
resetoperation of the component’sapplyendpoint.- Parameters:
tesseract (
Tesseract|None, default:None)
- prismo.pipeline.seed_design_field(coords: ndarray, kind: str = 'lateral')[source]#
Seed one of
SEED_KINDSon the design nodes at|theta| = 0.3.lateral:seed_signed_junction()– n-type left of the median x, p-type right of it.vertical: in the rib, n-type below the rib’s mid-height and p-type above it; a p column along the rib’s right wall joins the p top to the right slab, and the slab itself is lateral so both regions reach their contact.u: n-type wraps under (lower third of the rib) and beside (left wall) a p core that, with the right wall and the right slab, is p-type. All three are reverse-biased with the cathode on the right.- Parameters:
coords (
ndarray)kind (
str, default:'lateral')
- prismo.pipeline.seed_signed_junction(coords: ndarray)[source]#
Seed a signed lateral P/N junction across the mesh in every run path.
Nodes left of the median x seed n-type (
+0.3), nodes to the right seed p-type (-0.3). With the cathode on the right at -5 V, this is the reverse-bias orientation. sign(theta) remains free, so the optimizer can move, dissolve, or reverse this junction.- Parameters:
coords (
ndarray)
- prismo.pipeline.teardown_containers(components: PipelineComponents)[source]#
Stop and remove the running tesseract containers owned by
components.- Parameters:
components (
PipelineComponents)
- prismo.pipeline.vpi_lpi_v_cm(delta_neff: float | Array, bias_v: float = -5.0)[source]#
Half-wave voltage-length product VπLπ [V·cm] from signed Δneff.
A carrier-depletion phase modulator accrues
φ = (2π/λ)·Δneff·L, so the length for a π shift at the fixed reverse bias isLπ = λ/(2·Δneff)andVπLπ = |V_bias|·λ/(2·Δneff). This is the field-standard modulation- efficiency headline (smaller|VπLπ|is better); it is reported, not optimized – signed Δneff is the optimized proxy. VπLπ carries Δneff’s sign (a wrong-polarity optimum reads negative) and diverges as Δneff → 0.bias_vis the bias Δneff was measured at; it defaults to the operating point the objective uses, and the bias sweep passes its own.
- prismo.pipeline.write_gyptis_mesh(mesh_path: str | Path, *, tesseract: Tesseract | None = None)[source]#
Persist gyptis’ unified mesh on host and return its design-cell vertices.
Move-limited MMA optimization loop for the PRISMO pipeline.
Wraps the JAX-differentiable pipeline in NLopt’s MMA, one fresh MMA subproblem
per outer step inside a move-limit box.
Design variables: the signed design field theta in [-1, 1], one entry per
design node (the silicon nodes of the shared mesh; see pipeline.DesignNodes).
Objective: maximize signed delta_n_eff, optionally minus a weighted modal
free-carrier loss.
- exception prismo.optimizer.OptimizationCancelled[source]#
Bases:
ExceptionRaised when the user interrupts the optimization loop.
- prismo.optimizer.optimize_doping(initial_rho: ndarray | None = None, n_nodes: int | None = None, H: Array | None = None, H_sum: Array | None = None, mesh_coords: ndarray | None = None, mesh_path: str | Path | None = None, r_min: float = 0.05, *, max_iter: int = 200, ftol_rel: float = 0.001, move_limit: float = 0.05, max_move_halvings: int = 8, checkpoint_path: str | Path | None = None, use_jit: bool = True, on_iteration: Callable[[int, ndarray], None] | None = None, design_transfer: Array | None = None, design_nodes: DesignNodes | None = None, mesh_ref: MeshRef | None = None, components: PipelineComponents | None = None, loss_weight: float = 0.0, mode_overlap: ndarray | Array | None = None)[source]#
Run the move-limited MMA optimization loop.
- Parameters:
initial_rho (
ndarray|None, default:None) – Starting signed design field(n_design,)in[-1, 1]– one entry per design node, which on a real mesh is the silicon nodes rather than every mesh node. Defaults to a uniform 0.25 fallback; the real run seeds a signed junction (main.py). The name is retained for call-site compatibility.n_nodes (
int|None, default:None) – Number of design variables (derived frominitial_rhoif given, else required whenH/mesh_coords/mesh_pathis provided).H (
Array|None, default:None) – Dense filter matrix(n_design, n_design). Built frommesh_coordsormesh_pathif omitted.H_sum (
Array|None, default:None) – Pre-computed row sums ofH.mesh_coords (
ndarray|None, default:None) –(n_design, 2)design-node coordinates for building the filter matrix.mesh_path (
str|Path|None, default:None) – Path to a.mshfile for building the filter matrix (requiresgmsh).r_min (
float, default:0.05) – Filter radius in micrometres – the unit the shared mesh’s coordinates are authored in (default 0.05, i.e. 50 nm).max_iter (
int, default:200) – Maximum number of physics evaluations (optimizer iterations).ftol_rel (
float, default:0.001) – Relative tolerance on the objective: the loop stops once an accepted full-move-limit step improves the objective by less than this fraction.move_limit (
float, default:0.05) – Per-iteration move limit on theta: no design variable moves more than this in one iteration.max_move_halvings (
int, default:8) – Consecutive halvings of the move limit (after failed or non-improving steps) before the iterate is declared stalled.checkpoint_path (
str|Path|None, default:None) – Where to write{"rho_opt", "history", ...}after every evaluation, so a killed run still yields a figure.Nonewrites no checkpoint.use_jit (
bool, default:True) – JIT-compile combined objective and gradient computation.on_iteration (
Callable[[int,ndarray],None] |None, default:None) – Optional callback receiving an iteration number and its candidate design field immediately before its solver evaluation.design_transfer (
Array|None, default:None) – Dense(n_design_cells, n_nodes)mesh-transfer matrix carrying the nodal perturbation onto the gyptis design cells.Nonemaps the perturbation node-for-node (identity).design_nodes (
DesignNodes|None, default:None) – Which shared-mesh nodes the design vector addresses (the silicon nodes on a real mesh). Forwarded topipeline, which scatters the filtered field back to full node order.Nonemeans one variable per mesh node.mesh_ref (
MeshRef|None, default:None) – Shared-mesh reference forwarded to the ChargeTransport solves so they run on the real 2D grid.Noneleaves CT on its 1D fallback device.components (
PipelineComponents|None, default:None) – Live pipeline components to compose. Defaults to the in-process components (seepipeline).loss_weight (
float, default:0.0) – Weight of the modal free-carrier loss in the objectivedelta_neff - loss_weight * loss_db_cm.0keeps the pure Δneff objective; the loss is still recorded whenmode_overlapis given.mode_overlap (
ndarray|Array|None, default:None) –(n_design_cells,)mode-overlap weights frompipeline.read_mode_overlap; required for a positiveloss_weight.
- Returns:
(rho_opt, history)whererho_optis the best design whose physics solved andhistoryis a list of per-evaluation records:objective(what MMA maximized),delta_n_eff,modal_loss_db_cm(Nonewhen not evaluated), step and timing fields.- Raises:
OptimizationCancelled – If the user interrupts with Ctrl-C.
ValueError – On a non-positive move limit or missing sizing inputs.
- Return type:
- prismo.density_filter.apply_filter(rho: ndarray, H: csr_matrix)[source]#
Apply density filter: rho_tilde = H @ rho / H_sum.
- Parameters:
rho (
ndarray)H (
csr_matrix)
- prismo.density_filter.assemble_filter_matrix(mesh_coords: ndarray, r_min: float = 0.05)[source]#
Build sparse convolution filter matrix from node coordinates.
H_ij = max(0, r_min - dist(i,j)) Returns csr_matrix.
r_minis a distance in whatever length unitmesh_coordsis authored in – the two must agree. The PRISMO run paths author both in micrometres (seeprismo.waveguide_mesh), which is what the0.05default (a 50 nm radius) is expressed in; a radius in the wrong unit silently turns the filter into either an all-pairs average or the identity rather than failing.- Parameters:
mesh_coords (
ndarray)r_min (
float, default:0.05)
- prismo.density_filter.vjp_filter(cotangent: ndarray, H: csr_matrix)[source]#
Adjoint of density filter: dJ/drho = H @ (dJ/drho_tilde / H_sum).
The cotangent is normalized by H_sum before the matmul: the forward pass normalizes each output row by H_sum_i, so the adjoint divides the incoming cotangent by H_sum_i before propagating through H.
- Parameters:
cotangent (
ndarray)H (
csr_matrix)
Carrier-field -> gyptis-design-cell mesh-transfer operator.
Both solvers now share one gmsh geometry, so the transfer is an
exact local restriction rather than a cross-mesh interpolation: every gyptis
design cell is a triangle of the shared mesh whose three vertices are shared-mesh
nodes. The design-cell value is the mean of its three vertex nodal values – the
value a linear nodal field takes at the cell centroid – so the operator has
weight 1/3 on each vertex node and nothing else.
The result is a static, sparse operator T of shape
(n_design_cells, n_nodes). Two properties hold by construction:
Silicon-only support. A design cell is a silicon rib cell, so only silicon nodes carry weight; oxide-only nodes have zero columns.
Partition of unity. Each row sums to one (three weights of
1/3), so a uniform nodal field maps to the same uniform field on the design cells – the invariant the pipeline relies on to keep the background solve rho-independent.
- class prismo.mesh_transfer.MeshTransferOperator(matrix: csr_matrix)[source]#
Bases:
objectA static nodal-field -> design-cell restriction, as a sparse matrix.
- Variables:
matrix – Sparse
(n_design_cells, n_nodes)operator. Calling the instance applies it to a nodal field.- Parameters:
matrix (
csr_matrix)
- dense()[source]#
The operator as a dense
(n_design_cells, n_nodes)array.The pipeline needs a dense, JAX-traceable matrix to apply the transfer inside an autodiff pass; pass
operator.dense()asdesign_transfer.
- matrix: csr_matrix#
- prismo.mesh_transfer.build_mesh_transfer_operator(node_coords: ndarray, design_cell_vertices: ndarray, tol: float = 1e-06)[source]#
Build the exact nodal-field -> design-cell restriction operator.
- Parameters:
node_coords (
ndarray) –(n_nodes, 2)shared-mesh node coordinates, in the same frame and units asdesign_cell_vertices.design_cell_vertices (
ndarray) –(n_design_cells, 3, 2)vertex coordinates of each gyptis design cell (fromwrite_mesh/gyptis.tesseract_api.design_cell_vertices); the operator’s row order follows this array. Each vertex is a shared-mesh node.tol (
float, default:1e-06) – Maximum distance for matching a design-cell vertex to a shared-mesh node. The two are the same mesh, so a match is essentially exact; the tolerance only absorbs gmsh/dolfin float round-trip.
- Returns:
A
MeshTransferOperatorwrapping the sparse(n_design_cells, n_nodes)matrix with weight1/3on each of a cell’s three vertices.- Return type:
Shared waveguide mesh generation for the PRISMO pipeline.
Generates a 2D SOI rib waveguide cross-section mesh as a Gmsh .msh file.
Both the ChargeTransport.jl and gyptis Tesseracts consume the same mesh file at runtime via
MeshRef.path. Physical-group naming is defined here so both integrators
agree on which subdomains are contacts, silicon, and oxide.
Units and vocabulary follow the shared-mesh contract, not SI.
A container run authors the shared mesh with gyptis, an in-process run authors
it here, and ChargeTransport reads whichever one was written: ct_common.jl
scales the grid it loads by MICROMETRES_TO_METRES and collects its silicon
cells from the slab and rib_silicon physical groups. Both authors
therefore emit micrometre coordinates under gyptis’ group names – and so
do the geometry dimensions below, the --r-min filter radius, and the node
coordinates the plots consume. Authoring this mesh in metres instead made the
µm-scaled default filter radius an all-pairs filter over a 3 µm device (which
collapses the design field to its global mean) and left the Julia silicon
lookup with no group it recognised.
Geometry (cross-section, not to scale):
┌───────────────────────────────────────┐ ← SiO₂ cladding
│ │
│ ┌─────────────┐ │ ← Si rib (220 nm x 500 nm)
│ │ │ │
├─────────┤ ├─────────┬─────┤ ← Si slab (100 nm)
│ │ │ │ │ ← Al contacts on slab shoulders
│ └─────────────┘ │ │
│ │
├───────────────────────────────────────┤ ← SiO₂ substrate
│ │
└───────────────────────────────────────┘
Physical groups exported to the .msh file:
slab, rib_silicon, oxide, contact_anode, contact_cathode.
- class prismo.waveguide_mesh.RibWaveguideGeometry(*, rib_thickness: float = 0.22, rib_width: float = 0.5, slab_thickness: float = 0.1, substrate_thickness: float = 0.5, cladding_thickness: float = 0.5, box_width: float = 3.0, contact_width: float = 0.05, contact_offset: float = 0.2, mesh_res_junction: float = 0.01, mesh_res_core: float = 0.02, mesh_res_bulk: float = 0.05, silicon_index: float = 3.4757, oxide_index: float = 1.444, wavelength: float = 1.55)[source]#
Bases:
objectSOI rib waveguide cross-section dimensions and material properties.
Every length is in micrometres, matching the shared-mesh contract this module documents; the refractive indices are dimensionless.
- Parameters:
rib_thickness (
float, default:0.22)rib_width (
float, default:0.5)slab_thickness (
float, default:0.1)substrate_thickness (
float, default:0.5)cladding_thickness (
float, default:0.5)box_width (
float, default:3.0)contact_width (
float, default:0.05)contact_offset (
float, default:0.2)mesh_res_junction (
float, default:0.01)mesh_res_core (
float, default:0.02)mesh_res_bulk (
float, default:0.05)silicon_index (
float, default:3.4757)oxide_index (
float, default:1.444)wavelength (
float, default:1.55)
- prismo.waveguide_mesh.build_rib_waveguide_mesh(mesh_path: str | Path | None = None, geometry: RibWaveguideGeometry | None = None)[source]#
Build the SOI rib waveguide mesh and write a
.mshfile.- Parameters:
mesh_path (
str|Path|None, default:None) – Destination path for the.mshfile. Defaults tooutputs/waveguide.msh.geometry (
RibWaveguideGeometry|None, default:None) – Geometry parameters. Defaults toRibWaveguideGeometry().
- Returns:
Absolute path to the generated mesh file.
- Return type:
- prismo.waveguide_mesh.build_rib_waveguide_mesh_via_gmsh(mesh_path: str, geometry: RibWaveguideGeometry, *, view: bool = False)[source]#
Low-level mesh builder using gmsh Python API directly.
Exported so tests can call it after
gmsh.initialize().- Parameters:
mesh_path (
str)geometry (
RibWaveguideGeometry)view (
bool, default:False)
- prismo.waveguide_mesh.read_mesh_node_coordinates(mesh_path: str | Path)[source]#
Extract 2D node coordinates from a Gmsh
.mshv4 file.Returns a
(n_nodes, 2)float64 array of (x, y) positions, suitable for feeding intoprismo.density_filter.assemble_filter_matrix().Requires
gmshto be importable.
- prismo.waveguide_mesh.read_mesh_silicon_triangulation(mesh_path: str | Path)[source]#
Extract zero-based silicon triangle node indices from a Gmsh mesh.
The returned indices address the coordinate rows from
read_mesh_node_coordinates(). Only dim-2, three-node triangle elements in theSILICON_GROUP_NAMESphysical groups (slabandrib_silicon, the same pairct_common.jlcollects) are included, so the reader spans the whole silicon domain of either mesh author. As with the node coordinate reader, an environment without Gmsh returns an empty array so no-backend code paths remain importable.
Soref-Bennett free-carrier plasma dispersion coupling layer.
Converts carrier-density perturbations from the charge-transport solver into optical permittivity and absorption perturbations for gyptis via the Soref-Bennett model (Soref & Bennett, IEEE JQE 23(1), 1987).
The charge-transport solver reports carrier densities in m^-3 while the Soref-Bennett coefficients are calibrated for cm^-3, so densities are converted before the model is applied (1 m^-3 = 1e-6 cm^-3).
Delta_n = -(A_e * Delta_N_e^B_e + A_h * Delta_N_h^B_h) Delta_alpha = C_e * Delta_N_e^D_e + C_h * Delta_N_h^D_h Delta_epsilon = 2 * n_bg * Delta_n (first-order expansion)
Pure, element-wise and vectorized — no Python loops. Operates on lists,
NumPy arrays, or JAX arrays (converted via np.asarray).
- prismo.soref_bennett.soref_bennett(carriers: CarrierDensityField, coeffs: SorefBennettCoefficients | None = None)[source]#
Compute Soref-Bennett permittivity and absorption perturbations.
- Parameters:
carriers (
CarrierDensityField) –CarrierDensityFieldwithelectrons,holesand equilibrium densities (all [m^-3]).coeffs (
SorefBennettCoefficients|None, default:None) –SorefBennettCoefficients; defaults to the lambda = 1.55 um silicon values if omitted.
- Returns:
SorefBennettResultwithdelta_permittivityanddelta_absorption(both element-wise lists).- Raises:
ValueError – if equilibrium carrier densities are not supplied.
- Return type:
SorefBennettResult
Paper-ready visualization outputs for the PRISMO pipeline.
The headline figure set. generate_outputs emits four: the MMA convergence
curve carrying both the optimized signed Δneff and the reported VπLπ, the
adjoint-vs-finite-difference gradient validation, the initial-vs-optimized
signed θ field, and the optical mode |E| on the rib cross-section. Panels that serve none of those were dropped. A
loss-aware run adds the loss history, the Δneff-vs-loss trade-off
plane, and an animation of the doping field over the optimizer’s evaluations.
A run that swept the bias adds the initial-vs-optimized figures of merit
against reverse bias.
- class prismo.outputs.ColdReevaluation(warm_delta_neff: float, cold_delta_neff: float, tolerance: float = 0.0001)[source]#
Bases:
objectWarm-vs-cold Δneff of the reported design.
warm_delta_neffis the value the optimizer saw (solved from the neighbouring designs’ Newton starting points);cold_delta_neffis the same design solved after the ChargeTransport worker dropped every warm solution. The headline (and VπLπ) is the cold value.
- class prismo.outputs.GradientValidationResult(n_directions: int, best_rel_errors: list[float], worst_rel_error: float, tolerance: float, passed: bool, figure_path: Path)[source]#
Bases:
objectOutcome of an adjoint-vs-finite-difference gradient check.
best_rel_errors[i]is the smallest relative error over the feasible finite-difference steps for directioni– the point where central FD is closest to the adjoint’s directional derivative before rounding or the CT readout’s own FD floor takes over.worst_rel_erroris the largest of those per-direction minima; the gradient passes when it stays at or belowtolerance.- Parameters:
- class prismo.outputs.ModeField(abs_e: ndarray, coords_um: ndarray, rib_bounds: tuple[float, float, float, float] | None = None, mode_index: int = 0)[source]#
Bases:
objectThe tracked optical mode’s
|E|profile on the shared mesh.abs_eis the peak-normalized magnitude at each mesh vertex andcoords_umthe matching(n_vertices, 2)vertex coordinates. Unlike the doping-field plots – which take SI node coordinates and scale them – these come straight off the gyptis mesh and are already in micrometres, so the plot uses them unscaled.rib_boundsis the optional(x_min, x_max, y_min, y_max)rectangle of the silicon rib interior, in the same micrometre frame, drawn as an outline so the mode can be read against the guiding core.mode_indexis the guided-mode order the solve targeted (0 = fundamental), shown in the figure title.- Parameters:
- abs_e: ndarray#
- coords_um: ndarray#
- class prismo.outputs.ObjectiveLineScan(offsets: ndarray, values: ndarray, fit_coefficients: ndarray, residuals: ndarray, rms_rel_residual: float, max_rel_residual: float, noise_estimate: float, adjoint_slope: float, fitted_slope: float, figure_path: Path | None, json_path: Path | None)[source]#
Bases:
objectf(rho + t*d)sampled along one direction at uniform spacing.The smoothness probe: the optimizer’s trials around an earlier optimum sat ~2e-3 relative below the iterate regardless of how small the box became, and this scan separates a genuine kink (the residual of a quadratic fit is structured and shrinks with the spacing) from an evaluation noise floor (the residual is white and does not). All relative quantities are relative to
|f(rho)|at the centre sample.noise_estimateis the white-noise amplitude implied by the second differences of the samples,std(diff(values, 2)) / sqrt(6), relative to|f(rho)|: second differences of a smooth function at fine spacing are ~0, so whatever is left is the per-evaluation scatter.- Parameters:
- fit_coefficients: ndarray#
- offsets: ndarray#
- residuals: ndarray#
- values: ndarray#
- class prismo.outputs.OverlayGeometry(rib_left: float, rib_right: float, slab_top: float, rib_top: float, substrate_top: float, half_width: float, contact_offset: float, contact_width: float)[source]#
Bases:
objectThe overlay frame of
_overlay_geometry(), as plain values.RibWaveguideGeometrysatisfies the same duck-typed contract but can only describe the mesh the local author builds (y from 0 at the substrate bottom, 0.5 µm substrate). The gyptis author centres its layer stack on y = 0 with a 0.35 µm substrate, so drawing the local frame on container figures put the rib outline off the device entirely. The container path builds one of these from the gyptis mesh itself instead.substrate_topis the y of the substrate/slab interface – the value the local geometry exposes assubstrate_thickness, which only equals it in a frame whose substrate starts at y = 0.- Parameters:
- prismo.outputs.animate_doping_evolution(doping_frames: Sequence[ndarray], history: list[dict], mesh_coords: ndarray, geometry: object | None = None, output_dir: str | Path | None = None, triangles: ndarray | None = None, fps: int = 6, formats: Sequence[str] = ('gif', 'mp4'), name: str = 'doping_evolution')[source]#
Animate the net doping the solvers saw at every optimizer evaluation.
One frame per evaluated design, on a colour scale fixed across the whole run (symlog, like the live frames), titled with the iteration, its Δneff and – when evaluated – its modal loss; a trial the optimizer rejected (objective below the running best) is labelled as such, so the move-limit halvings read as what they are rather than as progress.
- Parameters:
doping_frames (
Sequence[ndarray]) – Full-node net doping(n_nodes,)[cm^-3] per evaluation, in history order – the filtered, scattered, mapped field, i.e. what ChargeTransport solved on.history (
list[dict]) – The matching per-evaluation records (same length).mesh_coords (
ndarray) –(n_nodes, 2)node coordinates in micrometres.geometry (
object|None, default:None) – Overlay frame (see_overlay_geometry()); optional.output_dir (
str|Path|None, default:None) – Directory for<name>.gif/<name>.mp4.triangles (
ndarray|None, default:None) – Silicon triangulation, as inplot_doping_field().fps (
int, default:6) – Frames per second.formats (
Sequence[str], default:('gif', 'mp4')) – Which ofgif(Pillow) andmp4(ffmpeg) to write; a format whose encoder is unavailable is skipped with a note.name (
str, default:'doping_evolution') – Output file stem.
- Returns:
Paths written (empty when there are no frames or no usable writer).
- Return type:
- prismo.outputs.feasible_offsets(rho: Array, direction: Array, offsets: ndarray)[source]#
The subset of signed
offsetstwithrho + t*directionin[-1, 1].Unlike
_feasible_steps()(which bounds a symmetric central stencil) each side is bounded on its own, so a design pinned at +1 in the direction’s sense still keeps its whole negative half-line.- Parameters:
rho (
Array)direction (
Array)offsets (
ndarray)
- prismo.outputs.generate_outputs(rho_initial: ndarray, rho_opt: ndarray, history: list[dict], mesh_coords: ndarray, geometry: object | None = None, pipeline_fn: Callable[[...], Array] | None = None, ftol_rel: float | None = None, gradient_validation_directions: int = 3, gradient_validation_steps: ndarray | None = None, gradient_validation_rho: ndarray | None = None, mode_field: ModeField | None = None, output_dir: str | Path | None = None, mesh_triangles: ndarray | None = None, cold_reevaluation: ColdReevaluation | None = None, design_history: Sequence[ndarray] | None = None, animation_history: list[dict] | None = None, animation_formats: Sequence[str] = ('gif', 'mp4'), swept_carriers: ndarray | None = None, bias_sweep_initial: Sequence[Any] | None = None, bias_sweep_optimized: Sequence[Any] | None = None)[source]#
Generate the headline output figures.
- Parameters:
rho_initial (
ndarray) – Initial design vector.rho_opt (
ndarray) – Optimized design vector.history (
list[dict]) – Optimization history fromoptimize_doping.mesh_coords (
ndarray) – Node coordinates(n_nodes, 2)in micrometres.mesh_triangles (
ndarray|None, default:None) –(n_triangles, 3)silicon triangulation of the shared mesh, so the doping figure draws the device’s own elements rather than a Delaunay fill of the whole domain.geometry (
object|None, default:None) –RibWaveguideGeometryinstance.pipeline_fn (
Callable[...,Array] |None, default:None) – Differentiable pipeline function for gradient validation. Skipped ifNone.ftol_rel (
float|None, default:None) – MMA relative tolerance for the convergence marker.gradient_validation_directions (
int, default:3) – Number of finite-difference directions.gradient_validation_steps (
ndarray|None, default:None) – Central-difference steps for gradient validation.gradient_validation_rho (
ndarray|None, default:None) – Reference design for gradient validation. Defaults torho_opt.mode_field (
ModeField|None, default:None) – Tracked optical mode|E|from the gyptis backend. Skipped ifNone(no gyptis backend bound).output_dir (
str|Path|None, default:None) – Output directory (default:outputs/).cold_reevaluation (
ColdReevaluation|None, default:None) – Warm/cold Δneff of the reported design, annotated on the convergence figure.Nonedraws nothing.design_history (
Sequence[ndarray] |None, default:None) – Full-node net doping [cm^-3] per evaluated design, in history order; animated intodoping_evolution.{gif,mp4}(seeanimate_doping_evolution()).Noneskips the animation.animation_history (
list[dict] |None, default:None) – The records matchingdesign_historyone-to-one; defaults tohistory(use it when only some records carry a design).animation_formats (
Sequence[str], default:('gif', 'mp4')) – Encoders to try for the animation.swept_carriers (
ndarray|None, default:None) –(n + p)(V_bias) - (n + p)(0 V)per node at the optimized design, forplot_depletion_field();Noneskips that figure.bias_sweep_initial (
Sequence[Any] |None, default:None) –BiasPointsequence frombias_sweep()for the seed design, forplot_bias_sweep().bias_sweep_optimized (
Sequence[Any] |None, default:None) – The same for the reported design. The figure is skipped when both are empty.
- Returns:
List of paths to the generated plot files.
- Return type:
- prismo.outputs.history_loss_series(history: list[dict])[source]#
(iterations, modal_loss_db_cm)for the entries that evaluated a loss.The optimizer records
modal_loss_db_cmasNonewhen no mode-overlap weights were bound; a history with no finite loss yields two empty lists and the convergence figure draws no loss panel.
- prismo.outputs.iso_fom_delta_neff(loss_db_cm: ndarray, fom_v_db: float)[source]#
Δneff along the curve
VπLπ·alpha = fom:|V|·λ·alpha / (2·fom).- Parameters:
loss_db_cm (
ndarray)fom_v_db (
float)
- prismo.outputs.plot_bias_sweep(initial: Sequence[Any] | None, optimized: Sequence[Any] | None, output_dir: str | Path | None = None)[source]#
The three reported figures of merit vs reverse bias, initial vs optimized.
Three panels sharing the bias axis – Δneff, the modal free-carrier loss alpha, and the VπLπ·alpha efficiency-loss figure of merit – each carrying one curve per design, so what the optimizer bought is visible across the whole operating range rather than only at the single bias it optimized. The bias axis runs left to right from 0 V into reverse bias.
Δneff is zero at 0 V by construction (it is measured against that equilibrium), so VπLπ·alpha is infinite there and that point is dropped from the third panel rather than drawn off-scale.
- Parameters:
- Returns:
Path to the saved figure, or
Nonewhen neither design swept.- Return type:
Path | None
- prismo.outputs.plot_convergence(history: list[dict], output_dir: str | Path | None = None, ftol_rel: float | None = None, cold_reevaluation: ColdReevaluation | None = None)[source]#
Convergence plot: signed delta_n_eff and reported VpiLpi vs iteration.
The loss axis of a loss-aware run is its own figure,
plot_loss_convergence().- Parameters:
history (
list[dict]) – Per-iteration records fromoptimize_doping.output_dir (
str|Path|None, default:None) – Directory to writeconvergence.pdf.ftol_rel (
float|None, default:None) – MMA relative tolerance for the marker annotation.cold_reevaluation (
ColdReevaluation|None, default:None) – Warm/cold Δneff of the reported design; drawn as a marker at the last iteration, with a warning annotation when the discrepancy exceeds its tolerance.
- Returns:
Path to the saved figure.
- Return type:
- prismo.outputs.plot_depletion_field(swept_carriers: ndarray, mesh_coords: ndarray, mode_field: ModeField | None = None, geometry: object | None = None, output_dir: str | Path | None = None, triangles: ndarray | None = None)[source]#
Where the modulation happens: swept carriers under the optical mode.
swept_carriersis(n + p)(V_bias) - (n + p)(0 V)per node [cm^-3] – negative where reverse bias depletes the junction, which is the only place the design changes the index. Drawn on the silicon elements with the tracked mode’s|E|contours on top, so the figure shows at a glance how much of the depleted region the mode actually sees (and how much doping sits in the mode for nothing but loss).- Parameters:
swept_carriers (
ndarray) –(n_nodes,)change in total carrier density.mesh_coords (
ndarray) –(n_nodes, 2)node coordinates in micrometres.mode_field (
ModeField|None, default:None) – The tracked mode for the contour overlay;Nonedraws the carriers alone.geometry (
object|None, default:None) – Overlay frame (see_overlay_geometry()); optional.output_dir (
str|Path|None, default:None) – Directory to writedepletion_field.pdf.triangles (
ndarray|None, default:None) – Silicon triangulation, as inplot_doping_field().
- Returns:
Path to the saved figure.
- Return type:
- prismo.outputs.plot_doping_field(rho_initial: ndarray, rho_opt: ndarray, mesh_coords: ndarray, geometry: object | None = None, output_dir: str | Path | None = None, triangles: ndarray | None = None)[source]#
Side-by-side doping field: initial vs optimized.
- Parameters:
rho_initial (
ndarray) – Initial design vector(n_nodes,).rho_opt (
ndarray) – Optimized design vector(n_nodes,).mesh_coords (
ndarray) – Node coordinates(n_nodes, 2)in micrometres – the unit both mesh authors emit, and the unit the axes are labelled in, so no conversion happens here.geometry (
object|None, default:None) –RibWaveguideGeometryfor overlay (optional).output_dir (
str|Path|None, default:None) – Directory to writedoping_field.pdf.triangles (
ndarray|None, default:None) –(n_triangles, 3)silicon triangulation of the shared mesh. With it the field is drawn on the device’s own elements and the oxide stays blank; without it the whole point set is drawn on a Delaunay triangulation.
- Returns:
Path to the saved figure.
- Return type:
- prismo.outputs.plot_gradient_validation(pipeline_fn: Callable[[...], Array], rho: Array, directions: list[Array] | None = None, n_directions: int = 3, step_sizes: ndarray | None = None, output_dir: str | Path | None = None, tolerance: float | None = None)[source]#
Gradient validation: relative error vs FD step size.
- Parameters:
pipeline_fn (
Callable[...,Array]) – Callable(rho) -> scalardifferentiable by JAX.rho (
Array) – Reference design vector.directions (
list[Array] |None, default:None) – Random unit perturbation directions. Generated ifNone.n_directions (
int, default:3) – Number of random directions (ignored ifdirectionsgiven).step_sizes (
ndarray|None, default:None) – Central-difference steps. Defaults to 20 logarithmic steps from1e-6to1e-1.output_dir (
str|Path|None, default:None) – Directory to writegradient_validation.pdf.tolerance (
float|None, default:None) – If given, draw the acceptance bar as a horizontal line on the figure. Usevalidate_gradient()for the pass/fail result.
- Returns:
Path to the saved figure.
- Return type:
- prismo.outputs.plot_live_doping_field(doping: ndarray, mesh_coords: ndarray, iteration: int, geometry: object | None = None, output_dir: str | Path | None = None, name: str | None = None, triangles: ndarray | None = None)[source]#
Write the optimizer’s current signed doping-field image.
One image per optimizer callback, named by
name; callers pass the iteration number so a long run leaves a snapshot per iteration rather than one file overwritten in place.mesh_coordsis in micrometres, as everywhere else in the app.trianglesis the silicon triangulation, as inplot_doping_field().
- prismo.outputs.plot_loss_convergence(history: list[dict], output_dir: str | Path | None = None)[source]#
Modal loss [dB/cm] and the VπLπ·alpha figure of merit [V·dB] vs iteration.
The companion of
plot_convergence()for the loss axis: the free-carrier loss of the unbiased device at every evaluation, and the literature’s efficiency-loss figure of merit on a twin axis. ReturnsNone(writes nothing) when the history carries no evaluated loss.
- prismo.outputs.plot_mode_field(mode: ModeField, output_dir: str | Path | None = None)[source]#
Optical mode
|E|on the rib cross-section.The eigensolver’s mode is what turns the doping design into a phase shift, so the headline set shows it directly: whichever mode the solve tracked, on the same cross-section as the doping figure.
- prismo.outputs.plot_tradeoff(history: list[dict], output_dir: str | Path | None = None)[source]#
The optimizer’s path in the (modal loss, Δneff) plane.
Every evaluated design is a point, coloured by iteration and joined in order, against iso-
VπLπ·alphahyperbolae: a design moving up-left trades loss for efficiency, and crossing below the 30 V·dB curve puts it in the literature’s band. ReturnsNonewhen no loss was evaluated.
- prismo.outputs.probe_direction(pipeline_fn: Callable[[...], Array], rho: Array, kind: str, before_evaluation: Callable[[], None] | None = None)[source]#
Unit direction for a line scan through
rho, plus the gradient used."gradient"is the normalized adjoint gradient atrho(the direction the optimizer would step);"random"is the first seeded unit vectorvalidate_gradient()also samples. Either way the direction is projected off rail-pinned variables so the scan can move both ways inside the[-1, 1]box. Returns(direction, gradient)– the gradient isNonefor a random direction – soscan_objective_line()can take its adjoint slope from the same adjoint solve instead of running a second one. RaisesValueErrorfor an unknownkindandRuntimeErrorwhen there is nothing to scan (zero gradient, or every weighted variable pinned).
- prismo.outputs.scan_objective_line(pipeline_fn: Callable[[...], Array], rho: Array, direction: Array, offsets: ndarray, output_dir: str | Path | None = None, before_evaluation: Callable[[], None] | None = None, gradient: Array | None = None)[source]#
Sample
pipeline_fnalongdirectionand fit a quadratic.- Parameters:
pipeline_fn (
Callable[...,Array]) – Callable(rho) -> scalardifferentiable by JAX.rho (
Array) – Centre design.direction (
Array) – Unit directiond.offsets (
ndarray) – Signed stepst; everyrho + t*dis evaluated as given (filter withfeasible_offsets()first to respect the box).output_dir (
str|Path|None, default:None) – When given, writeobjective_line_scan.pdfandobjective_line_scan.jsonthere.before_evaluation (
Callable[[],None] |None, default:None) – Hook run before the gradient and before every sample.gradient (
Array|None, default:None) – The adjoint gradient atrhoif the caller already has it (e.g. fromprobe_direction()); computed here whenNone.
- Returns:
- Return type:
- prismo.outputs.validate_gradient(pipeline_fn: Callable[[...], Array], rho: Array, directions: list[Array] | None = None, n_directions: int = 3, step_sizes: ndarray | None = None, tolerance: float = 0.01, output_dir: str | Path | None = None, before_evaluation: Callable[[], None] | None = None)[source]#
Check the adjoint against central FD and gate on a stated tolerance.
The proof behind the headline: the composed
rho -> Delta n_effgradient is exercised through whateverpipeline_fncloses over – pass apipelinebound to the live ChargeTransport and gyptis components to prove the cross-boundary adjoint (CT included), not a stub. The figure is always written;passedrecords whether the worst per-direction best error stayed at or belowtolerance(it does not raise, so a failing check still produces the diagnostic figure).- Parameters:
pipeline_fn (
Callable[...,Array]) – Callable(rho) -> scalardifferentiable by JAX.rho (
Array) – Reference design vector to validate the gradient at.directions (
list[Array] |None, default:None) – Perturbation directions. Random unit vectors ifNone.n_directions (
int, default:3) – Number of random directions (ignored ifdirections).step_sizes (
ndarray|None, default:None) – Central-difference steps. Defaults to 20 logarithmic steps from1e-6to1e-1.tolerance (
float, default:0.01) – Acceptance bar on the worst per-direction best relative error.output_dir (
str|Path|None, default:None) – Directory to writegradient_validation.pdf.before_evaluation (
Callable[[],None] |None, default:None) – Hook run before every pipeline evaluation (the cold-start option: reset the ChargeTransport worker so the FD reference is not polluted by warm-start path dependence).
- Returns:
A
GradientValidationResultwith the per-direction best errors, the worst of them, the pass/fail verdict, and the figure path.- Return type:
- prismo.outputs.vpi_axis_is_symlog(magnitudes: list[float])[source]#
Whether the convergence figure’s VpiLpi axis should be symlog, not linear.
symlog exists to compress a wide span. VpiLpi goes as
1/delta_n_eff, so a run that starts near zero spans many decades and a linear axis flattens every iteration after the first. A run that starts already converged spans almost nothing, and there symlog’s locator places no ticks at all, leaving a labelled axis with no numbers on it. Switch at two decades.