Conversation
mrke
left a comment
There was a problem hiding this comment.
This looks great - main suggestions are to remove the sphere and the axis-ratio as an effector (and shifting instead to a discrete or continuous alteration of limb exposure)
| (radiation, convection, evaporation) happens per part. | ||
|
|
||
| The existing single-body, dorsal/ventral-split model becomes a special case: | ||
| one part with `Naked`, or two `HalfCylinder` parts with a `SharedCore` join. |
There was a problem hiding this comment.
Shouldn't this also be two parts? Because the point is to capture the distinct dorso/ventral environments. Historically in the NicheMapR ectotherm model we never did this at all.
There was a problem hiding this comment.
Hm. Wouldnt we just do that generically with a view factors list for sky, ground and between components? I think thats what the plan has.
So every geometry has that and its orthoginal to other reasons to split parts?
There was a problem hiding this comment.
I was interpreting this as if it was saying that the two half cylinder approach would be unavailable for naked objects but perhaps I'm not reading it right
|
|
||
| abstract type AbstractShape end | ||
| abstract type AbstractCylindrical <: AbstractShape end | ||
| abstract type AbstractSpherical <: AbstractShape end |
There was a problem hiding this comment.
perhaps we don't need this and we make a sphere a special case of ellipsoid
There was a problem hiding this comment.
I think it allows performance optimisations
|
|
||
| abstract type AbstractInsulation end | ||
| abstract type AbstractFibrous <: AbstractInsulation end # fur, feathers, hair | ||
| abstract type AbstractLayer <: AbstractInsulation end # fat, muscle, skin |
There was a problem hiding this comment.
we could distinguish between metabolically active (flesh, fat) and metabolically inactive (e.g. bone)
| invalidated by `Piloerect`, `Vasodilate`, `Hyperthermia`, `Pant`, `Sweat` — | ||
| the vast majority of thermoregulation-loop iterations. | ||
|
|
||
| Posture-change behaviors (`Roll`, `Curl`) are out of scope for this refactor. |
There was a problem hiding this comment.
Curl was in part a hack for capturing things like ears popping up or wings being outstretched. Curling in a ball/rolling can end up being a sudden change in shape to be implemented via BiophsicalBehaviour.jl and we don't need a gradual curling.
| `::Effector` no-op and the compiler inlines them away at the call site. | ||
| Only `Piloerect`, `Uncurl`, and `Vasodilate` do real work. | ||
|
|
||
| ### 3.6 HeatExchange as pure physics + `CommonSolve` interface |
| `+ 2 · NumCompartments` (per-compartment `core_temperature` and | ||
| `flesh_conductivity`) | ||
| `+ 3 · NumParts` (per-part `insulation_depth`, `skin_wetness`, | ||
| `aspect_ratio`) |
There was a problem hiding this comment.
as i've been suggesting - maybe we abandon this and instead have an appearance / disappearance of parts
There was a problem hiding this comment.
Ok lets delete aspect_ratio
| `aspect_ratio`) | |
| ) |
| | Insulation properties, piloerection range | `part.physiology.insulation`, `.insulation_limits` | | ||
| | Skin wetness range, flesh conductivity range | `part.physiology.skin_wetness_range`, `.flesh_conductivity_limits` | | ||
| | Panting capacity | field on the `LungPart` wrapper around the lung part's physiology (§3.9); not present on other parts | | ||
| | Aspect-ratio bounds | `aspect_ratio_bounds(part.shape, limits)` dispatch (`::Sphere` returns `(1,1)`); no user input | |
| Add `OrganismTraits.lung_part::Symbol` as an O(1) index into | ||
| `body.parts` (e.g. `:torso`, or `:body` for single-part organisms). | ||
| Semantics: `lung_temperature` derives from `lung_part`'s | ||
| `core_temperature`; `Pant`'s default selector is |
| ### 3.10 Effector + selector interface | ||
|
|
||
| Selectors let a behavior target the whole body, a subset, or a single | ||
| part. `WholeBody()` degenerates to the current single-body API. |
There was a problem hiding this comment.
that's great to have this flexibilty
| be added. | ||
| - **8.3 Local convection per part** (leeward parts see less wind). | ||
| Requires: Tier 2 cache accepts an extra per-part `flow_factor` scalar | ||
| (default 1.0). No structural change. |
There was a problem hiding this comment.
interesting - and have wind direction from some weather data sets - animal can orient according to both wind and solar
| `SharedCore`) but do not provide shims. | ||
| - The four extensions in section 8 — see below. | ||
|
|
||
| ## 8. Future extensions (design must not preclude) |
There was a problem hiding this comment.
one more thing I had forgotten about - we need this setup to extend to an animal in a shelter, e.g. possum in a nestbox, where the heat exchange is being calculated for both the shelter and the animal and their interaction. We have some old code that at one point was integrated with the endotherm model when it was a big monolith but I never hooked it back into the NicheMapR setup
Phase 2 — src/parts.jl: PartSelector (WholeBody/ByName/Compartment) and type-stable traversal (map_parts/foldl_parts/set_part); a plain Body is treated as a one-part composite named :body. test/parts.jl covers round-trips and inference of the lightweight primitives. Phase 3 — effector tags (Piloerect/Uncurl/Vasodilate/Hyperthermia/Pant/ Sweat) + effect() entry point. Rule-based effectors are now reached via effect(op, WholeBody(), ...) with the old scalar names forwarding; numerics are unchanged (thermoregulate loop untouched). The genuine per-part/per-compartment apply split is deferred to Phase 4 with per-part physiology. plan — add 8.5 animal-in-shelter as a future extension. Note: Project.toml [sources] is repointed to local dev checkouts of BiophysicalGeometry/HeatExchange for cross-package work; left uncommitted to avoid absolute local paths in the repo.
Introduces the per-part physiology foundation (§3.9, §4):
- LungPart{Physiology,PantingCapacity} wraps the lung-hosting part's physiology,
carrying panting_capacity that lives ONLY on this wrapper — non-lung parts have
plain physiology and no panting field. All HeatExchange physiology accessors
forward transparently through the wrapper.
- physiology(organism) is a per-part NamedTuple keyed by part name, with the lung
part wrapped in LungPart. broadcast_physiology builds it: a single lumped
HeatExchangeTraits broadcasts to every part (a plain Body -> (; body = LungPart)),
or a per-part NamedTuple is used as given with only the lung entry wrapped.
- OrganismTraits gains lung_part::Symbol. The heat_exchange field now holds either
a single lumped physiology (single-body) or a per-part NamedTuple (multi-part);
the 3-arg constructor is unchanged and defaults lung_part to :body. Whole-organism
forwarding accessors (metabolism, respiration, options, ...) resolve through the
lung part when per-part physiology is stored, and pass a lumped physiology
straight through — so the single-body path is byte-identical and fully
type-stable (endotherm 328 / ectotherm 20 unchanged).
- pant_selector(organism) = ByName{(lung_part,)} routes Pant to the lung part;
degenerates to the whole body for a single Body.
- lung_physiology(organism) is the unwrapped source for whole-organism respiration/
metabolism and lung_temperature.
34 new tests: LungPart forwarding, broadcasting (single + distinct per-part), and
organism-level accessors for both single-body and a 2-part lung-in-torso dog.
Build the per-part solve_part_surface setups directly from a multi-part Organism (CompositeBody parts + per-part physiology NamedTuple) and route respiration, lung mass, and metabolism through lung_part, then close the whole-organism balance via HeatExchange.solve_coupled_metabolic_rate. solve_multipart_metabolic_rate / part_surface_setups generalise HeatExchange's _pack_sides from a dorsal/ventral pseudo-split to genuine anatomical parts: each part sees the full (un-doubled) sky/ground/bush/vegetation hemisphere through its own radiation_pars, insulation through its own insulation_pars, and ground contact through its own conduction_pars_external. All parts share one regulated core (the all-SharedCore compartment); floating compartments layer on top later. A single Body collapses to one part reproducing the dorsal/ventral baseline in the symmetric case (Phase 7 equivalence gate, now driven from the organism API). Tests: single-body regression vs solve_metabolic_rate + two-part solve with lung routed to the torso. Full BB suite green.
Route the endotherm effector ladder through per-part physiology for
CompositeBody organisms, driving solve_multipart_metabolic_rate:
- map_part_physiology(f, selector, organism): substitute f(physiology) for the
selected parts' stored HeatExchangeTraits (type-stable ntuple/Val).
- Per-part effectors: vasodilation and sweating span every part;
hyperthermia (core) and panting land on the lung_part only via pant_selector,
so panting affects only the lung part's respiration and energy balance
(§3.9 Phase 4 exit criterion). Metabolic heat flow is produced at the lung.
- thermoregulate(::Endotherm, ::RuleBasedSequentialControl, ::Organism{<:CompositeBody})
runs the vasodilate -> hyperthermia -> pant -> sweat ladder against the
multipart solve. Single-Body organisms keep the exact dorsal/ventral path
(dispatch on Organism{<:CompositeBody} leaves it byte-identical). Piloerection
and uncurl (geometry reshaping) deferred to the Phase 5 cache layer.
Also add the plan's whole-organism scalars to ThermoregulationLimits: the four
magic-number replacement fields (skin_temperature_undershoot / _core_overshoot,
metabolic_heat_flow_max_multiplier, minimum_normalisation_range) and
weight_for(weight, part) resolving scalar-broadcast vs NamedTuple per-part
penalty coefficients (consumed by the Phase 7.5 NLP builder).
Tests: panting-lung-only, per-part vasodilate/sweat spanning all parts,
multi-part ladder runs, weight_for + magic-number defaults. Full BB suite green
(endotherm 328 unchanged, multipart_solve 29).
- examples/dog_thermoregulation.jl: six-part dog (torso lung + head + 4 legs), per-part physiology, run through the rule-based controller end-to-end. - Tests: map_part_physiology ByName selector touches only its target part; six-part dog direct solve (one surface result per part, lung mass from torso) and full rule-based ladder. Full BB suite green.
Precompute each part's total_area, silhouette_area, and characteristic_dim once
(pure functions of shape + insulation + constant solar orientation) instead of
recomputing them every outer-loop iteration inside part_surface_setups.
- ShapeCache{Parts}: immutable per-part NamedTuple geometry cache.
- precompute_shape_cache(organism) builds it; refresh(cache, organism, effector)
is a no-op for the physiological effectors (Vasodilate/Hyperthermia/Pant/Sweat,
elided at the call site) and rebuilds for Piloerect/Uncurl (§3.5 invalidation
by direct effector dispatch).
- solve_multipart_metabolic_rate / part_surface_setups take an optional cache;
_part_geometry reads cached values or computes them on the fly (identical
results). characteristic_dim now flows through the setup to solve_part_surface.
- The multi-part rule-based loop precomputes the cache once and refreshes after
each effector; the physiological ladder keeps it valid throughout.
Tests: cached solve is bitwise-identical to uncached; refresh no-ops for
physiological effectors (=== cache) and rebuilds for geometry effectors. Full BB
suite green (endotherm 328 unchanged, multipart_solve 52).
Generalise the IPOPT thermoregulation solver from the fixed dorsal/ventral WeightedMeanNLP / MultiSidedNLP layouts to a genuine multi-part organism. New MultipartNLP strategy (a HeatExchange.NLPStrategy) with a MultipartNLPPacked problem carrying the per-part solve_part_surface setups plus the whole-organism respiration inputs. Decision variables are a regular per-part stride — core / log-metabolic / pant whole-organism, then skin / insulation / flesh_conductivity / skin_wetness per part (3 + 4N variables). Constraints are per-part surface balance + skin temperature, one whole-organism respiration balance, and the Q10 inequality (2N + 2). Physics is HeatExchange.part_surface_residuals per part, so nothing is duplicated; geometry effectors (insulation_depth / axis_ratio) are deferred exactly as the multi-part rule-based ladder defers Piloerect / Uncurl, so the per-part setups stay fixed across the solve and no body is rebuilt under AD. The multipart methods slot into the existing direct-Ipopt + Enzyme callback machinery (IPOPTSolverCache, _ipopt_solve!, the five Evaluate* functors, _lagrangian) unchanged — they dispatch physics on the packed type and operate on a flat Float64 vector — by adding MultipartNLPPacked methods for _heat_balance_ residuals!, _objective_value, _inputs!, _problem_size, _scaling, _assemble, and a nlp_pack method. Also repairs the shared Ipopt/Enzyme callbacks so they run under the current Enzyme: set_runtime_activity on the reverse/forward passes, mark the captured LagrangianGradient functor Const in the Hessian pass, and replace the BLAS dot in _lagrangian (which segfaulted under nested AD via Enzyme's fallback BLAS) with a plain loop. The single-body path had no tests and did not run in this environment; the 328-case endotherm regression is behaviourally untouched. Tests (test/multipart_nlp.jl, 16): residuals vanish at the iterative coupled solution; 1-part and two-part organisms solve via IPOPT to heat-balanced, Q10-satisfying, physically sane states.
Validates the reusable cache path (cold-in-cache then primal+dual warm-started) converges to the same multi-part solution as a fresh per-call solve.
The plan is a forward-looking roadmap; progress belongs in git history, not the plan document.
Replace the 3+4N index-stride decoding of Ipopt's decision-variable vector with a nested NamedTuple template flattened/reconstructed by Flatten. The variable structure is defined once (_variable_structure); the variable template, bounds, scaling, and initial-value templates are all that same shape flattened in the same order, so a bound can never drift out of alignment with its variable, and adding a part or a per-part effector is a change to the structure, not to a pile of x[3+4i] offsets. Residual/objective/assemble read named fields off the reconstructed template. The flat optimiser vector and the reconstructed template are Float64: Ipopt's space is unitless, so keeping the reconstructed leaves homogeneous Float64 makes Flatten.reconstruct type-stable (a heterogeneous data[n] infers to a Union and breaks Enzyme's AD). Units are attached where each named field enters the physics (* u"K", exp(_) * u"W", …). Enzyme differentiates straight through reconstruct. Per-part accumulation in the residual uses a plain for-loop with loop-carried SSA scalars, not a map/ntuple closure that reassigns a captured accumulator (which boxes it — non-isbits — and crashes the AD compiler). Constraint counts are derived by flattening the residual template, never hand-counted. Full BB suite green (multipart_nlp 19, endotherm 328 unchanged).
The multi-part NLP template now holds Unitful leaves. Targeting Real, Flatten.flatten rips out only each Quantity's inner Float64 magnitude (the optimiser vector stays homogeneous Float64, so reconstruct is type-stable and Enzyme-safe), and reconstruct puts a Float64 back into the typed slot — units round-trip through the Quantity type. The residual/objective/assemble read Unitful named fields straight off reconstruct; no field is hand-attached with * u"K" and nothing is hand-stripped. Removes the last targetless ustrip (the test's pack helper now flattens to Real magnitudes). The only remaining ustrips are targeted (ustrip(u"K"/u"W", _)) at genuine boundaries: the objective's dimensionless penalty space, the log-transform variable, and residual outputs. All 19 multipart_nlp tests pass (incl. the two-part case through Enzyme); full BB suite green, endotherm 328 unchanged.
Stop stripping quantities to a target unit (which bakes a unit choice into the code and blocks later generalisation). The objective and _inputs! now carry Unitful quantities throughout — bounds/initials are built Unitful and flatten(Real) rips the magnitudes at the one Flatten boundary; the objective's penalty terms are ratios of like-dimensioned quantities (dimensionless without ustrip); the Q10 slack subtracts Unitful quantities and strips only the temperature difference the empirical correlation needs. ustrip now appears only where a bare number is genuinely required: the external solver's Float64 vectors (residual writes, bounds), the Q10 formula, and the log-transform variable. Fix two parameter-structure smells surfaced by this: ThermoregulationLimits target_core_skin_gradient is now a Quantity (2.0u"K") via the existing Tu type param, not a bare Float64 that consumers slapped u"K" onto; and the objective/nlp heat-flow parameters are Unitful W, dropping the heat_flow_min_W / setpoint_temperature_K unit-suffixed names (the unit lives in the type). Full BB suite green, endotherm 328 unchanged.
…, type-stable lung routing
Objective / NLP:
- Replace the hardcoded five-term objective with a tuple of RegulationTarget
recipes folded by the objective; rename the *_penalty weight fields to *_weight
and drop the dead weight_for helper.
- Delete the single-body IPOPT path (WeightedMeanNLP/MultiSidedNLP); MultipartNLP is
the sole strategy and the IPOPTControl default.
- Remove every hardcoded unit literal between flatten/reconstruct: the variable
template derives its units from the model, residuals are dimensionless-ised by
their own oneunit, Q10 and the log-metabolic transform use model-derived units.
- Wire respire into the whole-organism balance so the NLP and the coupled solver
constrain the same thing.
Physiology:
- Store the lung part as Val{name} in OrganismTraits so the name lives in the type;
physiology(o) is now type-stable (guarded by an @inferred test) instead of
inferring Union{LungPart,_} per part.
- map_part_physiology works on a single-Body organism; merge the duplicate
_organism_physiology methods; drop dead _set_lung_metabolic_heat_flow.
flesh_conductivity had a bounds envelope but no objective term, so it was unobservable whenever core ~ skin (the core->skin conduction flux is then insensitive to it) and the solver parked it at an arbitrary bound. Add a RegulationTarget pulling it toward its vasoconstricted reference, matching the other effectors, plus a flesh_conductivity_weight limit (default 0.0, so no existing behaviour changes). The budgerigar example sets it to 0.05, which removes the vasodilation flip-flop at the hot end.
The unified Newton surface solve conserves surface energy where NicheMapR's linearised thermal-node update does not, so the furred cylinder (mildly) and furred plate/near-cube (substantially) no longer match the NicheMapR reference. Mark exactly the diverging assertions broken via a @check helper rather than loosening tolerances, keeping every other comparison tight and flagging us if the divergence ever disappears.
OrganismTraits gains a `couplings` field — a tuple parallel to `body.joins` (values <: HeatCoupling), defaulting to `()` which resolves to all-SharedCore (every part shares one regulated core, preserving today's single-body / dorsal-ventral behaviour). `organism_compartment_graph(o)` builds the HeatExchange.CompartmentGraph from the body topology and couplings: SharedCore joins contract into one compartment, ConductiveCoupling joins split parts into separate floating compartments. Behaviour-preserving — the solve path is unchanged this increment. Re-exports HeatCoupling/SharedCore/ConductiveCoupling.
…partments solve_multipart_metabolic_rate now dispatches on the couplings type: the default empty tuple (every organism that doesn't opt in) keeps the exact single-compartment path and return type — no graph, no regression. Organisms with explicit couplings build the compartment graph; all-SharedCore still collapses to the single-compartment path, while ConductiveCoupling joins trigger the multi-compartment solve. The multi-compartment path pins the regulated (lung) compartment at setpoint and runs a fixed point over the floating cores: per-part surface solves at each compartment's current core -> per-compartment aggregates -> solve_regulated_core_ temperatures -> repeat to convergence. The regulated compartment's metabolic heat is then closed by solve_coupled_metabolic_rate, charged the conductive loss to its floating neighbours via extra_net_metabolic. Inter-compartment conductances derive from join_area + internal_distance + per-part flesh conductivity. Tests: floating-compartment behaviour with environment-independent limits (tight interface -> head core to setpoint; loose -> floats furthest away).
Adds the plan's Phase 7 exit-gate test: two equal-mass HalfCylinders joined flat-to-flat with a FullCover join and the default SharedCore coupling reproduce the full-Cylinder dorsal/ventral baseline within ~0.5% on metabolic heat and ~0.04 K / ~0.15 K on skin / insulation temperatures. The two halves reconstruct the full cylinder's exposed surface exactly; the only residual is the half-cylinder characteristic dimension (2^(1/3) smaller) feeding convection. Together with the existing single-Body identity and the ConductiveCoupling floating-compartment test (Phase 7b), this completes the Phase 7 equivalence gates.
thermoregulate(::Endotherm, ::RuleBasedSequentialControl, ...) gains an inner_solve keyword (default _multisided_inner — the legacy dorsal/ventral path, behaviour unchanged). _multipart_inner routes the same ladder through the genuine per-part solve_multipart_metabolic_rate. This lets both paths be driven for validation before the dorsal/ventral machinery is deleted. Validation (full converged ladder, 8 endoR cases, old vs new): the genuine per-part path reproduces the dorsal/ventral converged results to <0.7% on metabolic heat and <0.6 K on skin across all shapes/fur. The earlier large input-state divergences were artifacts of comparing the unregulated state; at convergence the paths agree to sub-percent. Confirms the Phase 8 deletion is safe and preserves the NicheMapR validation.
_assemble_endotherm_output builds the full ThermoregulationOutput (thermoregulation/ morphology/energy_flows/mass_flows) from a single-part multipart solve, so the genuine per-part path (_multipart_inner) can return the rich output downstream consumers read. Morphology is body-only (same expressions as the legacy assembler → matches to tolerance); energy/mass flows come from the one part's flows + respiration. Gross longwave is recomputed as ε·σ·A·T⁴ (the part sees the full hemisphere, view factors sum to one) with incoming = outgoing − net. Default inner_solve stays _multisided_inner (legacy) so the suite is unchanged; the genuine path is exercised via inner_solve=_multipart_inner. Switching the default surfaces the expected off-assumption endoR divergences (gross-flux decomposition, per-side dorsal/ventral assertions), which is a validation-restructuring decision, not a code bug — the physical net agrees (<0.7% metabolic).
The single-part endotherm output assembler reconstructed gross longwave out as ε·σ·total_area·T⁴, but the internal surface physics radiates only from area_convection = total_area·(1 − conduction_fraction) (heat_balance line 489). The ground-contact patch's exchange with the substrate is bundled into the conduction term, so including it in the environmental longwave over-stated gross in/out by 1/(1 − conduction_fraction) (43% at conduction_fraction = 0.3). The net longwave (and metabolic balance) was never affected, since longwave_flow_in = out − flows.longwave.
Locks in the one operating point where the genuine per-part model and the legacy dorsal/ventral model must coincide: an animal standing upright at midday in symmetric surroundings — sun overhead, uniform radiant environment, symmetric insulation/view factors, and no ground contact (conduction_fraction = 0). There the two ladders agree on the regulated quantities (metabolic heat, core, skin, insulation) to ~1e-4 and both conserve energy. Ground contact is deliberately excluded: it is where the old model's conflation bites (ventral solar carries a (1 − 2·conduction_fraction) factor; ground conduction is weighted by the ground view factor), so the models diverge there by design — the reason dorsal/ventral is retired, not a regression.
Adds the init! step for multi-part surface radiation and threads its output through the solve as an opt-in per-part view: - precompute_view_partition(organism, environment_vars): for a CompositeBody, bakes each part's occlusion-aware (sky, ground, neighbours) fractions from view_partition plus its direct-beam lit silhouette from silhouette_area_per_part (sun direction derived from the zenith angle, azimuth 0). A single Body has no parts to occlude, so it returns nothing and the solve keeps its naive path. - part_surface_setups / _part_surface_setup / solve_multipart_metabolic_rate take an optional �[?1049h�[>4;2m�[?1h�=�[?2004h�[?1004h�[1;24r�[?12h�[?12l�[22;2t�[22;1t�[27m�[23m�[29m�[m�[H�[2J�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�]11;?��]10;?��[?25l�[2;1H�[94m~ �[3;1H~ �[4;1H~ �[5;1H~ �[6;1H~ �[7;1H~ �[8;1H~ �[9;1H~ �[10;1H~ �[11;1H~ �[12;1H~ �[13;1H~ �[14;1H~ �[15;1H~ �[16;1H~ �[17;1H~ �[18;1H~ �[19;1H~ �[20;1H~ �[21;1H~ �[22;1H~ �[23;1H~ �[m�[7;32HVIM - Vi IMproved�[9;33Hversion 9.1.1752�[10;29Hby Bram Moolenaar et al.�[11;19HVim is open source and freely distributable�[13;26HHelp poor children in Uganda!�[14;18Htype :help iccf�[34m<Enter>�[m for information �[16;18Htype :q�[34m<Enter>�[m to exit �[17;18Htype :help�[34m<Enter>�[m or �[34m<F1>�[m for on-line help�[18;18Htype :help version9�[34m<Enter>�[m for version info�[1;1H�[?25h�[?4m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[?2004l�[>4;m�[?2004h�[>4;2m�[24;1H�[?2004l�[>4;m�[?1004l�[?2004l�[?1l�>�[?1049l�[>4;mVim: Error reading input, exiting... Vim: Finished. �[24;1H�[23;2t�[23;1t�[J. When supplied, a part's sky/ground view factors and solar silhouette come from the precomputed partition instead of its own naive full- hemisphere values, so a shadowed part gets no phantom direct beam. Default (view = nothing) is the existing behaviour, so no current path is disturbed. The modeller poses the body biologically (dorsal up); init! reads that pose. Validated: dorsal-up two halves under an overhead sun — the ventral half's lit silhouette is zero and its solar drops to the diffuse component only. Remaining: the inter-part surface exchange term (neighbour fractions), the convection shared-boundary-layer fix, and the two-half-under-load gate.
Build the per-part neighbour topology (_neighbour_topology: sibling index + view fraction, from the precomputed view partition) and pass it to solve_coupled_metabolic_rate so the occluded solid angle of each part exchanges longwave with its siblings' surfaces instead of the sky/ground behind them. The occlusion testset now also asserts the coupling: the view partitions sum to one (energy-conserving), and against an identical solve with the neighbour term stripped, coupling shrinks the warm-dorsal/cool-ventral surface-temperature gap (warm half radiates more, cool half less).
Splitting a body into parts must not shrink the convective length scale: the two halves of a cylinder sit in one boundary layer, so each should convect at the reconstructed whole's V^(1/3) (+ fur), not its own (V/2)^(1/3) — a factor ~2^(1/3) too small. part_surface_setups now computes the composite's characteristic dimension (_parent_characteristic_dim: cbrt of the summed part volumes plus the shared fur thickness) and hands it to every part's setup; a single Body keeps its own (nothing to share). This removes the last standing residual between the two-half model and the old dorsal/ventral one: at thermoneutral, two HalfCylinders + SharedCore now reproduce the full-cylinder dorsal/ventral solve to ~5e-5 in metabolic rate and ~1e-6 in surface temperatures (was ~0.5%, entirely the half char dim). Gate tightened accordingly; a direct test asserts each half's setup uses the whole-body scale.
The floating-core fixed point (_solve_multicompartment) now rides the inter-part surface exchange: _bake_neighbours injects each part's neighbours — view fraction plus the sibling's current outer surface temperature — into its packed environment_vars each outer iteration, so the compartment cores and the neighbour surface temperatures co-converge. The converged temperatures are baked once more for the regulated solve_coupled_metabolic_rate close (a fixed exchange there, no extra iteration). A nothing topology (single Body, or no precomputed view) passes the setups through untouched, so the pre-coupling multicompartment path stays bit-identical. Test asserts the baking deterministically (no-op without topology; the right fraction + sibling temperature with one), since the term itself is already validated at the HeatExchange level and end-to-end in the single-compartment path.
Half shapes have no mass field; read mass(body.shape) so half-body parts report their own (halved) mass.
Replace view_partition → silhouette_factors and silhouette_area_per_part → silhouette(body, Point(sun)) at the multipart radiation precompute.
Migrate cache / heatexchange / multipart / ectotherm call sites; rename a shadowing local (silhouette → area_silhouette) exposed by the function rename.
`precompute_view_partition` now calls `silhouette_factors(body, Sky(0.5))` (explicit flat-ground hemisphere, byte-identical to the old default) and `silhouette(body, Beam(sun))`, matching BiophysicalGeometry's beam/diffuse region API. A sloped or occluded site would pass its own `Sky`/`Ground`/`Horizon` here.
@mrke this is a plan to implement multi-part geometries