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:
objectA single grain’s smoothed 2D boundary, wrapping one
ring2d.Composition, not subclassing:
ring2dis shared by non-Polygon2dcallers throughoutpxtal/geometrification.pyand is itself documented as “in active development”, so it is kept free to evolve independently. Composition also makesfrom_ring2d()a genuine zero-copy wrap.- ring
The wrapped boundary – an ordered, closed loop of
MSline2dsegments. A segment may be Python-object-identity-shared with a neighbouring grain’sPolygon2d.ring(this is how UPXO’s geometrification pipeline represents a shared grain-boundary wall); seeedit_segment().- Type:
- props
Arbitrary per-grain scalar/vector data. An open dict, not
__slots__-restricted attributes – seepolygons_to_prop_dataframe()to bridge this to UPXO’s dominant per-grain scalar convention (a pandas DataFrame indexed bygid - 1, one column per property, as used bypxtal.mcgs2_temporal_slice’sself.prop).- Type:
- ring
- gid
- props
- classmethod from_ring2d(ring, gid=None, props=None)[source]
Zero-copy wrap of an existing
ring2d(e.g.self.GB[gid]frompolygonised_grain_structure).
- classmethod from_shapely_polygon(poly, gid=None, props=None)[source]
Build a fresh
ring2dfrom a ShapelyPolygon’s exterior.Uses the full, already-closed coordinate ring (
poly.exterior.coords, which repeats the first point at the end) withMSline2d.by_coords(..., close=False)– notclose=Trueon the de-duplicated ring.MSline2d.by_coords’s ownclose=Truepath does not append the input’s final point tonodesbefore closing (unlike its siblingfrom_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 withclose=Falsesidesteps that entirely: the last segment already returns to the first point, andnodescorrectly ends up with one entry per distinct vertex.
- property nsegments
Number of boundary segments (
MSline2dwalls) in this grain’s ring.
- property area
Polygon area, via
make_shapely()(notring2d.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
MSline2dobject’s.lines/.nodesslots are rebound to freshly-built values, but the object itself keeps its identity – so if this segment is shared with a neighbouring grain’sPolygon2d.ring(the normal case for an interior wall), that neighbour sees the edit immediately, with no extra step. This mirrors exactly howMSline2d.smooth()already safely mutates a shared segment.A new
MSline2dis 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 interiorPoint2d``s, endpoint identity preserved, in-place ``.lines/.nodesrebind) – so a subdivided segment shared with a neighbouring grain still propagates the new node layout to that neighbour automatically, the same as any otheredit_segment()call.Does not use
MSline2d.add_nodes(exact-collinearity only, not applicable to inserting points off the segment’s original coordinates) orMSline2d.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 toring2d.clone. Cloning severalPolygon2dinstances that may share segments (e.g. all grains of one structure) must pass the SAMEseg_clonesdict to every call – exactly assmooth_gbsegsdoes – or the sharing will not survive the clone. Defaults to a fresh, private{}when omitted, which is only correct for cloning a single, standalonePolygon2d.
- class upxo.geoEntities.polygon2d.NestedPolygon2d(host, holes=None, gid=None, props=None)[source]
Bases:
objectA grain with island/hole sub-polygons.
Recursive: a hole may itself be a
NestedPolygon2d(a hole with its own hole), not only a flatPolygon2d– this is what lets UPXO represent genuine hole-in-hole-in-hole structures natively, supersedingpolygonised_grain_structure’s older, abandonedpolygons_raw_holes/hlpolnested-dict pathway (never wired intomake_gsmp(), and with a confirmed bug in its own level-3 branch). This class does not bridge to or read that legacy structure.- holes
Sub-polygons cut out of
host.- Type:
list of Polygon2d or NestedPolygon2d
- host
- holes
- gid
- props
- classmethod from_host_and_holes(host, holes, gid=None, props=None)[source]
Construct directly from a host
Polygon2dand a list of holes (Polygon2dorNestedPolygon2d, 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-sourcedNestedPolygon2dcan 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 nestingNestedPolygon2dinstances directly (seefrom_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)), mirroringpolygonised_grain_structure._collect_grains’s own proven pattern – when any hole is itself aNestedPolygon2d(a hole-in-hole), since Shapely’sPolygonhas 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_clonesis 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 – seePolygon2d.clone().
- upxo.geoEntities.polygon2d.polygons_to_prop_dataframe(polygons)[source]
Gather
{gid: Polygon2d | NestedPolygon2d}.propsinto one DataFrame.- Parameters:
polygons (dict) –
{gid: Polygon2d | NestedPolygon2d}.- Returns:
Indexed by
gid - 1(matchingmcgs2_temporal_slice.py’sself.propconvention), one column per key observed across any polygon’s.props. A polygon missing a given key getsNaNin that column, not aKeyError.- 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:
polygons (dict) –
{gid: Polygon2d | NestedPolygon2d}.df (pandas.DataFrame) – Indexed by
gid - 1, as returned bypolygons_to_prop_dataframe().
Notes
Updates each polygon’s
.propsdict in place (existing keys not present as columns indfare left untouched); does not replace it. Rows containingNaNfor a given column leave that key unset on that polygon rather than writing aNaNvalue.