Connector Ports
A PointPort turns the value of a PDE field at a point into an MTK connector, so a discretized PDE composes with lumped components through connect. A heated rod can exchange heat with a HeatCapacitor, or a probe can feed a field value to a controller.
Ports are declared on the PDESystem, before discretization, at coordinates rather than grid indices. The discretization chooses the grid nodes.
Declaring a port
PointPort(connector; name, var, at, across, through = nothing, select = Interpolate())connectorconstructs the connector, called asconnector(; name, kwargs...). Any@connectorworks, for exampleModelingToolkitStandardLibrary.Thermal.HeatPort.varis the dependent variable the port couples to, such asT(t, x).atgives one coordinate per spatial argument ofvar, such as[x => 0.3].acrossnames the potential variable of the connector, such as:T. After discretization it equals the selected field value.through = :Q_flow => scalenames aconnect = Flowvariable of the connector. The flow is added toDt(var)as the sourcescale * Q_flow. Omitthroughfor a read-out port, which only observes the field.
Put the ports in the systems of the PDESystem:
using ModelingToolkit, MethodOfLines, DomainSets, OrdinaryDiffEq
using ModelingToolkit: t_nounits as t, D_nounits as Dt
using ModelingToolkitStandardLibrary.Thermal
@parameters x k ρ c A
@variables T(..)
Dx = Differential(x)
Dxx = Differential(x)^2
@named heater = PointPort(HeatPort; var = T(t, x), at = [x => 0.31], across = :T,
through = :Q_flow => 1 / (ρ * c * A))
@named probe = PointPort(HeatPort; var = T(t, x), at = [x => 0.7], across = :T,
select = Nearest())
@named rod = PDESystem([Dt(T(t, x)) ~ k / (ρ * c) * Dxx(T(t, x))],
[T(0, x) ~ 300.0, Dx(T(t, 0)) ~ 0, Dx(T(t, 1)) ~ 0],
[t ∈ Interval(0.0, 10.0), x ∈ Interval(0.0, 1.0)], [t, x], [T(t, x)],
[k => 50.0, ρ => 8000.0, c => 500.0, A => 1e-4]; systems = [heater, probe])Composing and solving
discretize refuses a system with ports, since compiling the rod alone would leave the ports unconnected. Discretize symbolically, connect, then compile the composed model:
sys, tspan = symbolic_discretize(rod, MOLFiniteDifference([x => 0.01], t))
@named cap = HeatCapacitor(C = 10.0, T = 400.0)
@named contact = ThermalConductor(G = 1.0)
@named sensor = TemperatureSensor()
@named model = System(
[connect(sys.heater, contact.port_a), connect(contact.port_b, cap.port),
connect(sys.probe, sensor.port)], t;
systems = [sys, contact, cap, sensor])
prob = ODEProblem(mtkcompile(model), nothing, tspan)
sol = solve(prob, FBDF())
sol[sys.heater.Q_flow] # heat flow into the rod
sol[sys.T] # the discretized field, one vector per time stepThe initial condition of the PDE travels with the discretized system, so ODEProblem needs no u0 for it.
Keep the discretized system nested like this. Keeping it at top level and adding the components with extend does not work: its ports are then outer connectors, and MTK sets the flow of a top-level outer connector to zero.
Read-out ports also work on the discretized system compiled on its own. Its ports are then outer connectors, so their flows are zero, and their variables are read from the underlying ODE solution with the port objects you declared:
sol = solve(ODEProblem(mtkcompile(sys), nothing, tspan), FBDF())
sol.original_sol[probe.T]Point selection
The select keyword decides which grid nodes I and weights w_I represent the point. The weights sum to one and form a tensor product across spatial directions.
Interpolate(p)uses Lagrange interpolation of degreepoverp + 1nodes per direction. The nodes grow outward from the two that bracket the point, so it never extrapolates, and it reproduces polynomials of degreep. The defaultInterpolate()is linear,p = 1, whose weights are non-negative. Withp >= 2some weights are negative, so a heater also cools a few nodes slightly. A point that lies on a node uses that node alone.Nearest()uses the single nearest node per direction. At an exact tie it prefers a node inside the domain over an edge-aligned ghost node, then the lower index.
The across variable is Σ w_I u_I. The flow is spread over the same nodes, with source density scale * Q_flow * w_I / V_I at node I, where V_I is the node's control volume (half a cell at a boundary end, a full cell across a periodic seam). One weight table serves both directions, so the coupling conserves the exchanged quantity, Σ V_I q_I = scale * Q_flow, and is passive: the power the PDE receives is Q_flow times the value the component sees.
Sign and scale
Q_flow is positive into the PDE. This is the MTK convention for a component's own port, and it matches ModelingToolkitStandardLibrary components such as HeatCapacitor.
The source is added to Dt(var) as if the PDE were solved for it, whichever way the PDE is written: ρ * c * Dt(T) ~ k * Dxx(T), Dt(T) ~ k / (ρ * c) * Dxx(T) and k / (ρ * c) * Dxx(T) ~ Dt(T) all take the same scale. The PDE must contain Dt(var) linearly with a constant or parameter coefficient; a variable without a time derivative cannot take a flow.
scale converts the flow into a rate of change of var. A rod with cross-section A, density ρ and heat capacity c, whose state is temperature, takes 1 / (ρ * c * A). A PDE for a conserved density per unit length takes 1. scale may contain parameters of the PDESystem.
Limitations
- A port that carries a flow selects interior nodes only, because boundary nodes hold boundary conditions. A point closer to a boundary than the first interior node is an error. Write flow through a boundary as a flux boundary condition instead.
- Ports on boundary faces, ports spanning a whole face, and ports embedded in boundary conditions are not supported.
- Stationary (time-independent) systems,
StaggeredGridandPseudospectralDiscretizationdo not support ports. - The composed model must go through
mtkcompileandODEProblem. ADAEProblemof the composed model fails initialization. - The
PDESolutioninterface (sol[T(t, x)]) does not cover composed models. Index the namespaced unknowns instead, such assol[sys.T].