upxo.geoEntities.polygon2d module

2D polygon geometric entities for UPXO, built on the live UPXO boundary stack (Sline2d -> MSline2d -> ring2d) rather than plain Shapely, while remaining convertible to/from Shapely.

Fills a documented gap: mulsline2d.py’s own “See Also” references upxo.geoEntities.polygon2d, a module that did not previously exist.

Classes

Polygon2d : A single grain’s smoothed boundary, wrapping one ring2d. NestedPolygon2d : A grain with island/hole sub-polygons (recursive).

Usage

from upxo.geoEntities.polygon2d import Polygon2d, NestedPolygon2d

# Zero-copy wrap of an existing ring2d (e.g. from Technique A’s self.GB[gid]) poly = Polygon2d.from_ring2d(ring, gid=7)

# From a Shapely polygon poly = Polygon2d.from_shapely_polygon(shapely_poly, gid=7)

# Back to Shapely shapely_poly = poly.make_shapely()

class upxo.geoEntities.polygon2d.Polygon2d(ring, gid=None, props=None)[source]

Bases: object

A single grain’s smoothed 2D boundary, wrapping one ring2d.

Composition, not subclassing: ring2d is shared by non-Polygon2d callers throughout pxtal/geometrification.py and is itself documented as “in active development”, so it is kept free to evolve independently. Composition also makes from_ring2d() a genuine zero-copy wrap.

ring

The wrapped boundary – an ordered, closed loop of MSline2d segments. A segment may be Python-object-identity-shared with a neighbouring grain’s Polygon2d.ring (this is how UPXO’s geometrification pipeline represents a shared grain-boundary wall); see edit_segment().

Type:

ring2d

gid

Grain id, for bookkeeping (e.g. keying a {gid: Polygon2d} dict).

Type:

int or None

props

Arbitrary per-grain scalar/vector data. An open dict, not __slots__-restricted attributes – see polygons_to_prop_dataframe() to bridge this to UPXO’s dominant per-grain scalar convention (a pandas DataFrame indexed by gid - 1, one column per property, as used by pxtal.mcgs2_temporal_slice’s self.prop).

Type:

dict

ring
gid
props
classmethod from_ring2d(ring, gid=None, props=None)[source]

Zero-copy wrap of an existing ring2d (e.g. self.GB[gid] from polygonised_grain_structure).

classmethod from_shapely_polygon(poly, gid=None, props=None)[source]

Build a fresh ring2d from a Shapely Polygon’s exterior.

Uses the full, already-closed coordinate ring (poly.exterior.coords, which repeats the first point at the end) with MSline2d.by_coords(..., close=False) – not close=True on the de-duplicated ring. MSline2d.by_coords’s own close=True path does not append the input’s final point to nodes before closing (unlike its sibling from_lines, which does), so closing from an already-deduplicated point list silently drops the last vertex and closes one edge short. Passing the pre-closed ring with close=False sidesteps that entirely: the last segment already returns to the first point, and nodes correctly ends up with one entry per distinct vertex.

make_shapely()[source]

Return a Shapely Polygon for this grain’s current boundary.

coords(force_close=True)[source]

Return this grain’s boundary coordinates as an (n, 2) array.

property nsegments

Number of boundary segments (MSline2d walls) in this grain’s ring.

property area

Polygon area, via make_shapely() (not ring2d.area, which calls a dead-duplicate method – see module notes).

property perimeter

Polygon perimeter, via make_shapely().

property centroid

Mean of boundary vertex coordinates (ring2d.centroid’s own definition – a vertex-mean, not an area centroid).

edit_segment(seg_index, new_node_coords)[source]

Replace one boundary segment’s interior coordinates in place.

The targeted MSline2d object’s .lines/.nodes slots are rebound to freshly-built values, but the object itself keeps its identity – so if this segment is shared with a neighbouring grain’s Polygon2d.ring (the normal case for an interior wall), that neighbour sees the edit immediately, with no extra step. This mirrors exactly how MSline2d.smooth() already safely mutates a shared segment.

A new MSline2d is deliberately never swapped in for the old one: nothing in UPXO tracks which other rings reference a given segment, so replacing the object (rather than mutating it) would silently break sharing for every other referrer.

Parameters:
  • seg_index (int) – Index into self.ring.segments.

  • new_node_coords (array-like of shape (n, 2), n >= 2) – New coordinates for every node of this segment, in order. The first and last rows are junction points – shared by value (never by object identity) with whichever other segments meet there – and must match the segment’s existing first/last node coordinate exactly (within MSline2d.EPS_coord_coincide). Moving an endpoint here would silently desync every other wall meeting at that junction, so it is rejected outright.

Raises:

ValueError – If fewer than 2 coordinate rows are given, or if the first/last row does not match the segment’s existing endpoint coordinate.

subdivide_segment(seg_index, n=None, at_fractions=None)[source]

Insert new interior points into one boundary segment, in place, positioned by arc-length fraction along the segment’s current coordinate sequence.

Composes on edit_segment() rather than reimplementing its safe rebuild (fresh interior Point2d``s, endpoint identity preserved, in-place ``.lines/.nodes rebind) – so a subdivided segment shared with a neighbouring grain still propagates the new node layout to that neighbour automatically, the same as any other edit_segment() call.

Does not use MSline2d.add_nodes (exact-collinearity only, not applicable to inserting points off the segment’s original coordinates) or MSline2d.sub_divide (currently broken – raises on its very first call, see its docstring).

Parameters:
  • seg_index (int) – Index into self.ring.segments.

  • n (int or None) – Insert this many new points, evenly spaced at arc-length fractions 1/(n+1), ..., n/(n+1) of the segment’s current total length. Mutually exclusive with at_fractions.

  • at_fractions (sequence of float or None) – Explicit arc-length fractions in (0, 1) at which to insert new points. Mutually exclusive with n.

Raises:

ValueError – If neither or both of n/at_fractions are given.

clone(seg_clones=None)[source]

Return an independent copy of this grain’s boundary.

Parameters:

seg_clones (dict or None) – {id(original segment): cloned segment}, forwarded to ring2d.clone. Cloning several Polygon2d instances that may share segments (e.g. all grains of one structure) must pass the SAME seg_clones dict to every call – exactly as smooth_gbsegs does – or the sharing will not survive the clone. Defaults to a fresh, private {} when omitted, which is only correct for cloning a single, standalone Polygon2d.

class upxo.geoEntities.polygon2d.NestedPolygon2d(host, holes=None, gid=None, props=None)[source]

Bases: object

A grain with island/hole sub-polygons.

Recursive: a hole may itself be a NestedPolygon2d (a hole with its own hole), not only a flat Polygon2d – this is what lets UPXO represent genuine hole-in-hole-in-hole structures natively, superseding polygonised_grain_structure’s older, abandoned polygons_raw_holes / hlpol nested-dict pathway (never wired into make_gsmp(), and with a confirmed bug in its own level-3 branch). This class does not bridge to or read that legacy structure.

host

The outer boundary.

Type:

Polygon2d

holes

Sub-polygons cut out of host.

Type:

list of Polygon2d or NestedPolygon2d

gid
Type:

int or None

props
Type:

dict

host
holes
gid
props
classmethod from_host_and_holes(host, holes, gid=None, props=None)[source]

Construct directly from a host Polygon2d and a list of holes (Polygon2d or NestedPolygon2d, may nest arbitrarily).

classmethod from_shapely_polygon(poly, gid=None, props=None)[source]

Build from a Shapely Polygon’s exterior and interior rings.

Every produced hole is a flat Polygon2d – Shapely interior rings are always simple (LinearRing, no holes of their own), so a Shapely-sourced NestedPolygon2d can only ever be one level deep. This is a permanent property of the Shapely polygon format, not a limitation to fix later: genuine multi-level nesting can only be built natively, by nesting NestedPolygon2d instances directly (see from_host_and_holes()).

make_shapely()[source]

Return a Shapely geometry for this grain, holes included.

Takes Shapely’s native shell+holes constructor when every hole is a flat Polygon2d (the common, cheap case). Falls back to boolean composition – host.difference(union(holes)), mirroring polygonised_grain_structure._collect_grains’s own proven pattern – when any hole is itself a NestedPolygon2d (a hole-in-hole), since Shapely’s Polygon has no native representation for a hole ring that itself has holes.

coords(force_close=True)[source]

Host boundary coordinates (holes are not representable as a single coordinate array).

property area

Net area (host minus holes), via make_shapely().

property gids_all

This grain’s id followed by every hole’s id, recursively.

clone(seg_clones=None)[source]

Independent copy; seg_clones is threaded through the host and every hole (recursively) so a segment shared between the host, a hole, or a sibling grain elsewhere stays shared in the clone – see Polygon2d.clone().

upxo.geoEntities.polygon2d.polygons_to_prop_dataframe(polygons)[source]

Gather {gid: Polygon2d | NestedPolygon2d}.props into one DataFrame.

Parameters:

polygons (dict) – {gid: Polygon2d | NestedPolygon2d}.

Returns:

Indexed by gid - 1 (matching mcgs2_temporal_slice.py’s self.prop convention), one column per key observed across any polygon’s .props. A polygon missing a given key gets NaN in that column, not a KeyError.

Return type:

pandas.DataFrame

upxo.geoEntities.polygon2d.apply_prop_dataframe(polygons, df)[source]

Scatter a gid - 1-indexed DataFrame’s values back onto .props.

Parameters:

Notes

Updates each polygon’s .props dict in place (existing keys not present as columns in df are left untouched); does not replace it. Rows containing NaN for a given column leave that key unset on that polygon rather than writing a NaN value.