From 59999959e597f833c9e6031bcc6d4e984ab90684 Mon Sep 17 00:00:00 2001 From: Connor Ferster Date: Sat, 8 Jun 2024 10:13:16 -0700 Subject: [PATCH] fixes: misc bug fixes --- src/papermodels/datatypes/geometry_graph.py | 4 +- src/papermodels/fileio/utils.py | 229 +++++++++++++++++++- src/papermodels/paper/annotations.py | 2 +- src/papermodels/paper/plot.py | 28 +-- 4 files changed, 246 insertions(+), 17 deletions(-) diff --git a/src/papermodels/datatypes/geometry_graph.py b/src/papermodels/datatypes/geometry_graph.py index a5d7e86..b618598 100644 --- a/src/papermodels/datatypes/geometry_graph.py +++ b/src/papermodels/datatypes/geometry_graph.py @@ -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. """ @@ -26,7 +26,7 @@ def __init__(self): @classmethod 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) diff --git a/src/papermodels/fileio/utils.py b/src/papermodels/fileio/utils.py index 6c4e3ab..a5a11a5 100644 --- a/src/papermodels/fileio/utils.py +++ b/src/papermodels/fileio/utils.py @@ -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: @@ -34,4 +49,216 @@ def read_csv_file(filename: str) -> str: """ with open(filename, "r") as file: csv_data = list(csv.reader(file)) - return csv_data \ No newline at end of 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 diff --git a/src/papermodels/paper/annotations.py b/src/papermodels/paper/annotations.py index 8a54243..3211afc 100644 --- a/src/papermodels/paper/annotations.py +++ b/src/papermodels/paper/annotations.py @@ -222,7 +222,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 diff --git a/src/papermodels/paper/plot.py b/src/papermodels/paper/plot.py index cefde88..950ccab 100644 --- a/src/papermodels/paper/plot.py +++ b/src/papermodels/paper/plot.py @@ -9,6 +9,7 @@ import parse from papermodels.db.data_model import Annotation +from loguru import logger def plot_annotations( annots: list[Annotation] | dict[Annotation, dict], @@ -29,24 +30,28 @@ 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", "rectangle", "rectangle sketch to scale", ): - xy = xy_vertices(annot.vertices, dpi) - if sum(max_extent) == float('inf'): - min_extent = np.maximum(max_extent, np.max(xy, axis=1)) - max_extent = np.minimum(min_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)) + ax.add_patch( Polygon( xy=xy.T, @@ -79,10 +84,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) # Distance between bot-left and top-right ax.set_xlim(min_extent[0] - plot_margin_metric * 0.05, max_extent[0]+ plot_margin_metric * 0.05)