Skip to content

Commit

Permalink
Merge pull request #8 from StructuralPython/features/geometry-graph
Browse files Browse the repository at this point in the history
Features/geometry graph
  • Loading branch information
connorferster authored Nov 4, 2024
2 parents a349dc8 + 7f75958 commit 0e11f90
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 9 deletions.
4 changes: 2 additions & 2 deletions src/papermodels/datatypes/geometry_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class GeometryGraph(nx.DiGraph):
A class to represent a connected geometry system in a graph. Inherits from networkx.DiGraph
and adds a .node_hash attribute for storing a hash of all the nodes.
Can be used to generate a LoadGraph.
Can be used to generate a GeometryGraph.
The node_hash is how changes to the graph nodes can be tracked.
"""
Expand All @@ -27,7 +27,7 @@ def from_elements(
cls, elements: list[Element], floor_elevations: Optional[dict] = None
) -> GeometryGraph:
"""
Returns a LoadGraph (networkx.DiGraph) based upon the intersections and correspondents
Returns a GeometryGraph (networkx.DiGraph) based upon the intersections and correspondents
of the 'elements'.
"""
top_down_elements = sorted(elements, key=lambda x: x.page, reverse=True)
Expand Down
227 changes: 227 additions & 0 deletions src/papermodels/fileio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
from enum import Enum
from fractions import Fraction
from typing import Optional
import forallpeople as si
import parse

si.environment("structural")
import forallpeople.tuplevector as vec


class UnitSystem(Enum):
kPa = 0
MPa = 1
GPa = 2
psf = 3
ksf = 4
psi = 5
ksi = 6


def str_to_int(s: str) -> int | str:
Expand Down Expand Up @@ -35,3 +50,215 @@ def read_csv_file(filename: str) -> str:
with open(filename, "r") as file:
csv_data = list(csv.reader(file))
return csv_data


def parse_unit_system(unit_designation: str) -> UnitSystem:
"""
Returns a UnitSystem enum based on the provided 'unit_designation'.
'unit_designation': one of {'kPa', 'MPa', 'GPa', 'psf', 'ksf', 'psi', 'ksi'}
The unit designation is considered enough to describe units of both force and
distance in a single three-character string. Their meanings are as follows:
kPa = kN / m2
MPa = N / mm2
GPa = kN / mm2
psf = lbs / ft2
psi = lbs / inch2
ksf = kips / ft2
ksi = kips / inch2
"""
if unit_designation.lower() == "kpa":
return UnitSystem.kPa
elif unit_designation.lower() == "mpa":
return UnitSystem.MPa
elif unit_designation.lower() == "gpa":
return UnitSystem.GPa
elif unit_designation.lower() == "psf":
return UnitSystem.psf
elif unit_designation.lower() == "psi":
return UnitSystem.psi
elif unit_designation.lower() == "ksf":
return UnitSystem.ksf
elif unit_designation.lower() == "ksi":
return UnitSystem.ksi


def parse_unit_string(unit_string: str | float) -> float | si.Physical:
"""
Returns a Physical to represent the physical quantity described by 'unit_string'
'unit_string': a string formatted as "{number} {unit}" where unit must match a variable
name within the forallpeople.environment dictionary. If the unit is not found, a
KeyError is raised.
"""
if isinstance(unit_string, (float, int)):
return unit_string
elif isinstance(unit_string, str) and ":" in unit_string:
return unit_string
try:
magnitude_value, unit_value = unit_string.split(" ")
except ValueError:
return unit_string

# Str has a space but the left of the space is not a number
if not isinstance(str_to_float(magnitude_value), float):
return unit_string

unit_value, exponent = parse_exponent(unit_value)
magnitude = str_to_float(magnitude_value)
unit = getattr(si, unit_value)
return magnitude * unit**exponent


def parse_exponent(unit_value: str) -> tuple[str, int]:
"""
Returns a tuple that includes the exponent described on the 'unit_value'.
e.g. unit_value = "mm4" -> ("mm", 4)
unit_value = "kN" -> ("kN", 1)
"""
if unit_value[-1].isnumeric():
if "-" in unit_value:
return (unit_value[:-2], -str_to_int(unit_value[-1]))
else:
return (unit_value[:-1], str_to_int(unit_value[-1]))
else:
return (unit_value, 1)


def parse_arch_notation(dimension_string: str) -> str:
"""
Returns a string representing the nominal value of 'dimension_string' in inches.
e.g.
1'6 -> "18.0 inch"
0'4 5/8 -> "4.625 inch"
"""
if isinstance(dimension_string, (float, int)):
return dimension_string
if "'" in dimension_string:
ft_value, inch_value = dimension_string.split("'")
elif '"' in dimension_string:
ft_value = "0"
inch_value = dimension_string.replace('"', "")
else:
return dimension_string
if "/" in inch_value:
whole_inch, fractional_inch = inch_value.split(" ")
whole_inch_magnitude = str_to_float(whole_inch)
fractional_inch_magnitude = float(Fraction(fractional_inch))
inch_magnitude = whole_inch_magnitude + fractional_inch_magnitude
ft_magnitude = str_to_float(ft_value)
else:
ft_magnitude = str_to_float(ft_value)
inch_magnitude = str_to_float(inch_value)
return f"{ft_magnitude * 12 + inch_magnitude} inch"


def convert_unit_string(unit_string: str, unit_system: UnitSystem) -> float:
"""
Returns a float of the 'unit_string' after it has been normalized into the
units of 'unit_system'.
"""
unit_string = parse_arch_notation(unit_string)
quantity = parse_unit_string(unit_string)
quantity_type = get_quantity_type(quantity)
if quantity_type == "length":
if unit_system in (UnitSystem.GPa, UnitSystem.MPa):
scaled_quantity = quantity.si().prefix("m")
elif unit_system == UnitSystem.kPa:
scaled_quantity = quantity.si().prefix("unity")
elif unit_system in (UnitSystem.psf, UnitSystem.ksf):
scaled_quantity = quantity.to("ft")
else:
scaled_quantity = quantity.to("inch")
elif quantity_type == "force":
if unit_system in (UnitSystem.GPa, UnitSystem.kPa):
scaled_quantity = quantity.si().prefix("k")
elif unit_system == UnitSystem.MPa:
scaled_quantity = quantity.si().prefix("unity")
elif unit_system in (UnitSystem.psf, UnitSystem.psi):
scaled_quantity = quantity.to("lb")
else:
scaled_quantity = quantity.to("kip")
elif quantity_type == "pressure":
if unit_system == UnitSystem.GPa:
scaled_quantity = quantity.si().prefix("G")
elif unit_system == UnitSystem.MPa:
scaled_quantity = quantity.si().prefix("M")
elif unit_system == UnitSystem.kPa:
scaled_quantity = quantity.si().prefix("k")
elif unit_system == UnitSystem.psi:
scaled_quantity = quantity.to("psi")
elif unit_system == UnitSystem.psf:
scaled_quantity = quantity.to("psf")
elif unit_system == UnitSystem.ksf:
scaled_quantity = quantity.to("ksf")
elif unit_system == UnitSystem.ksi:
scaled_quantity = quantity.to("ksi")
elif quantity_type == "line":
if unit_system == UnitSystem.GPa:
scaled_quantity = quantity.si().prefix("M")
elif unit_system == UnitSystem.MPa:
scaled_quantity = quantity.si().prefix("k")
elif unit_system == UnitSystem.kPa:
scaled_quantity = quantity.si().prefix("k")
elif unit_system == UnitSystem.psi:
scaled_quantity = quantity.to("lb_in")
elif unit_system == UnitSystem.psf:
scaled_quantity = quantity.to("lb_ft")
elif unit_system == UnitSystem.ksf:
scaled_quantity = quantity.to("kip_ft")
elif unit_system == UnitSystem.ksi:
scaled_quantity = quantity.to("kip_in")
elif quantity_type == "moment":
if unit_system == UnitSystem.GPa:
scaled_quantity = quantity.si().prefix("G")
elif unit_system == UnitSystem.MPa:
scaled_quantity = quantity.si().prefix("M")
elif unit_system == UnitSystem.kPa:
scaled_quantity = quantity.si().prefix("k")
elif unit_system == UnitSystem.psi:
scaled_quantity = quantity.to("lbin")
elif unit_system == UnitSystem.psf:
scaled_quantity = quantity.to("lbft")
elif unit_system == UnitSystem.ksf:
scaled_quantity = quantity.to("kipft")
elif unit_system == UnitSystem.ksi:
scaled_quantity = quantity.to("kipin")
else:
return quantity
return float(scaled_quantity)


def get_quantity_type(p: si.Physical) -> Optional[str]:
"""
Returns one of
{'length', 'force', 'pressure', 'line', 'torque', None} in correspondence
with the .dimensions attribute of 'p'.
Dimensions of 'p' that are powers of the above quantity types (such as
area or volume) are all considered whatever the "base" type is.
e.g. 'length' applies to lengths, areas, volumes, moments of area, etc.
"""
if not isinstance(p, si.Physical):
return None
if p.dimensions in [
(0, 1, 0, 0, 0, 0, 0),
(0, 2, 0, 0, 0, 0, 0),
(0, 3, 0, 0, 0, 0, 0),
(0, 4, 0, 0, 0, 0, 0),
]:
return "length"
elif p.dimensions == (1, 1, -2, 0, 0, 0, 0):
return "force"
elif p.dimensions == (1, -1, -2, 0, 0, 0, 0):
return "pressure"
elif p.dimensions == (1, 0, -2, 0, 0, 0, 0):
return "line"
elif p.dimensions == (1, 2, -2, 0, 0, 0, 0):
return "moment"
else:
return None
2 changes: 1 addition & 1 deletion src/papermodels/paper/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def filter_annotations(annots: list[Annotation], properties: dict) -> list[Annot
"""
filtered = []
for annot in annots:
if (asdict(annot) & properties.items()) == properties.items():
if (asdict(annot).items() & properties.items()) == properties.items():
filtered.append(annot)
return filtered

Expand Down
20 changes: 14 additions & 6 deletions src/papermodels/paper/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import parse

from papermodels.db.data_model import Annotation
from loguru import logger


def plot_annotations(
Expand All @@ -30,11 +31,21 @@ def plot_annotations(
ax = fig.gca()
annotation_dict = isinstance(annots, dict)
has_tags = False
min_extent = [float("-inf"), float("-inf")]
max_extent = [float("inf"), float("inf")]
min_extent = np.array([float("-inf"), float("-inf")])
max_extent = np.array([float("inf"), float("inf")])
for idx, annot in enumerate(annots):
if annotation_dict:
has_tags = "tag" in annots[annot]

xy = xy_vertices(annot.vertices, dpi)
if sum(max_extent) == float('inf'):
min_extent = np.maximum(min_extent, np.max(xy, axis=1))
max_extent = np.minimum(max_extent, np.min(xy, axis=1))
else:
min_extent = np.minimum(min_extent, np.min(xy, axis=1))
max_extent = np.maximum(max_extent, np.max(xy, axis=1))


if annot.object_type.lower() in (
"polygon",
"square",
Expand Down Expand Up @@ -80,10 +91,7 @@ def plot_annotations(
centroid * dpi / 72,
zorder=100 * len(annots),
)
minx = min(np.min(xy[0]), minx)
miny = min(np.min(xy[1]), miny)
maxx = max(np.max(xy[0]), maxx)
maxy = max(np.max(xy[1]), maxy)

ax.set_aspect("equal")
plot_margin_metric = np.linalg.norm(
max_extent - min_extent
Expand Down

0 comments on commit 0e11f90

Please sign in to comment.