Wedge refusal¶
Refusal policies: declining a trijunction observation the mesh's own geometry makes infeasible.
A WedgeRefusal is passed as refusal= to infer_tensions and its relatives, and to
infer_tensions_young_dupre_local. Only the methods of foambryo.tension_inference.ANGLE_READING_METHODS
honour it; the others raise ValueError on any policy but NO_REFUSAL. refusal_report counts
what a policy declines.
The problem¶
At a trijunctional edge three films meet and the three interior wedge angles sum to a full turn. A reflex wedge is one wider than a straight angle: the surface is locally folded there, the three films do not enclose the edge the way a trijunction requires, and the Young-Dupré balance every angle-reading estimator in this package imposes has no solution for such a triple. Feeding the estimator that row is feeding it an observation that cannot have come from the object being measured.
Two facts make this worth acting on rather than documenting:
foambryo.geometry's oriented reading can see the fold — but it cannot undo it. The per-edge estimator's rows are built fromcosof the wedge, andcosis even about a full turn, so a reflex wedge and its reflection are literally the same row; no reading reaches it.- The
log-of-sinestimator does not merely degrade on such a row, it fails: a reflex wedge has a negative oriented sine, and the row's logarithm is not defined.
So the estimator should refuse the observation. This module is the machinery for that, and it is
off by default — NO_REFUSAL reproduces every existing number bitwise, and which policy (if
any) should become a default is a separate decision taken on its own evidence.
The three policies, and the unit each acts on¶
Every angle-reading estimator here builds its rows from one of two observation units: a whole junction line (the region triple), for the methods that average the wedge angles along the line and weight the row by the line's arclength share; or a single trijunctional edge, for the per-edge method, whose row is one edge. The policies are defined on the unit, so each is one intervention with one meaning in both families:
- :attr:
WedgeRefusalPolicy.ExcludeEdgeFromLineMean(E) — a refused edge contributes neither its angles nor its length to its line. The line keeps its row, computed from the surviving edges and weighted by their surviving arclength. A line that loses every edge has no mean and therefore no row. - :attr:
WedgeRefusalPolicy.DropLine(D) — a line carrying at least one refused edge loses its row entirely. - :attr:
WedgeRefusalPolicy.DownweightLine(W) — nothing is removed from the measurement: the line's angle means are what they always were, and only its row weight is scaled, by the fraction of its arclength that is not refused. A line with no unrefused arclength has weight zero, which is the same as having no row.
A refused wedge takes the other two wedges of its own edge with it. The three are one observation of one edge and every row consumes all three; dropping one wedge key and keeping its two partners would leave a line's three means computed over different edge subsets and no longer summing to a full turn.
Two consequences follow from the definitions and are asserted as tests rather than assumed
(tests/test_wedge_refusal.py):
- For the per-edge method the unit is one edge, so under
Ea refused edge's unit is empty and it has no row —EandDare the same intervention there and agree exactly.Wis not: it keeps the refused rows and scales every row of their line instead. - For the line-averaged methods
EandWgive a surviving line the same row weight (E's surviving arclength over the surviving total;W's full arclength times the surviving fraction, over that same total). SoEandWdiffer only in the angle a line reports, andDandEonly in whether a partly folded line reports at all.
Underdetermination is an outcome, not an error¶
Refusing rows can leave an interface with no surviving junction line at all, or split the
junction graph into pieces that no longer share a gauge. That is handled exactly as
foambryo.junction_graph already handles a closed interface and a fragmented mesh: the
partition is rebuilt over the surviving lines
(junction_graph.partition_over_surviving_junctions), an interface touched by no surviving line
is reported float("nan") and never a number, a survivor graph in several pieces is
normalised in each piece, and JunctionGraphGaugeWarning fires unchanged. A mesh all of whose lines are refused
returns every interface undetermined rather than raising: an empty answer is a result, and it is
one the caller can count.
Report the refused counts next to any accuracy number. A policy that improves an error
statistic by declining a large share of the data has not improved anything, and
:func:refusal_report is what makes that visible.
Two reasons to decline an edge: a fold, and the crop¶
A junction edge with a vertex on a crop face (foambryo.crop_faces) is not an observation of the
tissue either: the tissue beyond the crop was cut away. It is declined for that reason on every
cropped mesh, whatever the policy, and always edge by edge: it leaves its line's mean and length,
and the rest of the line still reports. The policy then acts on what remains, as if the crop
edges were not part of the line: under DropLine a line is dropped when one of its other edges
is refused, and under DownweightLine its weight is the unrefused fraction of the arclength
that is not on the crop. The two sets are held apart (WedgeRefusal.crop_edges beside the
policy's own edges) and counted apart in RefusalReport. On a mesh without crop faces the crop
set is empty and nothing here changes.
Reading nothing but the mesh¶
:func:reflex_trijunction_edges takes a mesh and a tolerance. There is no parameter through
which a reference geometry, a target angle or an expected value could reach it, and the decision
is therefore available on data that has no reference at all.
WedgeRefusalPolicy ¶
Bases: Enum
Which observations an angle-reading estimator declines, and how. See the module docstring.
NoRefusal: the default. Nothing is declined and every result is bitwise what it was before this module existed.ExcludeEdgeFromLineMean: a refused edge leaves its line's angle mean and its line's length; the line still reports, from what survives.DropLine: a line carrying any refused edge does not report at all.DownweightLine: every line reports its unchanged mean, with its row weight scaled by the fraction of its arclength that is not refused.
WedgeRefusal
dataclass
¶
WedgeRefusal(policy: WedgeRefusalPolicy = WedgeRefusalPolicy.NoRefusal, edges: frozenset[GeometricEdge] | None = None, tolerance_deg: float = REFLEX_TOLERANCE_DEG, crop_edges: frozenset[GeometricEdge] = frozenset())
A refusal policy together with the set of edges it applies to.
Attributes:
| Name | Type | Description |
|---|---|---|
policy |
WedgeRefusalPolicy
|
which of the three interventions to apply. |
edges |
frozenset[GeometricEdge] | None
|
the geometric edges to refuse, as |
tolerance_deg |
float
|
excess past a straight angle above which a wedge is reflex, used only when
|
crop_edges |
frozenset[GeometricEdge]
|
the edges declined because they touch a crop face, excluded from their
line's mean whatever |
refuses_nothing
property
¶
refuses_nothing: bool
True when this refusal cannot change any number, so the untouched path is taken.
RefusalReport
dataclass
¶
RefusalReport(policy: WedgeRefusalPolicy, n_edges: int, n_edges_refused: int, n_lines: int, n_lines_without_row: int, n_lines_downweighted: int, length_total: float, length_refused: float, n_units: int, n_units_refused: int, n_units_used: int, surviving_lines: tuple[Triple, ...] = (), n_edges_touching_crop: int = 0, length_touching_crop: float = 0.0)
How much of a mesh one policy declined, in the units the estimator actually uses.
Every field is a count or a length, so a caller can put the refused share next to the
accuracy number it produced. n_units_* are in whichever unit the estimator's rows are —
lines for the line-averaged methods, edges for the per-edge one — and the edge and line
counts are always both present so the two families can be compared.
Attributes:
| Name | Type | Description |
|---|---|---|
policy |
WedgeRefusalPolicy
|
the policy these counts describe. |
n_edges |
int
|
trijunctional edges the mesh has. |
n_edges_refused |
int
|
how many of them the refusal names. |
n_lines |
int
|
junction lines the mesh has. |
n_lines_without_row |
int
|
lines left with no row at all by this policy. |
n_lines_downweighted |
int
|
lines that keep a row whose weight or angle the policy changed. |
length_total |
float
|
total trijunction arclength, summed over geometric edges. |
length_refused |
float
|
arclength carried by the refused edges. |
n_units |
int
|
rows' observation units before refusal. |
n_units_refused |
int
|
units the policy removed. |
n_units_used |
int
|
units that still carry a row. |
surviving_lines |
tuple[Triple, ...]
|
the region triples that still carry a row, sorted. |
n_edges_touching_crop |
int
|
edges declined because they touch a crop face (0 without crop
faces). They are not counted in |
length_touching_crop |
float
|
arclength carried by those edges. |
LineAveragedAngles
dataclass
¶
LineAveragedAngles(dict_rad: dict[Triple, float], dict_length: dict[Triple, float], partition: JunctionGraphPartition, report: AngleReadingReport)
The three maps a line-averaged builder consumes, after a refusal policy has been applied.
Attributes:
| Name | Type | Description |
|---|---|---|
dict_rad |
dict[Triple, float]
|
mean wedge angle in radians, keyed by wedge key, over the edges the policy left in the line. |
dict_length |
dict[Triple, float]
|
accumulated length per wedge key, in the historical |
partition |
JunctionGraphPartition
|
the junction-graph partition over the surviving lines, so an interface left with no surviving line is reported undetermined rather than given a number. |
report |
AngleReadingReport
|
the angle reading's own coverage report, unchanged. |
PerEdgeRows
dataclass
¶
PerEdgeRows(keep: tuple[int, ...], weight_scale: tuple[float, ...], surviving_lines: frozenset[Triple], n_refused: int)
Which of a mesh's trijunctional edges still carry a row, and how their weight is scaled.
Attributes:
| Name | Type | Description |
|---|---|---|
keep |
tuple[int, ...]
|
index, into the caller's own |
weight_scale |
tuple[float, ...]
|
the multiplier on each kept row's weight, aligned with |
surviving_lines |
frozenset[Triple]
|
the region triples still carrying at least one row. |
n_refused |
int
|
how many edges lost their row, for either reason (the policy or the crop). |
triple_of_edge ¶
triple_of_edge(edge: TrijunctionEdge) -> Triple
The sorted region triple of one trijunctional edge, i.e. which junction line it lies on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
edge
|
TrijunctionEdge
|
one geometric trijunctional edge. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Triple |
Triple
|
|
reflex_trijunction_edges ¶
reflex_trijunction_edges(mesh: DcelData, tolerance_deg: float = REFLEX_TOLERANCE_DEG) -> frozenset[GeometricEdge]
Every geometric trijunctional edge of mesh whose mesh is locally folded.
An edge is reflex when its widest oriented wedge exceeds a straight angle by more than
tolerance_deg. The oriented chord reading is used whatever reading the estimator itself
consumes, so that "reflex" has exactly one definition here and the chord reading — still the
shipped default — has a well-defined refused set too.
Reads the mesh and nothing else.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
Mesh to analyze. |
required |
tolerance_deg
|
float
|
excess past a straight angle, in degrees, above which a
wedge counts as reflex. Defaults to :data: |
REFLEX_TOLERANCE_DEG
|
Returns:
| Type | Description |
|---|---|
frozenset[GeometricEdge]
|
frozenset[GeometricEdge]: |
resolve_refused_edges ¶
resolve_refused_edges(mesh: DcelData, refusal: WedgeRefusal) -> frozenset[GeometricEdge]
The edges refusal declines on this mesh: its explicit set, or the folded ones.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
Mesh to analyze. |
required |
refusal
|
WedgeRefusal
|
the policy and, optionally, an explicit edge set. |
required |
Returns:
| Type | Description |
|---|---|
frozenset[GeometricEdge]
|
frozenset[GeometricEdge]: empty under |
frozenset[GeometricEdge]
|
they are |
refusal_with_crop ¶
refusal_with_crop(mesh: DcelData, refusal: WedgeRefusal) -> WedgeRefusal
refusal, with the mesh's crop edges added as the second reason to decline an edge.
Every estimator calls this on its own refusal before reading any angle, so the crop is
honoured whatever the caller passed. Idempotent; on a mesh without crop faces it returns
refusal itself, unchanged, and the untouched code path stays untouched.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
the mesh, whose |
required |
refusal
|
WedgeRefusal
|
the caller's refusal. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
WedgeRefusal |
WedgeRefusal
|
the same policy, explicit set and tolerance, with |
WedgeRefusal
|
edge that has a vertex on a crop face ( |
refusal_report ¶
refusal_report(mesh: DcelData, refusal: WedgeRefusal, *, per_edge_rows: bool = False) -> RefusalReport
Count what refusal declines on mesh, in the unit the estimator's rows use.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
Mesh to analyze. |
required |
refusal
|
WedgeRefusal
|
the policy to account for. |
required |
per_edge_rows
|
bool
|
|
False
|
Returns:
| Name | Type | Description |
|---|---|---|
RefusalReport |
RefusalReport
|
the counts, including which lines still carry a row. |
line_averaged_angles ¶
line_averaged_angles(mesh: DcelData, *, reading: str = CHORD_READING, refusal: WedgeRefusal = NO_REFUSAL) -> LineAveragedAngles
Angles, lengths and gauge partition for a line-averaged builder, under a refusal policy.
With :data:NO_REFUSAL this calls geometry.compute_angles_tri_with_report(unique=False)
and junction_graph.partition_from_mesh and nothing else, in that order — so the untouched
path really is untouched and every existing number is bitwise unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
Mesh to analyze. |
required |
reading
|
str
|
one of
|
CHORD_READING
|
refusal
|
WedgeRefusal
|
which observations to decline. Keyword-only, defaults to refusing nothing. |
NO_REFUSAL
|
Returns:
| Name | Type | Description |
|---|---|---|
LineAveragedAngles |
LineAveragedAngles
|
the maps, the partition and the coverage report. |
per_edge_rows ¶
per_edge_rows(mesh: DcelData, trijunction_edges: Sequence[TrijunctionEdge], refusal: WedgeRefusal = NO_REFUSAL) -> PerEdgeRows
Apply a refusal policy to a per-edge estimator, whose observation unit is one edge.
ExcludeEdgeFromLineMean and DropLine coincide here and the coincidence is exact, not
approximate: the unit is a single edge, so excluding a folded edge from its own unit's mean
empties that unit, and an empty unit has no row — which is what dropping it means. They are
the same intervention. DownweightLine is genuinely different: it keeps every row, including
the folded ones, and scales the whole line's rows by its unrefused arclength fraction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
Mesh to analyze — read only to decide which edges are folded. |
required |
trijunction_edges
|
Sequence[TrijunctionEdge]
|
the estimator's own edge list, under whichever reading it uses. The edge set and lengths do not depend on the reading. |
required |
refusal
|
WedgeRefusal
|
which observations to decline. Defaults to refusing
nothing, in which case every edge is kept with scale |
NO_REFUSAL
|
Returns:
| Name | Type | Description |
|---|---|---|
PerEdgeRows |
PerEdgeRows
|
the surviving rows and their weight scales. |
all_interfaces_undetermined ¶
all_interfaces_undetermined(keys: Sequence[Interface], mean_tension: float, partition: JunctionGraphPartition) -> ComponentTensionSolution
The answer when a policy leaves no observation at all: every interface nan.
Assembles the normalisation's row alone and hands it to
junction_graph.solve_gauged_least_squares, so the undetermined interfaces are produced by
the same machinery that already produces them for a closed surface, and the result carries
the same undetermined / component_of / gauge_notes structure every caller reads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
Sequence[Interface]
|
interface key per column, in order. |
required |
mean_tension
|
float
|
the gauge value the caller requested. |
required |
partition
|
JunctionGraphPartition
|
the partition over the surviving lines — with no surviving line every interface is isolated. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
ComponentTensionSolution |
ComponentTensionSolution
|
|
require_refusal_is_honoured ¶
require_refusal_is_honoured(method_name: str, refusal: WedgeRefusal) -> None
Refuse a non-default policy on an estimator that reads no wedge angle.
The counterpart of tension_inference._require_reading_is_honoured, and for the same reason:
an estimator that assembles an area/volume or force-balance operator has no trijunction row
to decline, so silently accepting a policy would hand the caller a result they believe
refused its folded observations and which never had any.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method_name
|
str
|
the estimator's name, for the message. |
required |
refusal
|
WedgeRefusal
|
the requested refusal. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |