diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index d76e4e6..c6d7cd6 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -29,7 +29,7 @@ jobs: - name: Test with pytest run: | - python -m pytest --cov=efficalc --cov-report=xml tests + python -m pytest tests --cov=efficalc --cov-report=xml tests - name: Coveralls uses: coverallsapp/github-action@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dc48cd3..eb34164 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,4 +37,4 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | - python -m pytest + python -m pytest tests diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..bc8638b --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,103 @@ + + + + + + + + + + { + "lastFilter": { + "state": "OPEN", + "assignee": "janderson4" + } +} + { + "selectedUrlAndAccountId": { + "url": "https://github.com/janderson4/efficalc.git", + "accountId": "a7be60f2-e51f-4141-aa6b-ced89a97ed62" + } +} + { + "customColor": "", + "associatedIndex": 7 +} + + + + { + "keyToString": { + "Python tests.pytest in test_calculation.py.executor": "Run", + "Python.visual_test_document_wrapper.executor": "Run", + "Python.visual_test_pmm_plotter_plotly.executor": "Run", + "Python.visual_test_point_plotter.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "git-widget-placeholder": "main", + "last_opened_file_path": "C:/Users/ja299/PycharmProjects/efficalc" + } +} + + + + + + + + + + + + + + + + + + + + + 1730055042660 + + + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile index b21ff46..dfda002 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ publish: # within venv tests: - python -m pytest + python -m pytest tests # within venv docs: diff --git a/README.md b/README.md index 4a5efcf..69360d1 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@

Tests    - Coverage Status    + Coverage Status    License: MIT    - PyPI version + PyPI version

diff --git a/docs/.buildinfo b/docs/.buildinfo index 214219c..4b040f0 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 0b61954b10429afc60dfc155a8e18b44 +config: 585cac338760b6bd68fab13cc0af68e6 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.doctrees/base_classes.doctree b/docs/.doctrees/base_classes.doctree index 3dc2d1e..8881a93 100644 Binary files a/docs/.doctrees/base_classes.doctree and b/docs/.doctrees/base_classes.doctree differ diff --git a/docs/.doctrees/calculation_helpers.doctree b/docs/.doctrees/calculation_helpers.doctree index 25c030c..2d50895 100644 Binary files a/docs/.doctrees/calculation_helpers.doctree and b/docs/.doctrees/calculation_helpers.doctree differ diff --git a/docs/.doctrees/canvas.doctree b/docs/.doctrees/canvas.doctree index 708689d..651194f 100644 Binary files a/docs/.doctrees/canvas.doctree and b/docs/.doctrees/canvas.doctree differ diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle index 215b8e9..b4b2002 100644 Binary files a/docs/.doctrees/environment.pickle and b/docs/.doctrees/environment.pickle differ diff --git a/docs/_sources/base_classes.rst.txt b/docs/_sources/base_classes.rst.txt index 406713a..fe33d4b 100644 --- a/docs/_sources/base_classes.rst.txt +++ b/docs/_sources/base_classes.rst.txt @@ -31,6 +31,14 @@ Base Classes :members: +.. autoclass:: efficalc.Table + :members: + + +.. autoclass:: efficalc.InputTable + :members: + + .. autoclass:: efficalc.TextBlock :members: diff --git a/docs/_sources/calculation_helpers.rst.txt b/docs/_sources/calculation_helpers.rst.txt index 9b95518..7d664c6 100644 --- a/docs/_sources/calculation_helpers.rst.txt +++ b/docs/_sources/calculation_helpers.rst.txt @@ -12,6 +12,10 @@ Calculation Helpers :members: +.. autoclass:: efficalc.report_builder.LongCalcDisplayType + :members: + + .. autofunction:: efficalc.save_calculation_item diff --git a/docs/_sources/canvas.rst.txt b/docs/_sources/canvas.rst.txt index 3db5562..0b339d4 100644 --- a/docs/_sources/canvas.rst.txt +++ b/docs/_sources/canvas.rst.txt @@ -175,6 +175,15 @@ Canvas Elements .. autoclass:: efficalc.canvas.Rectangle :members: +.. autoclass:: efficalc.canvas.Text + :members: + +.. autoclass:: efficalc.canvas.Dimension + :members: + +.. autoclass:: efficalc.canvas.Leader + :members: + Line/Polyline Markers ********************* @@ -193,3 +202,6 @@ Base Classes .. autoclass:: efficalc.canvas.Marker :members: +.. autoclass:: efficalc.canvas.ElementWithMarkers + :members: + diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 3f42394..dc5c51d 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '1.2.0', + VERSION: '1.2.6', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/base_classes.html b/docs/base_classes.html index edd3b82..ea27e8b 100644 --- a/docs/base_classes.html +++ b/docs/base_classes.html @@ -6,7 +6,7 @@ - Base Classes - efficalc 1.2.0 documentation + Base Classes - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
-
efficalc 1.2.0 documentation
+
efficalc 1.2.6 documentation
@@ -152,7 +152,7 @@
- efficalc 1.2.0 documentation + efficalc 1.2.6 documentation
- + diff --git a/docs/calculation_helpers.html b/docs/calculation_helpers.html index 10668f0..02039fa 100644 --- a/docs/calculation_helpers.html +++ b/docs/calculation_helpers.html @@ -3,10 +3,10 @@ - + - Calculation Helpers - efficalc 1.2.0 documentation + Calculation Helpers - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
-
efficalc 1.2.0 documentation
+
efficalc 1.2.6 documentation
@@ -152,7 +152,7 @@
- efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@ @@ -349,6 +353,20 @@ +
+
+class efficalc.report_builder.LongCalcDisplayType(value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None)#
+

An enumeration for controlling how to display long mathematical expressions in reports.

+
+
Variables:
+
    +
  • SCALE – Scale the expression display and font size down to fit within the report width

  • +
  • LINEBREAK – Break the expression into multiple lines to fit within the report width

  • +
+
+
+
+
efficalc.save_calculation_item(item)#
@@ -402,14 +420,14 @@
- +
Previous
-
Figures in Calc Reports
+
Drawing on a Canvas
@@ -455,6 +473,7 @@
  • ReportBuilder.view_report()
  • +
  • LongCalcDisplayType
  • save_calculation_item()
  • clear_saved_objects()
  • get_override_or_default_value()
  • @@ -472,7 +491,7 @@ - + diff --git a/docs/canvas.html b/docs/canvas.html index f1a9740..49a92cb 100644 --- a/docs/canvas.html +++ b/docs/canvas.html @@ -6,7 +6,7 @@ - Drawing on a Canvas - efficalc 1.2.0 documentation + Drawing on a Canvas - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -351,7 +351,7 @@

    API docs

    Canvas#

    -class efficalc.canvas.Canvas(width: float, height: float, caption: str = None, centered: bool = True, full_width: bool = False, background_color: str = None, border_width: float = None, border_color: str = None, scale: float = 1.0, default_element_fill: str = 'none', default_element_stroke: str = 'black', default_element_stroke_width: float = 1)#
    +class efficalc.canvas.Canvas(width: float, height: float, min_xy: tuple[float, float] = (0, 0), caption: str = None, centered: bool = True, full_width: bool = False, display_type: Literal['report-only', 'report-input', 'report-result'] = 'report-only', background_color: str = None, border_width: float = None, border_color: str = None, scale: float = 1.0, default_element_fill: str = 'none', default_element_stroke: str = 'black', default_element_stroke_width: float = 1)#

    Represents a canvas to hold and manage multiple SVG elements. This is the backdrop of the drawn figure. Coordinate system starts in the top left corner of the canvas with x-axis pointing right and y-axis pointing down.

    @@ -359,12 +359,14 @@

    Canvas#
    • width – Width of the canvas drawing space.

    • height – Height of the canvas drawing space.

    • -
    • background_color – Background color of the canvas, defaults to “white”.

    • -
    • border_width – Width of the border around the canvas, defaults to 0.

    • -
    • border_color – Color of the border around the canvas, defaults to “black”.

    • +
    • min_xy – Minimum x and y values of the canvas drawing space, defaults to (0, 0).

    • caption – Caption for the canvas, defaults to None.

    • centered – Whether to center the canvas, defaults to True.

    • full_width – Whether to make the canvas full width, defaults to False.

    • +
    • display_type – Where the canvas should be displayed, defaults to “report-only”

    • +
    • background_color – Background color of the canvas, defaults to “white”.

    • +
    • border_width – Width of the border around the canvas, defaults to 0.

    • +
    • border_color – Color of the border around the canvas, defaults to “black”.

    • scale – Scale the display size of the canvas, defaults to 1.

    • default_element_fill – Default fill color for elements, defaults to “none”.

    • default_element_stroke – Default stroke color for elements, defaults to “black”.

    • @@ -563,12 +565,114 @@

      Canvas Elements +
      +class efficalc.canvas.Text(text: str, x: float, y: float, font_size: float | str = 'auto', rotate: float = 0, horizontal_base: Literal['start', 'center', 'end'] = 'start', vertical_base: Literal['auto', 'top', 'middle', 'bottom'] = 'auto', fill: str = 'black', stroke: str = 'none', stroke_width: float = 0)#
      +

      Represents a text element in the canvas.

      +
      +
      Parameters:
      +
        +
      • text – The text content to be rendered.

      • +
      • x – The x-coordinate of the text base point.

      • +
      • y – The y-coordinate of the text base point.

      • +
      • font_size – The font size of the text.

      • +
      • rotate – The rotation angle of the text about the base point (clockwise in degrees).

      • +
      • horizontal_base – The horizontal base point location of the text.

      • +
      • vertical_base – The vertical base point location of the text.

      • +
      • fill – The fill color of the text.

      • +
      • stroke – The stroke color of the text.

      • +
      • stroke_width – The stroke width of the text.

      • +
      +
      +
      +
      +
      +to_svg() str#
      +

      Converts the element to its SVG representation.

      +
      +
      Returns:
      +

      SVG representation of the element.

      +
      +
      +
      + +

    + +
    +
    +class efficalc.canvas.Dimension(x1: float, y1: float, x2: float, y2: float, text: str | None = None, gap: float = 0, offset: float = 10, unit: str | None = None, text_position: Literal['top', 'bottom'] = 'top', text_size: float = 1.0, **kwargs)#
    +

    Represents a dimension line between two points.

    +
    +
    Parameters:
    +
      +
    • x1 – X coordinate of the start point.

    • +
    • y1 – Y coordinate of the start point.

    • +
    • x2 – X coordinate of the end point.

    • +
    • y2 – Y coordinate of the end point.

    • +
    • text – The text to display as the dimension, defaults to the length of the dimension line.

    • +
    • gap – The gap between the points being dimensioned and the start of the extension lines, defaults to 2.

    • +
    • offset – Offset distance from the parallel dimension line to the dimensioned points. Positive offset will +result in the dimension extending upward, negative offset will result in the dimension extending downward. +Defaults to 10.

    • +
    • unit – The unit of the dimension, defaults to None.

    • +
    • text_position – The position of the text relative to the dimension line. Defaults to ‘top’.

    • +
    • text_size – Scaling factor for text size. Defaults to 1.

    • +
    • kwargs – Additional properties such as fill, stroke, and stroke_width.

    • +
    +
    +
    +
    +
    +to_svg() str#
    +

    Converts the element to its SVG representation.

    +
    +
    Returns:
    +

    SVG representation of the element.

    +
    +
    +
    + +
    + +
    +
    +class efficalc.canvas.Leader(marker_x: float, marker_y: float, text_x: float, text_y: float, text: str, marker: Marker = None, landing_len: float = 5, direction: Literal['right', 'left'] = 'right', text_size: float = 1.0, **kwargs)#
    +

    Represents a leader text with a polyline leader.

    +
    +
    Parameters:
    +
      +
    • marker_x – X coordinate of the marker point.

    • +
    • marker_y – Y coordinate of the marker point.

    • +
    • text_x – X coordinate of the text position.

    • +
    • text_y – Y coordinate of the text position.

    • +
    • text – The text content to display.

    • +
    • marker – The marker at the end of the leader, defaults to None.

    • +
    • landing_len – The length of the landing line.

    • +
    • direction – Relative position of the text in relationship to the landing line (‘right’ or ‘left’).

    • +
    • text_size – Scaling factor for text size. Defaults to 1.

    • +
    • kwargs – Additional properties such as fill, stroke, and stroke_width.

    • +
    +
    +
    +
    +
    +to_svg() str#
    +

    Converts the element to its SVG representation.

    +
    +
    Returns:
    +

    SVG representation of the element.

    +
    +
    +
    + +
    +

    Line/Polyline Markers#

    -class efficalc.canvas.ArrowMarker(reverse: bool = False, orientation: Literal['auto', 'auto-start-reverse'] | float = 'auto', **kwargs)#
    +class efficalc.canvas.ArrowMarker(reverse: bool = False, orientation: Literal['auto', 'auto-start-reverse'] | float = 'auto', base: Literal['point', 'center', 'flat'] = 'center', **kwargs)#

    Creates an arrow marker for a line or polyline.

    Parameters:
    @@ -667,6 +771,18 @@

    Base Classes +
    +class efficalc.canvas.ElementWithMarkers(fill: str = None, stroke: str = None, stroke_width: float = None)#
    +

    Base class for elements with markers. Subclasses must implement the _get_markers method.

    +
    +
    +get_markers() list[Marker]#
    +

    Returns a list of all markers contained by the element with formatting applied.

    +
    + +

    +
    @@ -763,6 +879,18 @@

    Base ClassesRectangle.to_svg() +
  • Text +
  • +
  • Dimension +
  • +
  • Leader +
  • Line/Polyline Markers
  • @@ -798,7 +930,7 @@

    Base Classes +

    diff --git a/docs/constants.html b/docs/constants.html index 2686ab6..5170583 100644 --- a/docs/constants.html +++ b/docs/constants.html @@ -6,7 +6,7 @@ - Constants and Unit Conversions - efficalc 1.2.0 documentation + Constants and Unit Conversions - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    - + diff --git a/docs/examples.html b/docs/examples.html index a5415b6..0af80d9 100644 --- a/docs/examples.html +++ b/docs/examples.html @@ -6,7 +6,7 @@ - Examples - efficalc 1.2.0 documentation + Examples - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    diff --git a/docs/figures.html b/docs/figures.html index 5c07851..b5772e7 100644 --- a/docs/figures.html +++ b/docs/figures.html @@ -6,7 +6,7 @@ - Figures in Calc Reports - efficalc 1.2.0 documentation + Figures in Calc Reports - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -459,7 +459,7 @@

    Example#<

    - + diff --git a/docs/genindex.html b/docs/genindex.html index 2cae1e2..0626299 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -4,7 +4,7 @@ - Index - efficalc 1.2.0 documentation + Index - efficalc 1.2.6 documentation @@ -124,7 +124,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -150,7 +150,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -310,6 +310,8 @@

    D

    @@ -457,6 +465,8 @@

    K

    L

      @@ -343,6 +345,8 @@

      E

    @@ -636,9 +648,13 @@

    S

    T

    @@ -728,7 +750,7 @@

    Z

    - + diff --git a/docs/get_started.html b/docs/get_started.html index ac1da43..5dde751 100644 --- a/docs/get_started.html +++ b/docs/get_started.html @@ -6,7 +6,7 @@ - Quickstart - efficalc 1.2.0 documentation + Quickstart - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    diff --git a/docs/index.html b/docs/index.html index 0eb3c54..26313fd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,7 @@ - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -333,7 +333,7 @@

    More#

    - + diff --git a/docs/integration.html b/docs/integration.html index a0ffdaf..500e431 100644 --- a/docs/integration.html +++ b/docs/integration.html @@ -6,7 +6,7 @@ - Integrating and Extending efficalc - efficalc 1.2.0 documentation + Integrating and Extending efficalc - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    diff --git a/docs/math_operations.html b/docs/math_operations.html index 34e25bb..74daf9e 100644 --- a/docs/math_operations.html +++ b/docs/math_operations.html @@ -6,7 +6,7 @@ - Math Operations - efficalc 1.2.0 documentation + Math Operations - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    - + diff --git a/docs/objects.inv b/docs/objects.inv index b192a50..7e2c5f2 100644 Binary files a/docs/objects.inv and b/docs/objects.inv differ diff --git a/docs/purpose.html b/docs/purpose.html index dbf81a3..f65e322 100644 --- a/docs/purpose.html +++ b/docs/purpose.html @@ -6,7 +6,7 @@ - Purpose and Background - efficalc 1.2.0 documentation + Purpose and Background - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    diff --git a/docs/py-modindex.html b/docs/py-modindex.html index 4856312..905d769 100644 --- a/docs/py-modindex.html +++ b/docs/py-modindex.html @@ -4,7 +4,7 @@ - Python Module Index - efficalc 1.2.0 documentation + Python Module Index - efficalc 1.2.6 documentation @@ -124,7 +124,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -150,7 +150,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -276,7 +276,7 @@

    Python Module Index

    - + diff --git a/docs/search.html b/docs/search.html index 293d64e..f31912a 100644 --- a/docs/search.html +++ b/docs/search.html @@ -4,7 +4,7 @@ - Search - efficalc 1.2.0 documentation + Search - efficalc 1.2.6 documentation @@ -123,7 +123,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -149,7 +149,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -254,7 +254,7 @@
    - + diff --git a/docs/searchindex.js b/docs/searchindex.js index 1bc3cf7..4717485 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["base_classes", "calculation_helpers", "canvas", "constants", "examples", "figures", "get_started", "index", "integration", "math_operations", "purpose", "section_properties", "styling", "testing"], "filenames": ["base_classes.rst", "calculation_helpers.rst", "canvas.rst", "constants.rst", "examples.rst", "figures.rst", "get_started.rst", "index.rst", "integration.rst", "math_operations.rst", "purpose.rst", "section_properties.rst", "styling.rst", "testing.rst"], "titles": ["Base Classes", "Calculation Helpers", "Drawing on a Canvas", "Constants and Unit Conversions", "Examples", "Figures in Calc Reports", "Quickstart", "efficalc", "Integrating and Extending efficalc", "Math Operations", "Purpose and Background", "Section Properties", "Styling Reports", "Testing Your Calculations"], "terms": {"sphinx": [], "quickstart": 7, "thu": [], "mar": [], "7": [0, 2, 8], "21": [], "03": [], "13": 2, "2024": [], "you": [0, 2, 5, 6, 7, 8, 10, 13], "can": [0, 1, 2, 5, 6, 7, 8, 10, 12, 13], "adapt": 13, "thi": [0, 1, 2, 3, 5, 6, 7, 8, 10, 11], "file": 1, "complet": [1, 10], "your": [0, 2, 5, 6, 7, 8, 10], "like": [7, 8, 10], "should": [0, 1, 2, 5], "least": [], "contain": [8, 11], "root": 9, "toctre": [], "direct": 2, "_home": [], "A": [0, 1, 2, 7, 8, 11, 13], "featur": [7, 8], "rich": 7, "librari": [5, 7, 8], "reimagin": 7, "calcul": [0, 2, 3, 4, 5, 7, 10], "i": [0, 1, 2, 5, 6, 7, 8, 10, 11, 13], "design": [0, 6, 7, 8, 10, 11, 13], "transform": 7, "how": [5, 6, 7, 8, 10], "engin": [7, 8, 10, 13], "approach": [7, 8, 10], "move": 7, "awai": 7, "from": [0, 1, 3, 6, 7, 8, 10, 11], "tradit": [7, 10], "method": [1, 7, 10], "manual": [7, 10], "spreadsheet": [7, 10, 13], "toward": 7, "effici": [7, 8, 10], "accur": 7, "collabor": 7, "process": [5, 7, 10], "built": 7, "modern": [7, 8], "mind": 7, "leverag": 7, "power": [7, 8, 9], "python": [0, 5, 7, 8, 10, 13], "offer": 7, "an": [0, 1, 2, 5, 6, 7, 8, 10, 11, 12, 13], "extens": 7, "testabl": 7, "framework": [7, 13], "build": [6, 7, 8, 10], "ani": [0, 1, 2, 6, 7, 8, 10, 12, 13], "order": 7, "doesn": [7, 10], "t": [7, 10, 11], "lock": 7, "predefin": 7, "sequenc": [7, 12], "suit": 7, "project": [6, 7, 8, 10], "": [1, 2, 5, 6, 7, 8, 9, 10, 11], "need": [2, 5, 7, 10, 12, 13], "control": [0, 7], "content": 7, "decid": 7, "what": [7, 10], "displai": [0, 2, 5, 7, 12], "report": [0, 1, 2, 3, 4, 7, 8, 10], "ensur": [7, 13], "onli": [0, 1, 7, 8, 10], "relev": [1, 7], "inform": [7, 8], "commun": [7, 10], "make": [2, 6, 7, 8, 10, 13], "concis": 7, "detail": [7, 8, 10, 11], "desir": [1, 7], "autom": 7, "gener": [0, 1, 2, 4, 5, 7, 8, 10], "automat": 7, "creat": [1, 2, 5, 7, 10, 12, 13], "profession": 7, "crystal": 7, "clear": [1, 7, 8], "review": [7, 10], "ambigu": 7, "free": [7, 8], "submitt": 7, "specif": [7, 13], "out": [2, 7, 10], "box": [7, 11], "helper": [2, 7], "common": [2, 6, 7, 10, 12], "problem": [5, 7], "section": [0, 7, 8], "properti": [2, 7, 8], "databas": [7, 11], "unit": [0, 7, 12], "reusabl": 7, "templat": [0, 7], "onc": 7, "reus": 7, "them": [5, 7, 10, 13], "across": 7, "multipl": [0, 2, 7, 8, 9, 12], "open": [1, 7], "sourc": [2, 4, 5, 7], "ar": [0, 1, 2, 4, 5, 6, 7, 8, 10, 12, 13], "we": [4, 5, 6, 7, 8, 10, 13], "miss": 7, "wish": 7, "had": 7, "request": [2, 7, 8], "yourself": [7, 10], "so": [6, 7, 8, 10], "everyon": 7, "benefit": 7, "improv": [7, 10], "pleas": 7, "give": [6, 7, 10, 13], "try": [7, 10], "let": [5, 7], "u": [0, 5, 6, 7, 10], "know": [5, 7], "think": [7, 8], "purpos": [7, 11], "base": [1, 7, 8, 10], "class": [1, 5, 6, 7, 11], "math": 7, "oper": [0, 7], "constant": [0, 7, 11], "convers": 7, "style": [2, 7], "integr": [5, 7, 10], "extend": [7, 10], "test": 7, "exampl": [6, 7, 8, 10, 12], "index": [], "modul": [], "search": [], "page": [], "thing": [2, 10, 13], "don": 10, "about": [8, 10, 11], "excel": [8, 10], "tabl": 8, "To": [5, 6, 10], "err": 13, "human": 13, "realli": [8, 13], "foul": 13, "up": [1, 2, 8, 13], "comput": [5, 13], "paul": 13, "r": [2, 5, 13], "ehrlich": 13, "background": [2, 7], "efficalc": [0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 13], "wa": [0, 8, 10, 11], "develop": [0, 10, 13], "provid": [0, 1, 8, 10, 13], "robust": 10, "flexibl": [5, 6, 10], "altern": 10, "which": [0, 1, 9, 10], "ha": [10, 11], "remain": 10, "industri": [10, 13], "standard": [10, 11, 12], "decad": [9, 10], "while": 10, "versatil": 10, "us": [0, 1, 5, 6, 8, 10, 11, 12, 13], "mani": [6, 8, 10, 12], "differ": [5, 6, 8, 10], "limit": 10, "its": [0, 1, 2, 10], "abil": 10, "furthermor": 10, "tool": 10, "particularli": 10, "good": [0, 10], "one": [0, 8, 10, 11, 13], "especi": [10, 13], "tailor": 10, "experi": 10, "work": [6, 8, 10, 13], "here": [2, 4, 5, 6, 8, 10, 12], "few": [4, 6, 10], "want": [6, 8, 10, 13], "fix": [8, 10], "If": [0, 1, 2, 5, 10, 11, 12], "write": [8, 10, 13], "ll": [6, 8, 10], "have": [0, 1, 2, 6, 8, 10], "some": [2, 8, 10, 12], "point": [2, 10, 11], "It": [0, 1, 10], "could": [8, 10], "debug": 10, "our": [6, 8, 10, 13], "own": 10, "understand": 10, "colleagu": 10, "share": [8, 10], "modifi": 10, "old": [10, 13], "repurpos": 10, "slightli": 10, "scenario": [8, 10], "when": [0, 5, 6, 8, 10, 13], "hard": 10, "follow": [5, 10, 11], "get": [1, 8, 10, 13], "wai": [5, 6, 8, 10, 13], "Not": [6, 8, 10], "tediou": 10, "annoi": 10, "lead": 10, "cost": 10, "time": [5, 9, 10, 13], "reput": 10, "potenti": 10, "licens": 10, "There": [6, 10], "also": [1, 8, 10], "other": [1, 8, 10, 12, 13], "check": [0, 10], "offici": 10, "peer": 10, "In": [8, 10, 13], "case": [8, 10, 12, 13], "error": [0, 8, 10], "ridden": 10, "host": 10, "implic": 10, "includ": [2, 10], "costli": 10, "delai": 10, "loss": 10, "rapport": 10, "import": [0, 2, 5, 6, 10], "client": 10, "take": [6, 10, 13], "look": [8, 10], "easier": 10, "imagin": 10, "all": [1, 2, 6, 8, 10, 13], "steel": 10, "floor": [9, 10], "beam": [8, 10], "favorit": [8, 10], "go": 10, "through": [8, 10], "input": [0, 1, 3, 8, 10, 12, 13], "uniqu": [10, 13], "dimens": 10, "load": [5, 10], "copi": 10, "next": [10, 13], "But": [6, 8, 10, 13], "realiz": 10, "cell": 10, "mayb": 10, "chang": [0, 8, 10, 13], "type": [0, 1, 2, 5, 8, 10, 11], "avail": [8, 10], "updat": [6, 8, 10], "back": [10, 11, 13], "everi": [8, 10], "same": [6, 8, 10], "exact": 10, "howev": [8, 10], "actual": [6, 9, 10], "function": [1, 2, 10, 11, 13], "Then": [10, 13], "re": [8, 10], "run": [1, 5, 6, 8, 10, 13], "simplifi": [8, 10], "all_beam_configur": 10, "1": [0, 2, 3, 5, 9, 10, 11, 12], "12": [2, 3, 8, 10], "3": [0, 2, 3, 5, 6, 8, 10, 11], "34": 10, "50": [2, 8, 10], "4": [0, 2, 5, 6, 8, 10, 11, 13], "15": [2, 10], "2": [0, 2, 3, 5, 6, 8, 9, 10, 11, 12, 13], "55": 10, "25": [2, 10, 13], "def": [2, 5, 6, 8, 10, 13], "beam_calcul": 10, "name": [0, 1, 10, 11, 12], "span": [8, 10], "ultimate_load": 10, "steel_strength": 10, "insert": 10, "design_all_beam": 10, "configur": 10, "result": [0, 1, 3, 8, 10, 12, 13], "print": [0, 1, 6, 8, 10], "return": [0, 1, 2, 5, 6, 9, 10, 11], "more": [4, 5, 6, 9, 10, 12, 13], "depth": [10, 11], "advanc": [6, 10], "At": 10, "end": [2, 10], "most": [6, 8, 10, 12], "submit": 10, "independ": 10, "author": 10, "noth": [9, 10], "just": [5, 8, 10], "bunch": 10, "number": [0, 10], "mai": [0, 2, 5, 6, 8, 10], "right": [2, 6, 8, 10], "often": 10, "facilit": 10, "proper": 10, "document": [1, 6, 10], "consum": 10, "By": [5, 10], "creation": [8, 10], "highli": 10, "readabl": [8, 10], "enabl": [8, 10], "spend": 10, "do": [6, 8, 10], "thei": [1, 10], "less": [3, 10], "calc": [2, 7, 10, 13], "focu": 10, "might": [8, 10], "veri": 10, "These": [8, 10], "dai": 10, "increas": [10, 13], "whether": [0, 2, 5, 10, 11], "csi": 10, "oapi": 10, "etab": 10, "analysi": 10, "grasshopp": 10, "rhino": 10, "script": 10, "parametr": 10, "model": 10, "without": [0, 1, 10], "lot": [5, 6, 8, 10], "past": 10, "date": 10, "intervent": 10, "nativ": 10, "plug": 10, "directli": [5, 10, 11], "bypass": 10, "friction": 10, "data": [5, 10], "mention": [8, 10], "ideal": 10, "languag": 10, "larg": [10, 13], "amount": 10, "With": [8, 10, 13], "panda": 10, "numpi": 10, "matplotlib": 10, "manag": [2, 10], "set": [0, 1, 2, 8, 10], "save": [1, 5, 8, 10], "headach": 10, "v": [3, 8, 10], "idea": [8, 10], "aim": 10, "shift": 10, "code": [0, 2, 4, 6, 8, 10, 12], "driven": 10, "empow": 10, "user": 10, "reliabl": [10, 13], "transit": 10, "qualiti": 10, "better": [8, 10], "among": 10, "team": 10, "stakehold": 10, "new": [6, 8, 13], "releas": 6, "distribut": 6, "pypi": 6, "via": 6, "pip": 6, "best": [6, 8], "defin": [0, 1, 2, 6, 8, 11, 13], "The": [0, 1, 2, 5, 6, 8, 11, 13], "For": [4, 6, 8, 12], "pythagorean": 6, "theorem": 6, "perimet": [6, 11], "triangl": [6, 8], "titl": [0, 6, 8], "sqrt": [6, 8, 9], "descript": [0, 6, 8], "length": [0, 6, 8], "side": 6, "b": [0, 6, 8, 11], "c": [0, 5, 6, 8, 11], "hypotenus": [6, 8], "p": 6, "produc": [6, 8], "browser": [1, 6], "someth": 6, "simpl": [5, 6, 8, 12], "report_build": [1, 6], "calculationreportbuild": [], "pythagorean_perimet": 6, "builder": [6, 8], "view_report": [1, 6, 8], "nice": 6, "now": [6, 8], "great": [6, 8], "easi": [6, 8, 13], "alwai": 6, "default": [0, 1, 2, 5, 6, 8], "gave": 6, "luckili": 6, "super": 6, "suppli": [5, 6], "overrid": [0, 1, 6], "second": [0, 6], "argument": [6, 8, 9], "new_input": 6, "5": [0, 2, 6, 8, 13], "6": [0, 2, 6, 8, 11], "show": [0, 3, 5, 6, 8], "And": 6, "well": [2, 6, 13], "real": 8, "life": 8, "overal": [6, 11], "pattern": 6, "matter": 6, "deeper": 6, "dive": 6, "api": 6, "see": [0, 5, 6, 8], "option": [0, 1, 2, 5, 6, 8], "perfect": [6, 8], "happi": 6, "equat": [10, 13], "stage": 10, "assumpt": [0, 1], "str": [0, 1, 2, 5, 11], "meant": 0, "clearli": 0, "declar": 0, "form": 0, "basi": 0, "paramet": [0, 1, 2, 5, 9, 11], "text": [0, 12], "describ": 0, "seismic": 0, "provis": 0, "asc": 0, "16": [0, 2], "assum": 0, "variable_nam": 0, "express": [0, 9], "variabl": [0, 3, 8, 9, 12], "float": [0, 2, 8, 11], "int": 0, "none": [0, 1, 2, 5, 8], "refer": 0, "result_check": [0, 1, 13], "bool": [0, 1, 2, 5], "fals": [0, 1, 2, 5], "primari": 0, "object": [0, 1, 3, 5, 8, 9], "symbol": 0, "latex": [0, 9, 12], "format": [0, 1, 12], "e": [0, 1, 2, 3, 5, 9], "physic": 0, "short": 0, "g": [0, 1, 5], "accompani": 0, "indic": [0, 11], "final": [0, 8], "true": [0, 1, 2, 13], "portal": 0, "version": 0, "ft": [0, 3, 8, 11], "1ft": 0, "4ft": 0, "5ft": 0, "estimate_display_length": 0, "calculationlength": 0, "estim": 0, "substitut": 0, "represent": [0, 2], "get_valu": [0, 8], "alia": [0, 9], "valueerror": [0, 11], "zerodivisionerror": 0, "0": [0, 2, 3, 8, 12], "messag": 0, "self": 0, "evalu": 0, "str_result_with_descript": 0, "string": [0, 1, 2], "symbolicexpr": 0, "str_substitut": 0, "valu": [0, 1, 9, 11], "str_symbol": 0, "qualnam": [], "start": [2, 5], "boundari": [], "comparison": [0, 1], "compar": 0, "liter": [0, 1, 2], "true_messag": 0, "ok": 0, "false_messag": 0, "explicit": 0, "against": 0, "specifi": [0, 1, 2, 11], "didplai": 0, "depend": 0, "first": 0, "comparis": 0, "desplai": 0, "get_messag": 0, "appropri": 0, "reult": 0, "NO": 0, "is_pass": 0, "comparisonstat": 0, "comparator2": 0, "doe": [0, 1, 2, 9], "given": [0, 1, 2, 8, 13], "rather": 0, "exactli": 0, "annot": 0, "embellish": 0, "els": [0, 8], "logic": 0, "third": 0, "requir": [0, 5], "pass": [0, 9], "head": 0, "head_level": 0, "8": [0, 2], "add": [0, 2, 5, 9], "auto": [0, 2], "increment": 0, "size": [0, 2, 8, 11], "larger": 0, "higher": 0, "level": 0, "than": [0, 8], "lower": 0, "each": [0, 8], "correspond": 0, "posit": 0, "would": 0, "befor": [0, 12], "anoth": 0, "default_valu": [0, 1], "input_typ": 0, "select": 0, "select_opt": 0, "list": [0, 1, 2, 12], "min_valu": 0, "max_valu": 0, "num_step": 0, "element": [0, 12, 13], "overridden": 0, "runner": 0, "html": [0, 1, 5], "applic": 0, "minimum": [0, 9], "allow": [0, 2], "maximum": [0, 9], "interv": 0, "between": [0, 8, 11], "legal": 0, "field": 0, "http": [0, 4, 8, 12], "mozilla": 0, "org": 0, "en": 0, "doc": 0, "web": [0, 1, 11], "attribut": 0, "step": 0, "note": [0, 11], "current": [0, 5], "str_result_with_nam": 0, "textblock": 0, "block": 0, "main": [0, 4], "bolder": 0, "save_calculation_item": 1, "item": 1, "global": 1, "store": [1, 5], "clear_saved_object": 1, "get_override_or_default_valu": 1, "input_nam": 1, "found": [1, 8, 11], "set_input_default_overrid": 1, "default_overrid": 1, "dict": [1, 13], "get_all_calc_object": 1, "clear_all_input_default_overrid": 1, "calculationrunn": [1, 13], "calc_funct": [1, 13], "callabl": 1, "input_v": 1, "execut": 1, "were": 1, "dure": 1, "instanti": 1, "perform": [1, 5], "necessari": [1, 8], "ignor": 1, "dictionari": 1, "empti": 1, "calculate_all_item": 1, "etc": [1, 2, 5], "calculate_result": [1, 13], "filter": 1, "those": [1, 13], "been": [1, 8], "mark": [1, 9], "where": [1, 2, 5, 8, 11], "view": [1, 8], "immedi": 1, "accordingli": 1, "kei": 1, "get_html_as_str": 1, "save_report": 1, "folder_path": 1, "file_nam": 1, "calc_report": 1, "open_on_cr": 1, "locat": [1, 2], "exist": 1, "path": [1, 2, 5], "folder": [1, 4], "filepath": 1, "temporari": 1, "pdf": 1, "calculation_runn": 1, "all_aisc_wide_flange_nam": [], "immut": [], "constructor": [], "tupl": 2, "iter": [], "initi": [], "w44x335": [], "w44x290": [], "w44x262": [], "w40x655": [], "w44x230": [], "w40x503": [], "w40x593": [], "w40x431": [], "w40x397": [], "w40x372": [], "w40x297": [], "w40x362": [], "w40x277": [], "w40x249": [], "w40x215": [], "w40x199": [], "w40x324": [], "w40x392": [], "w40x331": [], "w40x327": [], "w40x294": [], "w40x278": [], "w40x264": [], "w40x235": [], "w40x211": [], "w40x183": [], "w40x149": [], "w40x167": [], "w36x853": [], "w36x802": [], "w36x723": [], "w36x652": [], "w36x529": [], "w36x487": [], "w36x925": [], "w36x441": [], "w36x395": [], "w36x361": [], "w36x330": [], "w36x302": [], "w36x262": [], "w36x282": [], "w36x231": [], "w36x247": [], "w36x256": [], "w36x232": [], "w36x210": [], "w36x194": [], "w36x182": [], "w36x170": [], "w36x160": [], "w36x150": [], "w36x135": [], "w33x387": [], "w33x354": [], "w33x291": [], "w33x318": [], "w33x241": [], "w33x263": [], "w33x221": [], "w33x201": [], "w33x169": [], "w33x152": [], "w33x141": [], "w33x130": [], "w33x118": [], "w30x357": [], "w30x391": [], "w30x326": [], "w30x292": [], "w30x235": [], "w30x261": [], "w30x191": [], "w30x211": [], "w30x173": [], "w30x148": [], "w30x124": [], "w30x132": [], "w30x116": [], "w30x108": [], "w30x99": [], "w30x90": [], "w27x368": [], "w27x539": [], "w27x336": [], "w27x307": [], "w27x281": [], "w27x258": [], "w27x235": [], "w27x194": [], "w27x217": [], "w27x178": [], "w27x161": [], "w27x129": [], "w27x146": [], "w27x114": [], "w27x102": [], "w27x84": [], "w24x370": [], "w27x94": [], "w24x335": [], "w24x306": [], "w24x279": [], "w24x250": [], "w24x229": [], "w24x192": [], "w24x207": [], "w24x176": [], "w24x162": [], "w24x146": [], "w24x117": [], "w24x131": [], "w24x104": [], "w24x94": [], "w24x103": [], "w24x84": [], "w24x68": [], "w24x76": [], "w24x62": [], "w24x55": [], "w21x275": [], "w21x248": [], "w21x201": [], "w21x223": [], "w21x182": [], "w21x166": [], "w21x147": [], "w21x132": [], "w21x111": [], "w21x93": [], "w21x73": [], "w21x122": [], "w21x83": [], "w21x68": [], "w21x62": [], "w21x101": [], "w21x55": [], "w21x48": [], "w21x50": [], "w21x57": [], "w18x311": [], "w21x44": [], "w18x283": [], "w18x258": [], "w18x234": [], "w18x192": [], "w18x211": [], "w18x175": [], "w18x158": [], "w18x143": [], "w18x130": [], "w18x119": [], "w18x106": [], "w18x86": [], "w18x97": [], "w18x76": [], "w18x71": [], "w18x65": [], "w18x55": [], "w18x60": [], "w18x50": [], "w18x46": [], "w18x40": [], "w18x35": [], "w16x100": [], "w16x89": [], "w16x67": [], "w16x77": [], "w16x57": [], "w16x50": [], "w16x40": [], "w16x45": [], "w16x36": [], "w16x31": [], "w16x26": [], "w14x873": [], "w14x808": [], "w14x730": [], "w14x665": [], "w14x550": [], "w14x605": [], "w14x500": [], "w14x455": [], "w14x426": [], "w14x398": [], "w14x370": [], "w14x311": [], "w14x342": [], "w14x283": [], "w14x257": [], "w14x233": [], "w14x211": [], "w14x193": [], "w14x176": [], "w14x159": [], "w14x145": [], "w14x132": [], "w14x120": [], "w14x99": [], "w14x109": [], "w14x90": [], "w14x82": [], "w14x74": [], "w14x68": [], "w14x61": [], "w14x53": [], "w14x48": [], "w14x43": [], "w14x38": [], "w14x34": [], "w14x30": [], "w14x26": [], "w14x22": [], "w12x336": [], "w12x305": [], "w12x279": [], "w12x252": [], "w12x230": [], "w12x210": [], "w12x190": [], "w12x170": [], "w12x152": [], "w12x136": [], "w12x120": [], "w12x106": [], "w12x87": [], "w12x96": [], "w12x79": [], "w12x72": [], "w12x65": [], "w12x58": [], "w12x53": [], "w12x50": [], "w12x45": [], "w12x35": [], "w12x40": [], "w12x30": 8, "w12x26": 8, "w12x22": [], "w12x19": 8, "w12x14": 8, "w12x16": [], "w10x112": [], "w10x100": [], "w10x77": [], "w10x88": [], "w10x68": [], "w10x60": [], "w10x54": [], "w10x45": [], "w10x49": 8, "w10x39": [], "w10x30": [], "w10x33": 8, "w10x26": [], "w10x22": [], "w10x19": 8, "w10x17": [], "w10x15": [], "w10x12": 8, "w8x67": [], "w8x48": [], "w8x58": [], "w8x40": 8, "w8x35": [], "w8x31": [], "w8x28": [], "w8x24": [], "w8x21": 8, "w8x18": [], "w8x15": 8, "w8x13": [], "w8x10": [], "w6x25": [], "w6x20": [], "w6x15": [], "w6x12": [], "w6x16": [], "w6x9": [], "w6x8": [], "w5x19": [], "w5x16": [], "w4x13": [], "m12": [], "5x12": [], "m12x11": [], "5x11": [], "m12x10": [], "m10x9": [], "m10x8": [], "m10x7": [], "m8x6": [], "m6x4": [], "m6x3": [], "m5x18": [], "9": [11, 12], "m4x4": [], "08": [], "m4x6": [], "m4x3": [], "45": [], "s24x106": [], "s24x121": [], "s24x90": [], "s24x100": [], "s24x80": [], "m3x2": [], "s20x96": [], "s20x86": [], "s20x66": [], "s20x75": [], "s18x70": [], "s18x54": [], "s15x50": [], "s15x42": [], "s12x50": [], "s12x40": [], "s12x31": [], "s12x35": [], "s10x35": [], "s10x25": [], "s8x23": [], "s8x18": [], "s6x17": [], "s6x12": [], "s5x10": [], "s4x9": [], "s4x7": [], "s3x7": [], "s3x5": [], "hp18x204": [], "hp18x181": [], "hp18x157": [], "hp18x135": [], "hp16x162": [], "hp16x183": [], "hp16x121": [], "hp16x141": [], "hp16x101": [], "hp16x88": [], "hp14x117": [], "hp14x102": [], "hp14x73": [], "hp14x89": [], "hp12x89": [], "hp12x84": [], "hp12x74": [], "hp12x53": [], "hp10x57": [], "hp10x42": [], "hp12x63": [], "hp8x36": [], "cd": [], "devic": [], "aiscangl": 11, "angl": [9, 11], "aiscchannel": 11, "channel": 11, "aisccircular": 11, "circular": 11, "aiscdoubleangl": 11, "doubleangl": 11, "aiscrectangular": 11, "rectangular": 11, "aiscte": 11, "tee": 11, "aiscwideflang": 11, "wideflang": 11, "get_aisc_angl": 11, "section_s": [8, 11], "fetch": 11, "aisc": 11, "instanc": [3, 9, 11], "popul": 11, "aisc_nam": 11, "rais": [5, 11], "cannot": 11, "get_aisc_channel": 11, "get_aisc_circular": 11, "get_aisc_double_angl": 11, "doubl": 11, "get_aisc_rectangular": 11, "get_aisc_te": 11, "get_aisc_wide_flang": [8, 11], "wide": 11, "flang": 11, "cw": 11, "edi_std_nomenclatur": 11, "iw": 11, "ix": 11, "ii": 11, "iz": 11, "j": 11, "pa": 11, "pa2": 11, "pb": 11, "swa": 11, "swb": 11, "swc": 11, "sx": 11, "sy": 11, "sz": 11, "sza": 11, "szb": 11, "szc": 11, "t_f": 11, "w": [8, 11], "zx": [8, 11], "zy": 11, "b_t": 11, "d": 11, "kde": 11, "kdet": 11, "ro": 11, "rx": [2, 11], "ry": [2, 11], "rz": 11, "tana": 11, "wb": 11, "wc": 11, "x": [2, 11], "xp": 11, "y": [2, 11], "yp": 11, "za": 11, "zb": 11, "zc": 11, "dataclass": 11, "shape": [8, 11], "area": 11, "warp": 11, "edi": 11, "nomenclatur": 11, "moment": 11, "inertia": 11, "axi": [2, 11], "z": 11, "torsion": 11, "minu": [9, 11], "surfac": 11, "singl": [11, 12, 13], "long": 11, "leg": 11, "guid": 11, "19": 11, "elast": 11, "modulu": 11, "addit": [2, 8, 9, 11], "f": 11, "nomin": 11, "weight": [8, 11], "lb": [3, 11], "plastic": 11, "width": [2, 5, 11], "longer": 11, "slender": 11, "ratio": 11, "shorter": 11, "distanc": 11, "outer": 11, "face": 11, "toe": 11, "fillet": 11, "polar": 11, "radiu": [2, 11], "gyrat": 11, "shear": 11, "center": [2, 11], "thick": 11, "tangent": [9, 11], "ax": [5, 11], "graviti": 11, "along": 11, "horizont": [2, 11], "edg": 11, "member": 11, "neutral": 11, "vertic": [2, 11], "h": 11, "pc": 11, "pd": 11, "qf": 11, "qw": 11, "sw1": 11, "sw2": 11, "sw3": 11, "wgi": 11, "wno": 11, "bf": 11, "bfdet": 11, "ddet": 11, "eo": 11, "h_tw": 11, "ho": 11, "rt": 11, "tf": 11, "tfdet": 11, "tw": 11, "twdet": 11, "twdet_2": 11, "cross": 11, "flexur": 11, "static": 11, "abov": [8, 11], "mid": 11, "top": [2, 11], "bottom": 11, "special": 11, "workabl": 11, "gage": 11, "inner": 11, "fasten": 11, "hole": 11, "normal": 11, "centroid": 11, "effect": [11, 13], "d_t": 11, "od": 11, "tde": 11, "tnom": 11, "hss": 11, "round": [2, 9, 11], "pipe": 11, "outsid": 11, "diamet": 11, "wall": 11, "flat": 11, "separ": 11, "bout": 11, "ht": 11, "bin": 11, "b_tde": 11, "h_tde": 11, "typic": 11, "repres": [2, 3, 11], "hollow": 11, "structur": 11, "bf_2tf": 11, "deriv": 11, "m": [11, 12], "cut": 11, "2tf": 11, "half": 11, "k1": 11, "line": 11, "a_bracket": 9, "arg": 9, "bracket": 9, "wrap": [9, 12], "absolut": 9, "arg0": 9, "summat": 9, "arg1": 9, "arglast": 9, "r_bracket": 9, "c_bracket": 9, "curli": [9, 12], "co": 9, "cosinu": 9, "cosh": 9, "hyperbol": 9, "div": 9, "divis": 9, "frac": 9, "div2": 9, "within": [8, 9, 12], "exp": 9, "ln": 9, "natur": [9, 12], "logarithm": 9, "log": 9, "log_arg0": 9, "log10": 9, "log_10": 9, "max": 9, "argn": 9, "min": [9, 12], "sub": 9, "mul": 9, "neg": 9, "negat": 9, "plu": 9, "sum_el": 9, "po": 9, "positivit": 9, "squar": 9, "s_bracket": 9, "sin": 9, "sinu": 9, "sinh": 9, "sqr": 9, "subtract": 9, "tan": 9, "tanh": 9, "zero": 3, "latexexpr_efficalc": 3, "mathemat": 12, "phyical": [], "fundament": [], "overload": [], "numer": [], "throw": [], "except": [], "divsion": [], "consid": [], "4g": [], "3f": [], "unit_format": [], "mathrm": [], "non": [], "ital": [], "insid": [], "mode": [], "expon": [], "scientif": [], "v1": [], "a_": [], "22": [], "mm": [], "v2": [], "876934835": [], "kn": [], "87693": [], "v3": [], "434": [], "cdot": [], "10": 2, "v8": [], "unit_convers": 3, "deg_to_rad": 3, "degre": 3, "radian": 3, "divid": 3, "revers": [2, 3], "180": 3, "deg": 3, "rad": 3, "pi": 3, "180deg": 3, "142": 3, "ft_to_in": 3, "24": 3, "k_to_lb": 3, "kip": 3, "1000": [3, 8], "2000": 3, "reportbuild": [1, 6, 8], "ONE": 3, "two": 3, "call": 8, "possibl": 8, "sometim": 8, "workflow": 8, "compat": 5, "both": 8, "sinc": [], "That": [2, 8], "shown": 8, "easiest": [], "pythagorean_with_param": 8, "default_a": 8, "default_b": 8, "equival": 8, "pythagorean_without_param": 8, "810249675906654": 8, "still": [8, 13], "batch": [5, 8], "collect": 8, "Be": [], "abl": [], "come": [2, 5, 13], "soon": [], "github": [4, 8], "issu": [5, 8], "link": [], "graph": 8, "figur": [2, 7, 8], "matric": 8, "notat": 12, "help": [2, 8, 12], "tip": 12, "charact": 12, "underscor": 12, "_1": 12, "definit": 12, "min_a": 12, "m_2": 12, "must": 12, "brace": 12, "after": [12, 13], "_": 12, "min_": 12, "abc": 12, "m_": 12, "123": 12, "caret": 12, "circumflex": 12, "combin": 12, "www": 12, "overleaf": 12, "com": [4, 8, 12], "learn": 12, "list_of_greek_letters_and_math_symbol": 12, "phi_m": 12, "phi": 12, "squash": 12, "escap": 12, "forward": 12, "slash": 12, "todo": [], "intro": [], "column": 8, "whatev": 8, "highlight": 8, "over": [8, 13], "solut": 8, "As": 8, "bonu": 8, "find": 8, "constraint": 8, "beam_strength": 8, "default_s": 8, "default_span": 8, "default_fi": 8, "fy": 8, "f_y": 8, "ksi": 8, "size_nam": 8, "z_x": 8, "strength": 8, "complex": [8, 13], "m_p": 8, "k": 8, "optim": 8, "moment_strength": 8, "lightest": 8, "strong": 8, "enough": 8, "find_lightest_beam_for_demand": 8, "size_opt": 8, "moment_demand": 8, "lightest_beam": 8, "999999": 8, "strength_info": 8, "size_is_strong_enough": 8, "size_is_lighter_than_best": 8, "certain": 8, "available_beam_sect": 8, "moment_demand_on_beam": 8, "lightest_beam_s": 8, "digest": 8, "summari": 8, "demand": 8, "Or": 8, "extract": 8, "util": 8, "anyth": 8, "extra": 8, "invisible_square_sum": 8, "simpli": 8, "49": 8, "calculate_square_sum": 8, "sum": 8, "sup": [], "begin": [], "align": [], "4pt": [], "therefor": [], "left": 2, "\u00b2": 8, "standalon": 8, "resourc": [5, 8], "appreci": 8, "huge": 8, "ecosystem": 8, "conjunct": 8, "varieti": 8, "everydai": 8, "concept": 8, "youandvern": [4, 8], "feel": 8, "propos": 8, "pull": 8, "ve": 8, "plugin": 8, "elimin": 8, "necess": 8, "intend": 8, "incorpor": 8, "output": 8, "loop": 8, "previous": 8, "get_results_as_dict": [], "toler": 13, "mistak": 13, "slim": 13, "becom": 13, "digit": 13, "opportun": 13, "hundr": 13, "propag": 13, "affect": 13, "part": [2, 13], "unexpect": 13, "accuraci": 13, "evolv": 13, "confid": 13, "expect": 13, "rest": [5, 13], "behav": 13, "world": 13, "softwar": 13, "establish": 13, "disciplin": 13, "appli": [2, 13], "similar": 13, "principl": 13, "straightforward": 13, "assert": 13, "verifi": 13, "test_calc_funct": 13, "calc_function_simpl": 13, "pytest": 13, "prefer": 13, "regularli": 13, "ongo": 13, "continu": 4, "publish": 4, "repo": 4, "tree": 4, "visit": 4, "return_typ": 1, "either": 1, "calculate_results_as_dict": [], "save_fold": 1, "filenam": 1, "open_on_sav": 1, "support": 5, "three": 5, "imag": [2, 5], "caption": [2, 5], "figurefromfil": 5, "lazi": 5, "file_path": 5, "pathlik": 5, "full_width": [2, 5], "tag": 5, "png": 5, "jpg": 5, "svg": [2, 5], "gif": 5, "full": [2, 5], "load_image_data": 5, "my": 5, "pictur": 5, "calc_imag": 5, "popular": 5, "plot": 5, "figurefrommatplotlib": 5, "wrapper": 5, "around": [2, 5], "easili": 5, "pyplot": 5, "plt": 5, "draw_figure_with_matplotlib": 5, "fig": 5, "subplot": 5, "draw": [5, 7], "figurefrombyt": 5, "greater": 5, "throughout": 5, "becaus": 5, "entir": 5, "memori": 5, "figure_byt": 5, "generate_figure_byt": 5, "cloud": 0, "displi": 0, "nor": 0, "pure": 0, "str_result_with_unit": 0, "__str__": 0, "written": 2, "hand": 2, "usual": 2, "graphic": 2, "illustr": 2, "aspect": 2, "geometri": 2, "programmat": 2, "height": 2, "reinforc": 2, "cover": 2, "num_long_bar": 2, "long_bar_radiu": 2, "875": 2, "stirrup_diamet": 2, "375": 2, "stirrup_bend_radiu": 2, "stirrup_hook": 2, "scale": 2, "30": 2, "default_element_stroke_width": 2, "outlin": 2, "beam_outlin": 2, "rectangl": 2, "fill": 2, "bdbdbd": 2, "stirrup": 2, "transvers": 2, "hook": 2, "corner_radiu": 2, "stroke_width": 2, "stroke": 2, "black": 2, "longitudin": 2, "blue": 2, "circl": 2, "long_bar_starting_x": 2, "long_bar_spac": 2, "long_bar_i": 2, "rang": 2, "004aad": 2, "placement": 2, "bar": 2, "placement_bar": 2, "red": 2, "bf211e": 2, "pin": 2, "create_pin_support": 2, "arrow": 2, "create_load_arrow": 2, "marker_end": 2, "arrowmark": 2, "cap": 2, "create_load_cap_lin": 2, "x1": 2, "x2": 2, "100": 2, "20": 2, "60": 2, "40": 2, "80": 2, "diagram": 2, "arrow_count_per_sect": 2, "space": 2, "background_color": 2, "border_width": 2, "border_color": 2, "default_element_fil": 2, "default_element_strok": 2, "canvasel": 2, "ad": 2, "to_svg": 2, "convert": 2, "cx": 2, "cy": 2, "kwarg": 2, "ellips": 2, "y1": 2, "y2": 2, "marker_start": 2, "marker_mid": 2, "to_path_command": 2, "command": 2, "corner": 2, "orient": 2, "circlemark": 2, "get_common_svg_style_el": 2, "context": 2, "canva": 7, "hold": 2, "backdrop": 2, "drawn": 2, "color": 2, "white": 2, "border": 2, "coordin": 2, "param": [], "midpoint": 2, "fit": 2, "match": 2, "connect": 2, "rel": 2, "system": 2, "down": 2}, "objects": {"efficalc": [[0, 0, 1, "", "Assumption"], [0, 0, 1, "", "Calculation"], [0, 0, 1, "", "Comparison"], [0, 0, 1, "", "ComparisonStatement"], [5, 0, 1, "", "FigureFromBytes"], [5, 0, 1, "", "FigureFromFile"], [5, 0, 1, "", "FigureFromMatplotlib"], [0, 0, 1, "", "Heading"], [0, 0, 1, "", "Input"], [0, 0, 1, "", "Symbolic"], [0, 0, 1, "", "TextBlock"], [0, 0, 1, "", "Title"], [9, 2, 1, "", "a_brackets"], [9, 2, 1, "", "absolute"], [9, 2, 1, "", "add"], [9, 2, 1, "", "brackets"], [9, 2, 1, "", "c_brackets"], [1, 2, 1, "", "clear_all_input_default_overrides"], [1, 2, 1, "", "clear_saved_objects"], [3, 3, 0, "-", "constants"], [9, 2, 1, "", "cos"], [9, 2, 1, "", "cosh"], [9, 2, 1, "", "div"], [9, 2, 1, "", "div2"], [9, 2, 1, "", "exp"], [1, 2, 1, "", "get_all_calc_objects"], [1, 2, 1, "", "get_override_or_default_value"], [9, 2, 1, "", "ln"], [9, 2, 1, "", "log"], [9, 2, 1, "", "log10"], [9, 2, 1, "", "maximum"], [9, 2, 1, "", "minimum"], [9, 2, 1, "", "minus"], [9, 2, 1, "", "mul"], [9, 2, 1, "", "neg"], [9, 2, 1, "", "plus"], [9, 2, 1, "", "pos"], [9, 2, 1, "", "power"], [9, 2, 1, "", "r_brackets"], [9, 2, 1, "", "root"], [9, 2, 1, "", "s_brackets"], [1, 2, 1, "", "save_calculation_item"], [1, 2, 1, "", "set_input_default_overrides"], [9, 2, 1, "", "sin"], [9, 2, 1, "", "sinh"], [9, 2, 1, "", "sqr"], [9, 2, 1, "", "sqrt"], [9, 2, 1, "", "sub"], [9, 2, 1, "", "tan"], [9, 2, 1, "", "tanh"], [9, 2, 1, "", "times"], [3, 3, 0, "-", "unit_conversions"]], "efficalc.Calculation": [[0, 1, 1, "", "estimate_display_length"], [0, 1, 1, "", "get_value"], [0, 1, 1, "", "result"], [0, 1, 1, "", "str_result_with_description"], [0, 1, 1, "", "str_substituted"], [0, 1, 1, "", "str_symbolic"]], "efficalc.Comparison": [[0, 1, 1, "", "get_message"], [0, 1, 1, "", "get_value"], [0, 1, 1, "", "is_passing"], [0, 1, 1, "", "result"], [0, 1, 1, "", "str_substituted"], [0, 1, 1, "", "str_symbolic"]], "efficalc.ComparisonStatement": [[0, 1, 1, "", "str_symbolic"]], "efficalc.FigureFromBytes": [[5, 1, 1, "", "load_image_data"]], "efficalc.FigureFromFile": [[5, 1, 1, "", "load_image_data"]], "efficalc.FigureFromMatplotlib": [[5, 1, 1, "", "load_image_data"]], "efficalc.Input": [[0, 1, 1, "", "get_value"], [0, 1, 1, "", "str_result_with_name"]], "efficalc.Symbolic": [[0, 1, 1, "", "estimate_display_length"], [0, 1, 1, "", "get_value"], [0, 1, 1, "", "result"], [0, 1, 1, "", "str_result_with_description"], [0, 1, 1, "", "str_result_with_unit"], [0, 1, 1, "", "str_substituted"], [0, 1, 1, "", "str_symbolic"]], "efficalc.calculation_runner": [[1, 0, 1, "", "CalculationRunner"]], "efficalc.calculation_runner.CalculationRunner": [[1, 1, 1, "", "calculate_all_items"], [1, 1, 1, "", "calculate_results"]], "efficalc.canvas": [[2, 0, 1, "", "ArrowMarker"], [2, 0, 1, "", "Canvas"], [2, 0, 1, "", "CanvasElement"], [2, 0, 1, "", "Circle"], [2, 0, 1, "", "CircleMarker"], [2, 0, 1, "", "Ellipse"], [2, 0, 1, "", "Line"], [2, 0, 1, "", "Marker"], [2, 0, 1, "", "Polyline"], [2, 0, 1, "", "Rectangle"]], "efficalc.canvas.ArrowMarker": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Canvas": [[2, 1, 1, "", "add"], [2, 1, 1, "", "to_svg"]], "efficalc.canvas.CanvasElement": [[2, 1, 1, "", "get_common_svg_style_elements"], [2, 1, 1, "", "to_svg"]], "efficalc.canvas.Circle": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.CircleMarker": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Ellipse": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Line": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Polyline": [[2, 1, 1, "", "to_path_commands"], [2, 1, 1, "", "to_svg"]], "efficalc.canvas.Rectangle": [[2, 1, 1, "", "to_svg"]], "efficalc.constants": [[3, 4, 1, "", "E"], [3, 4, 1, "", "ONE"], [3, 4, 1, "", "PI"], [3, 4, 1, "", "TWO"], [3, 4, 1, "", "ZERO"]], "efficalc.report_builder": [[1, 0, 1, "", "ReportBuilder"]], "efficalc.report_builder.ReportBuilder": [[1, 1, 1, "", "get_html_as_str"], [1, 1, 1, "", "save_report"], [1, 1, 1, "", "view_report"]], "efficalc.sections": [[11, 0, 1, "", "AiscAngle"], [11, 0, 1, "", "AiscChannel"], [11, 0, 1, "", "AiscCircular"], [11, 0, 1, "", "AiscDoubleAngle"], [11, 0, 1, "", "AiscRectangular"], [11, 0, 1, "", "AiscTee"], [11, 0, 1, "", "AiscWideFlange"], [11, 2, 1, "", "get_aisc_angle"], [11, 2, 1, "", "get_aisc_channel"], [11, 2, 1, "", "get_aisc_circular"], [11, 2, 1, "", "get_aisc_double_angle"], [11, 2, 1, "", "get_aisc_rectangular"], [11, 2, 1, "", "get_aisc_tee"], [11, 2, 1, "", "get_aisc_wide_flange"]], "efficalc.unit_conversions": [[3, 4, 1, "", "deg_to_rad"], [3, 4, 1, "", "ft_to_in"], [3, 4, 1, "", "k_to_lb"]]}, "objtypes": {"0": "py:class", "1": "py:method", "2": "py:function", "3": "py:module", "4": "py:data"}, "objnames": {"0": ["py", "class", "Python class"], "1": ["py", "method", "Python method"], "2": ["py", "function", "Python function"], "3": ["py", "module", "Python module"], "4": ["py", "data", "Python data"]}, "titleterms": {"base": [0, 2], "class": [0, 2], "calcul": [1, 6, 8, 13], "helper": [1, 8], "constant": 3, "unit": 3, "convers": 3, "exampl": [2, 4, 5], "get": 7, "start": 7, "efficalc": [7, 8], "api": [2, 5, 7], "document": 7, "more": [7, 8], "indic": [], "tabl": [], "integr": 8, "extend": 8, "math": 9, "oper": 9, "purpos": 10, "section": [2, 11], "properti": 11, "style": 12, "report": [5, 6, 12], "test": 13, "your": 13, "about": [], "todo": [], "add": [], "simpl": 4, "graphic": [], "anim": [], "see": [], "librari": [], "action": [], "background": 10, "find": 10, "mistak": 10, "autom": 10, "scalabl": 10, "format": 10, "submitt": 10, "modern": 10, "workflow": 10, "A": 10, "new": 10, "era": 10, "instal": 6, "first": 6, "function": [6, 8], "view": 6, "run": [], "differ": [], "input": 6, "valu": [6, 8], "quickstart": 6, "chang": 6, "paramet": 8, "return": 8, "calc": [5, 8], "option": [], "1": [], "recommend": [], "2": [], "subscript": 12, "superscript": 12, "greek": 12, "letter": 12, "symbol": 12, "ad": 12, "space": 12, "hundr": 8, "One": 8, "invis": 8, "come": 8, "soon": 8, "why": 13, "matter": 13, "how": 13, "To": 13, "concret": [2, 4], "beam": [2, 4], "neutral": 4, "axi": 4, "advanc": 4, "steel": 4, "moment": 4, "strength": 4, "figur": 5, "from": 5, "file": 5, "doc": [2, 5], "matplotlib": 5, "raw": 5, "byte": 5, "draw": 2, "canva": 2, "cross": 2, "support": 2, "load": 2, "scheme": 2, "element": 2, "line": 2, "polylin": 2, "marker": 2}, "envversion": {"sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 60}, "alltitles": {"Constants and Unit Conversions": [[3, "constants-and-unit-conversions"]], "Unit Conversions": [[3, "module-efficalc.unit_conversions"]], "Constants": [[3, "id1"]], "Examples": [[4, "examples"], [2, "examples"]], "Simple": [[4, "simple"]], "Concrete Beam Neutral Axis": [[4, "concrete-beam-neutral-axis"]], "Advanced": [[4, "advanced"]], "Steel Beam Moment Strength": [[4, "steel-beam-moment-strength"]], "Quickstart": [[6, "quickstart"]], "Installation": [[6, "installation"]], "First Calculation Function": [[6, "first-calculation-function"]], "View Reports": [[6, "view-reports"]], "Change Input Values": [[6, "change-input-values"]], "Calculation Helpers": [[8, "calculation-helpers"], [1, "calculation-helpers"]], "Integrating and Extending efficalc": [[8, "integrating-and-extending-efficalc"]], "Parameters and Return Values in Calc Functions": [[8, "parameters-and-return-values-in-calc-functions"]], "Hundreds of Calculations with One Function": [[8, "hundreds-of-calculations-with-one-function"]], "Helper Functions": [[8, "helper-functions"]], "Invisible Helpers": [[8, "invisible-helpers"]], "More coming soon": [[8, "more-coming-soon"]], "Math Operations": [[9, "math-operations"]], "Purpose and Background": [[10, "purpose-and-background"]], "Finding mistakes": [[10, "finding-mistakes"]], "Automation and Scalability": [[10, "automation-and-scalability"]], "Formatting and Submittal": [[10, "formatting-and-submittal"]], "Modern Workflows": [[10, "modern-workflows"]], "A New Era": [[10, "a-new-era"]], "Styling Reports": [[12, "styling-reports"]], "Subscripts": [[12, "subscripts"]], "Superscripts": [[12, "superscripts"]], "Greek Letters and Symbols": [[12, "greek-letters-and-symbols"]], "Adding Spaces": [[12, "adding-spaces"]], "Testing Your Calculations": [[13, "testing-your-calculations"]], "Why Testing Matters": [[13, "why-testing-matters"]], "How To Test Your Calculations": [[13, "how-to-test-your-calculations"]], "Base Classes": [[0, "base-classes"], [2, "base-classes"]], "Section Properties": [[11, "section-properties"]], "efficalc": [[7, "efficalc"]], "Get Started": [[7, "get-started"]], "API Documentation": [[7, "api-documentation"]], "More": [[7, "more"]], "API docs": [[5, "api-docs"], [5, "id1"], [5, "id3"], [2, "api-docs"]], "Figures in Calc Reports": [[5, "figures-in-calc-reports"]], "Figure from a file": [[5, "figure-from-a-file"]], "Example": [[5, "example"], [5, "id2"], [5, "id4"]], "Figure from a matplotlib figure": [[5, "figure-from-a-matplotlib-figure"]], "Figure from raw bytes": [[5, "figure-from-raw-bytes"]], "Drawing on a Canvas": [[2, "drawing-on-a-canvas"]], "Concrete beam cross-section": [[2, "concrete-beam-cross-section"]], "Beam support and loading scheme": [[2, "beam-support-and-loading-scheme"]], "Canvas": [[2, "id1"]], "Canvas Elements": [[2, "canvas-elements"]], "Line/Polyline Markers": [[2, "line-polyline-markers"]]}, "indexentries": {"arrowmarker (class in efficalc.canvas)": [[2, "efficalc.canvas.ArrowMarker"]], "canvas (class in efficalc.canvas)": [[2, "efficalc.canvas.Canvas"]], "canvaselement (class in efficalc.canvas)": [[2, "efficalc.canvas.CanvasElement"]], "circle (class in efficalc.canvas)": [[2, "efficalc.canvas.Circle"]], "circlemarker (class in efficalc.canvas)": [[2, "efficalc.canvas.CircleMarker"]], "ellipse (class in efficalc.canvas)": [[2, "efficalc.canvas.Ellipse"]], "line (class in efficalc.canvas)": [[2, "efficalc.canvas.Line"]], "marker (class in efficalc.canvas)": [[2, "efficalc.canvas.Marker"]], "polyline (class in efficalc.canvas)": [[2, "efficalc.canvas.Polyline"]], "rectangle (class in efficalc.canvas)": [[2, "efficalc.canvas.Rectangle"]], "add() (efficalc.canvas.canvas method)": [[2, "efficalc.canvas.Canvas.add"]], "get_common_svg_style_elements() (efficalc.canvas.canvaselement method)": [[2, "efficalc.canvas.CanvasElement.get_common_svg_style_elements"]], "to_path_commands() (efficalc.canvas.polyline method)": [[2, "efficalc.canvas.Polyline.to_path_commands"]], "to_svg() (efficalc.canvas.arrowmarker method)": [[2, "efficalc.canvas.ArrowMarker.to_svg"]], "to_svg() (efficalc.canvas.canvas method)": [[2, "efficalc.canvas.Canvas.to_svg"]], "to_svg() (efficalc.canvas.canvaselement method)": [[2, "efficalc.canvas.CanvasElement.to_svg"]], "to_svg() (efficalc.canvas.circle method)": [[2, "efficalc.canvas.Circle.to_svg"]], "to_svg() (efficalc.canvas.circlemarker method)": [[2, "efficalc.canvas.CircleMarker.to_svg"]], "to_svg() (efficalc.canvas.ellipse method)": [[2, "efficalc.canvas.Ellipse.to_svg"]], "to_svg() (efficalc.canvas.line method)": [[2, "efficalc.canvas.Line.to_svg"]], "to_svg() (efficalc.canvas.polyline method)": [[2, "efficalc.canvas.Polyline.to_svg"]], "to_svg() (efficalc.canvas.rectangle method)": [[2, "efficalc.canvas.Rectangle.to_svg"]]}}) \ No newline at end of file +Search.setIndex({"docnames": ["base_classes", "calculation_helpers", "canvas", "constants", "examples", "figures", "get_started", "index", "integration", "math_operations", "purpose", "section_properties", "styling", "testing"], "filenames": ["base_classes.rst", "calculation_helpers.rst", "canvas.rst", "constants.rst", "examples.rst", "figures.rst", "get_started.rst", "index.rst", "integration.rst", "math_operations.rst", "purpose.rst", "section_properties.rst", "styling.rst", "testing.rst"], "titles": ["Base Classes", "Calculation Helpers", "Drawing on a Canvas", "Constants and Unit Conversions", "Examples", "Figures in Calc Reports", "Quickstart", "efficalc", "Integrating and Extending efficalc", "Math Operations", "Purpose and Background", "Section Properties", "Styling Reports", "Testing Your Calculations"], "terms": {"sphinx": [], "quickstart": 7, "thu": [], "mar": [], "7": [0, 2, 8], "21": [], "03": [], "13": 2, "2024": [], "you": [0, 2, 5, 6, 7, 8, 10, 13], "can": [0, 1, 2, 5, 6, 7, 8, 10, 12, 13], "adapt": 13, "thi": [0, 1, 2, 3, 5, 6, 7, 8, 10, 11], "file": 1, "complet": [1, 10], "your": [0, 2, 5, 6, 7, 8, 10], "like": [7, 8, 10], "should": [0, 1, 2, 5], "least": [], "contain": [2, 8, 11], "root": 9, "toctre": [], "direct": 2, "_home": [], "A": [0, 1, 2, 7, 8, 11, 13], "featur": [7, 8], "rich": 7, "librari": [5, 7, 8], "reimagin": 7, "calcul": [0, 2, 3, 4, 5, 7, 10], "i": [0, 1, 2, 5, 6, 7, 8, 10, 11, 13], "design": [0, 6, 7, 8, 10, 11, 13], "transform": 7, "how": [1, 5, 6, 7, 8, 10], "engin": [7, 8, 10, 13], "approach": [7, 8, 10], "move": 7, "awai": 7, "from": [0, 1, 2, 3, 6, 7, 8, 10, 11], "tradit": [7, 10], "method": [1, 2, 7, 10], "manual": [7, 10], "spreadsheet": [7, 10, 13], "toward": 7, "effici": [7, 8, 10], "accur": 7, "collabor": 7, "process": [5, 7, 10], "built": 7, "modern": [7, 8], "mind": 7, "leverag": 7, "power": [7, 8, 9], "python": [0, 5, 7, 8, 10, 13], "offer": 7, "an": [0, 1, 2, 5, 6, 7, 8, 10, 11, 12, 13], "extens": [2, 7], "testabl": 7, "framework": [7, 13], "build": [6, 7, 8, 10], "ani": [0, 1, 2, 6, 7, 8, 10, 12, 13], "order": 7, "doesn": [7, 10], "t": [7, 10, 11], "lock": 7, "predefin": 7, "sequenc": [7, 12], "suit": 7, "project": [6, 7, 8, 10], "": [1, 2, 5, 6, 7, 8, 9, 10, 11], "need": [2, 5, 7, 10, 12, 13], "control": [0, 1, 7], "content": [2, 7], "decid": 7, "what": [7, 10], "displai": [0, 1, 2, 5, 7, 12], "report": [0, 1, 2, 3, 4, 7, 8, 10], "ensur": [7, 13], "onli": [0, 1, 2, 7, 8, 10], "relev": [1, 7], "inform": [7, 8], "commun": [7, 10], "make": [2, 6, 7, 8, 10, 13], "concis": 7, "detail": [7, 8, 10, 11], "desir": [1, 7], "autom": 7, "gener": [0, 1, 2, 4, 5, 7, 8, 10], "automat": 7, "creat": [1, 2, 5, 7, 10, 12, 13], "profession": 7, "crystal": 7, "clear": [1, 7, 8], "review": [7, 10], "ambigu": 7, "free": [7, 8], "submitt": 7, "specif": [7, 13], "out": [2, 7, 10], "box": [7, 11], "helper": [2, 7], "common": [2, 6, 7, 10, 12], "problem": [5, 7], "section": [0, 7, 8], "properti": [2, 7, 8], "databas": [7, 11], "unit": [0, 2, 7, 12], "reusabl": 7, "templat": [0, 7], "onc": 7, "reus": 7, "them": [5, 7, 10, 13], "across": 7, "multipl": [0, 1, 2, 7, 8, 9, 12], "open": [1, 7], "sourc": [2, 4, 5, 7], "ar": [0, 1, 2, 4, 5, 6, 7, 8, 10, 12, 13], "we": [4, 5, 6, 7, 8, 10, 13], "miss": 7, "wish": 7, "had": 7, "request": [2, 7, 8], "yourself": [7, 10], "so": [6, 7, 8, 10], "everyon": 7, "benefit": 7, "improv": [7, 10], "pleas": 7, "give": [6, 7, 10, 13], "try": [7, 10], "let": [5, 7], "u": [0, 5, 6, 7, 10], "know": [5, 7], "think": [7, 8], "purpos": [7, 11], "base": [1, 7, 8, 10], "class": [1, 5, 6, 7, 11], "math": [0, 7], "oper": [0, 7], "constant": [0, 7, 11], "convers": 7, "style": [2, 7], "integr": [5, 7, 10], "extend": [2, 7, 10], "test": 7, "exampl": [6, 7, 8, 10, 12], "index": [], "modul": 1, "search": [], "page": [], "thing": [2, 10, 13], "don": 10, "about": [2, 8, 10, 11], "excel": [8, 10], "tabl": [0, 8], "To": [5, 6, 10], "err": 13, "human": 13, "realli": [8, 13], "foul": 13, "up": [1, 2, 8, 13], "comput": [5, 13], "paul": 13, "r": [2, 5, 13], "ehrlich": 13, "background": [2, 7], "efficalc": [0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 13], "wa": [0, 8, 10, 11], "develop": [0, 10, 13], "provid": [0, 1, 8, 10, 13], "robust": 10, "flexibl": [5, 6, 10], "altern": 10, "which": [0, 1, 9, 10], "ha": [10, 11], "remain": 10, "industri": [10, 13], "standard": [10, 11, 12], "decad": [9, 10], "while": 10, "versatil": 10, "us": [0, 1, 5, 6, 8, 10, 11, 12, 13], "mani": [6, 8, 10, 12], "differ": [5, 6, 8, 10], "limit": 10, "its": [0, 1, 2, 10], "abil": 10, "furthermor": 10, "tool": 10, "particularli": 10, "good": [0, 10], "one": [0, 8, 10, 11, 13], "especi": [10, 13], "tailor": 10, "experi": 10, "work": [6, 8, 10, 13], "here": [2, 4, 5, 6, 8, 10, 12], "few": [4, 6, 10], "want": [6, 8, 10, 13], "fix": [8, 10], "If": [0, 1, 2, 5, 10, 11, 12], "write": [8, 10, 13], "ll": [6, 8, 10], "have": [0, 1, 2, 6, 8, 10], "some": [2, 8, 10, 12], "point": [2, 10, 11], "It": [0, 1, 10], "could": [8, 10], "debug": 10, "our": [6, 8, 10, 13], "own": 10, "understand": 10, "colleagu": 10, "share": [8, 10], "modifi": 10, "old": [10, 13], "repurpos": 10, "slightli": 10, "scenario": [8, 10], "when": [0, 5, 6, 8, 10, 13], "hard": 10, "follow": [5, 10, 11], "get": [1, 8, 10, 13], "wai": [5, 6, 8, 10, 13], "Not": [6, 8, 10], "tediou": 10, "annoi": 10, "lead": 10, "cost": 10, "time": [5, 9, 10, 13], "reput": 10, "potenti": 10, "licens": 10, "There": [6, 10], "also": [1, 8, 10], "other": [1, 8, 10, 12, 13], "check": [0, 10], "offici": 10, "peer": 10, "In": [8, 10, 13], "case": [8, 10, 12, 13], "error": [0, 8, 10], "ridden": 10, "host": 10, "implic": 10, "includ": [2, 10], "costli": 10, "delai": 10, "loss": 10, "rapport": 10, "import": [0, 2, 5, 6, 10], "client": 10, "take": [6, 10, 13], "look": [8, 10], "easier": 10, "imagin": 10, "all": [1, 2, 6, 8, 10, 13], "steel": 10, "floor": [9, 10], "beam": [8, 10], "favorit": [8, 10], "go": 10, "through": [8, 10], "input": [0, 1, 2, 3, 8, 10, 12, 13], "uniqu": [0, 10, 13], "dimens": [2, 10], "load": [5, 10], "copi": 10, "next": [10, 13], "But": [6, 8, 10, 13], "realiz": 10, "cell": 10, "mayb": 10, "chang": [0, 8, 10, 13], "type": [0, 1, 2, 5, 8, 10, 11], "avail": [8, 10], "updat": [6, 8, 10], "back": [10, 11, 13], "everi": [8, 10], "same": [6, 8, 10], "exact": 10, "howev": [8, 10], "actual": [6, 9, 10], "function": [1, 2, 10, 11, 13], "Then": [10, 13], "re": [8, 10], "run": [1, 5, 6, 8, 10, 13], "simplifi": [8, 10], "all_beam_configur": 10, "1": [0, 1, 2, 3, 5, 9, 10, 11, 12], "12": [2, 3, 8, 10], "3": [0, 2, 3, 5, 6, 8, 10, 11], "34": 10, "50": [2, 8, 10], "4": [0, 2, 5, 6, 8, 10, 11, 13], "15": [2, 10], "2": [0, 2, 3, 5, 6, 8, 9, 10, 11, 12, 13], "55": 10, "25": [2, 10, 13], "def": [2, 5, 6, 8, 10, 13], "beam_calcul": 10, "name": [0, 1, 10, 11, 12], "span": [8, 10], "ultimate_load": 10, "steel_strength": 10, "insert": 10, "design_all_beam": 10, "configur": 10, "result": [0, 1, 2, 3, 8, 10, 12, 13], "print": [0, 1, 6, 8, 10], "return": [0, 1, 2, 5, 6, 9, 10, 11], "more": [4, 5, 6, 9, 10, 12, 13], "depth": [10, 11], "advanc": [6, 10], "At": 10, "end": [2, 10], "most": [6, 8, 10, 12], "submit": 10, "independ": 10, "author": 10, "noth": [9, 10], "just": [5, 8, 10], "bunch": 10, "number": [0, 10], "mai": [0, 2, 5, 6, 8, 10], "right": [2, 6, 8, 10], "often": 10, "facilit": 10, "proper": 10, "document": [1, 6, 10], "consum": 10, "By": [5, 10], "creation": [8, 10], "highli": 10, "readabl": [8, 10], "enabl": [8, 10], "spend": 10, "do": [6, 8, 10], "thei": [1, 10], "less": [3, 10], "calc": [2, 7, 10, 13], "focu": 10, "might": [8, 10], "veri": 10, "These": [8, 10], "dai": 10, "increas": [10, 13], "whether": [0, 2, 5, 10, 11], "csi": 10, "oapi": 10, "etab": 10, "analysi": 10, "grasshopp": 10, "rhino": 10, "script": 10, "parametr": 10, "model": 10, "without": [0, 1, 10], "lot": [5, 6, 8, 10], "past": 10, "date": 10, "intervent": 10, "nativ": 10, "plug": 10, "directli": [5, 10, 11], "bypass": 10, "friction": 10, "data": [0, 5, 10], "mention": [8, 10], "ideal": 10, "languag": 10, "larg": [10, 13], "amount": 10, "With": [8, 10, 13], "panda": 10, "numpi": 10, "matplotlib": 10, "manag": [2, 10], "set": [0, 1, 2, 8, 10], "save": [1, 5, 8, 10], "headach": 10, "v": [3, 8, 10], "idea": [8, 10], "aim": 10, "shift": 10, "code": [0, 2, 4, 6, 8, 10, 12], "driven": 10, "empow": 10, "user": 10, "reliabl": [10, 13], "transit": 10, "qualiti": 10, "better": [8, 10], "among": 10, "team": 10, "stakehold": 10, "new": [6, 8, 13], "releas": 6, "distribut": 6, "pypi": 6, "via": 6, "pip": 6, "best": [6, 8], "defin": [0, 1, 2, 6, 8, 11, 13], "The": [0, 1, 2, 5, 6, 8, 11, 13], "For": [4, 6, 8, 12], "pythagorean": 6, "theorem": 6, "perimet": [6, 11], "triangl": [6, 8], "titl": [0, 6, 8], "sqrt": [6, 8, 9], "descript": [0, 6, 8], "length": [0, 2, 6, 8], "side": 6, "b": [0, 6, 8, 11], "c": [0, 5, 6, 8, 11], "hypotenus": [6, 8], "p": 6, "produc": [6, 8], "browser": [1, 6], "someth": 6, "simpl": [5, 6, 8, 12], "report_build": [1, 6], "calculationreportbuild": [], "pythagorean_perimet": 6, "builder": [6, 8], "view_report": [1, 6, 8], "nice": 6, "now": [6, 8], "great": [6, 8], "easi": [6, 8, 13], "alwai": 6, "default": [0, 1, 2, 5, 6, 8], "gave": 6, "luckili": 6, "super": 6, "suppli": [5, 6], "overrid": [0, 1, 6], "second": [0, 6], "argument": [6, 8, 9], "new_input": 6, "5": [0, 2, 6, 8, 13], "6": [0, 2, 6, 8, 11], "show": [0, 3, 5, 6, 8], "And": 6, "well": [2, 6, 13], "real": 8, "life": 8, "overal": [6, 11], "pattern": 6, "matter": 6, "deeper": 6, "dive": 6, "api": 6, "see": [0, 5, 6, 8], "option": [0, 1, 2, 5, 6, 8], "perfect": [6, 8], "happi": 6, "equat": [10, 13], "stage": 10, "assumpt": [0, 1], "str": [0, 1, 2, 5, 11], "meant": 0, "clearli": 0, "declar": 0, "form": 0, "basi": 0, "paramet": [0, 1, 2, 5, 9, 11], "text": [0, 2, 12], "describ": 0, "seismic": 0, "provis": 0, "asc": 0, "16": [0, 2], "assum": 0, "variable_nam": 0, "express": [0, 1, 9], "variabl": [0, 1, 3, 8, 9, 12], "float": [0, 2, 8, 11], "int": 0, "none": [0, 1, 2, 5, 8], "refer": 0, "result_check": [0, 1, 13], "bool": [0, 1, 2, 5], "fals": [0, 1, 2, 5], "primari": 0, "object": [0, 1, 3, 5, 8, 9], "symbol": 0, "latex": [0, 9, 12], "format": [0, 1, 2, 12], "e": [0, 1, 2, 3, 5, 9], "physic": 0, "short": 0, "g": [0, 1, 5], "accompani": 0, "indic": [0, 11], "final": [0, 8], "true": [0, 1, 2, 13], "portal": 0, "version": 0, "ft": [0, 3, 8, 11], "1ft": 0, "4ft": 0, "5ft": 0, "estimate_display_length": 0, "calculationlength": 0, "estim": 0, "substitut": 0, "represent": [0, 2], "get_valu": [0, 8], "alia": [0, 9], "valueerror": [0, 11], "zerodivisionerror": 0, "0": [0, 2, 3, 8, 12], "messag": 0, "self": 0, "evalu": 0, "str_result_with_descript": 0, "string": [0, 1, 2], "symbolicexpr": 0, "str_substitut": 0, "valu": [0, 1, 2, 9, 11], "str_symbol": 0, "qualnam": 1, "start": [0, 1, 2, 5], "boundari": 1, "comparison": [0, 1], "compar": 0, "liter": [0, 1, 2], "true_messag": 0, "ok": 0, "false_messag": 0, "explicit": 0, "against": 0, "specifi": [0, 1, 2, 11], "didplai": 0, "depend": 0, "first": 0, "comparis": 0, "desplai": 0, "get_messag": 0, "appropri": 0, "reult": 0, "NO": 0, "is_pass": 0, "comparisonstat": 0, "comparator2": 0, "doe": [0, 1, 2, 9], "given": [0, 1, 2, 8, 13], "rather": 0, "exactli": 0, "annot": 0, "embellish": 0, "els": [0, 8], "logic": 0, "third": 0, "requir": [0, 5], "pass": [0, 9], "head": 0, "head_level": 0, "8": [0, 2], "add": [0, 2, 5, 9], "auto": [0, 2], "increment": 0, "size": [0, 1, 2, 8, 11], "larger": 0, "higher": 0, "level": 0, "than": [0, 8], "lower": 0, "each": [0, 8], "correspond": 0, "posit": [0, 2], "would": 0, "befor": [0, 12], "anoth": 0, "default_valu": [0, 1], "input_typ": 0, "select": 0, "select_opt": 0, "list": [0, 1, 2, 12], "min_valu": 0, "max_valu": 0, "num_step": 0, "element": [0, 12, 13], "overridden": 0, "runner": 0, "html": [0, 1, 5], "applic": 0, "minimum": [0, 2, 9], "allow": [0, 2], "maximum": [0, 9], "interv": 0, "between": [0, 2, 8, 11], "legal": 0, "field": 0, "http": [0, 4, 8, 12], "mozilla": 0, "org": 0, "en": 0, "doc": 0, "web": [0, 1, 11], "attribut": 0, "step": 0, "note": [0, 11], "current": [0, 5], "str_result_with_nam": 0, "textblock": 0, "block": 0, "main": [0, 4], "bolder": 0, "save_calculation_item": 1, "item": 1, "global": 1, "store": [1, 5], "clear_saved_object": 1, "get_override_or_default_valu": 1, "input_nam": 1, "found": [1, 8, 11], "set_input_default_overrid": 1, "default_overrid": 1, "dict": [1, 13], "get_all_calc_object": 1, "clear_all_input_default_overrid": 1, "calculationrunn": [1, 13], "calc_funct": [1, 13], "callabl": 1, "input_v": 1, "execut": 1, "were": 1, "dure": 1, "instanti": 1, "perform": [1, 5], "necessari": [1, 8], "ignor": 1, "dictionari": 1, "empti": 1, "calculate_all_item": 1, "etc": [1, 2, 5], "calculate_result": [1, 13], "filter": 1, "those": [1, 13], "been": [1, 8], "mark": [1, 9], "where": [0, 1, 2, 5, 8, 11], "view": [1, 8], "immedi": 1, "accordingli": 1, "kei": 1, "get_html_as_str": 1, "save_report": 1, "folder_path": 1, "file_nam": 1, "calc_report": 1, "open_on_cr": 1, "locat": [1, 2], "exist": 1, "path": [1, 2, 5], "folder": [1, 4], "filepath": 1, "temporari": 1, "pdf": 1, "calculation_runn": 1, "all_aisc_wide_flange_nam": [], "immut": [], "constructor": [], "tupl": 2, "iter": [], "initi": [], "w44x335": [], "w44x290": [], "w44x262": [], "w40x655": [], "w44x230": [], "w40x503": [], "w40x593": [], "w40x431": [], "w40x397": [], "w40x372": [], "w40x297": [], "w40x362": [], "w40x277": [], "w40x249": [], "w40x215": [], "w40x199": [], "w40x324": [], "w40x392": [], "w40x331": [], "w40x327": [], "w40x294": [], "w40x278": [], "w40x264": [], "w40x235": [], "w40x211": [], "w40x183": [], "w40x149": [], "w40x167": [], "w36x853": [], "w36x802": [], "w36x723": [], "w36x652": [], "w36x529": [], "w36x487": [], "w36x925": [], "w36x441": [], "w36x395": [], "w36x361": [], "w36x330": [], "w36x302": [], "w36x262": [], "w36x282": [], "w36x231": [], "w36x247": [], "w36x256": [], "w36x232": [], "w36x210": [], "w36x194": [], "w36x182": [], "w36x170": [], "w36x160": [], "w36x150": [], "w36x135": [], "w33x387": [], "w33x354": [], "w33x291": [], "w33x318": [], "w33x241": [], "w33x263": [], "w33x221": [], "w33x201": [], "w33x169": [], "w33x152": [], "w33x141": [], "w33x130": [], "w33x118": [], "w30x357": [], "w30x391": [], "w30x326": [], "w30x292": [], "w30x235": [], "w30x261": [], "w30x191": [], "w30x211": [], "w30x173": [], "w30x148": [], "w30x124": [], "w30x132": [], "w30x116": [], "w30x108": [], "w30x99": [], "w30x90": [], "w27x368": [], "w27x539": [], "w27x336": [], "w27x307": [], "w27x281": [], "w27x258": [], "w27x235": [], "w27x194": [], "w27x217": [], "w27x178": [], "w27x161": [], "w27x129": [], "w27x146": [], "w27x114": [], "w27x102": [], "w27x84": [], "w24x370": [], "w27x94": [], "w24x335": [], "w24x306": [], "w24x279": [], "w24x250": [], "w24x229": [], "w24x192": [], "w24x207": [], "w24x176": [], "w24x162": [], "w24x146": [], "w24x117": [], "w24x131": [], "w24x104": [], "w24x94": [], "w24x103": [], "w24x84": [], "w24x68": [], "w24x76": [], "w24x62": [], "w24x55": [], "w21x275": [], "w21x248": [], "w21x201": [], "w21x223": [], "w21x182": [], "w21x166": [], "w21x147": [], "w21x132": [], "w21x111": [], "w21x93": [], "w21x73": [], "w21x122": [], "w21x83": [], "w21x68": [], "w21x62": [], "w21x101": [], "w21x55": [], "w21x48": [], "w21x50": [], "w21x57": [], "w18x311": [], "w21x44": [], "w18x283": [], "w18x258": [], "w18x234": [], "w18x192": [], "w18x211": [], "w18x175": [], "w18x158": [], "w18x143": [], "w18x130": [], "w18x119": [], "w18x106": [], "w18x86": [], "w18x97": [], "w18x76": [], "w18x71": [], "w18x65": [], "w18x55": [], "w18x60": [], "w18x50": [], "w18x46": [], "w18x40": [], "w18x35": [], "w16x100": [], "w16x89": [], "w16x67": [], "w16x77": [], "w16x57": [], "w16x50": [], "w16x40": [], "w16x45": [], "w16x36": [], "w16x31": [], "w16x26": [], "w14x873": [], "w14x808": [], "w14x730": [], "w14x665": [], "w14x550": [], "w14x605": [], "w14x500": [], "w14x455": [], "w14x426": [], "w14x398": [], "w14x370": [], "w14x311": [], "w14x342": [], "w14x283": [], "w14x257": [], "w14x233": [], "w14x211": [], "w14x193": [], "w14x176": [], "w14x159": [], "w14x145": [], "w14x132": [], "w14x120": [], "w14x99": [], "w14x109": [], "w14x90": [], "w14x82": [], "w14x74": [], "w14x68": [], "w14x61": [], "w14x53": [], "w14x48": [], "w14x43": [], "w14x38": [], "w14x34": [], "w14x30": [], "w14x26": [], "w14x22": [], "w12x336": [], "w12x305": [], "w12x279": [], "w12x252": [], "w12x230": [], "w12x210": [], "w12x190": [], "w12x170": [], "w12x152": [], "w12x136": [], "w12x120": [], "w12x106": [], "w12x87": [], "w12x96": [], "w12x79": [], "w12x72": [], "w12x65": [], "w12x58": [], "w12x53": [], "w12x50": [], "w12x45": [], "w12x35": [], "w12x40": [], "w12x30": 8, "w12x26": 8, "w12x22": [], "w12x19": 8, "w12x14": 8, "w12x16": [], "w10x112": [], "w10x100": [], "w10x77": [], "w10x88": [], "w10x68": [], "w10x60": [], "w10x54": [], "w10x45": [], "w10x49": 8, "w10x39": [], "w10x30": [], "w10x33": 8, "w10x26": [], "w10x22": [], "w10x19": 8, "w10x17": [], "w10x15": [], "w10x12": 8, "w8x67": [], "w8x48": [], "w8x58": [], "w8x40": 8, "w8x35": [], "w8x31": [], "w8x28": [], "w8x24": [], "w8x21": 8, "w8x18": [], "w8x15": 8, "w8x13": [], "w8x10": [], "w6x25": [], "w6x20": [], "w6x15": [], "w6x12": [], "w6x16": [], "w6x9": [], "w6x8": [], "w5x19": [], "w5x16": [], "w4x13": [], "m12": [], "5x12": [], "m12x11": [], "5x11": [], "m12x10": [], "m10x9": [], "m10x8": [], "m10x7": [], "m8x6": [], "m6x4": [], "m6x3": [], "m5x18": [], "9": [11, 12], "m4x4": [], "08": [], "m4x6": [], "m4x3": [], "45": [], "s24x106": [], "s24x121": [], "s24x90": [], "s24x100": [], "s24x80": [], "m3x2": [], "s20x96": [], "s20x86": [], "s20x66": [], "s20x75": [], "s18x70": [], "s18x54": [], "s15x50": [], "s15x42": [], "s12x50": [], "s12x40": [], "s12x31": [], "s12x35": [], "s10x35": [], "s10x25": [], "s8x23": [], "s8x18": [], "s6x17": [], "s6x12": [], "s5x10": [], "s4x9": [], "s4x7": [], "s3x7": [], "s3x5": [], "hp18x204": [], "hp18x181": [], "hp18x157": [], "hp18x135": [], "hp16x162": [], "hp16x183": [], "hp16x121": [], "hp16x141": [], "hp16x101": [], "hp16x88": [], "hp14x117": [], "hp14x102": [], "hp14x73": [], "hp14x89": [], "hp12x89": [], "hp12x84": [], "hp12x74": [], "hp12x53": [], "hp10x57": [], "hp10x42": [], "hp12x63": [], "hp8x36": [], "cd": [], "devic": [], "aiscangl": 11, "angl": [2, 9, 11], "aiscchannel": 11, "channel": 11, "aisccircular": 11, "circular": 11, "aiscdoubleangl": 11, "doubleangl": 11, "aiscrectangular": 11, "rectangular": 11, "aiscte": 11, "tee": 11, "aiscwideflang": 11, "wideflang": 11, "get_aisc_angl": 11, "section_s": [8, 11], "fetch": 11, "aisc": 11, "instanc": [3, 9, 11], "popul": 11, "aisc_nam": 11, "rais": [5, 11], "cannot": 11, "get_aisc_channel": 11, "get_aisc_circular": 11, "get_aisc_double_angl": 11, "doubl": 11, "get_aisc_rectangular": 11, "get_aisc_te": 11, "get_aisc_wide_flang": [8, 11], "wide": 11, "flang": 11, "cw": 11, "edi_std_nomenclatur": 11, "iw": 11, "ix": 11, "ii": 11, "iz": 11, "j": 11, "pa": 11, "pa2": 11, "pb": 11, "swa": 11, "swb": 11, "swc": 11, "sx": 11, "sy": 11, "sz": 11, "sza": 11, "szb": 11, "szc": 11, "t_f": 11, "w": [8, 11], "zx": [8, 11], "zy": 11, "b_t": 11, "d": 11, "kde": 11, "kdet": 11, "ro": 11, "rx": [2, 11], "ry": [2, 11], "rz": 11, "tana": 11, "wb": 11, "wc": 11, "x": [2, 11], "xp": 11, "y": [2, 11], "yp": 11, "za": 11, "zb": 11, "zc": 11, "dataclass": 11, "shape": [8, 11], "area": 11, "warp": 11, "edi": 11, "nomenclatur": 11, "moment": 11, "inertia": 11, "axi": [2, 11], "z": 11, "torsion": 11, "minu": [9, 11], "surfac": 11, "singl": [11, 12, 13], "long": [1, 11], "leg": 11, "guid": 11, "19": 11, "elast": 11, "modulu": 11, "addit": [2, 8, 9, 11], "f": 11, "nomin": 11, "weight": [8, 11], "lb": [3, 11], "plastic": 11, "width": [0, 1, 2, 5, 11], "longer": 11, "slender": 11, "ratio": 11, "shorter": 11, "distanc": [2, 11], "outer": 11, "face": 11, "toe": 11, "fillet": 11, "polar": 11, "radiu": [2, 11], "gyrat": 11, "shear": 11, "center": [2, 11], "thick": 11, "tangent": [9, 11], "ax": [5, 11], "graviti": 11, "along": 11, "horizont": [2, 11], "edg": 11, "member": 11, "neutral": 11, "vertic": [2, 11], "h": 11, "pc": 11, "pd": 11, "qf": 11, "qw": 11, "sw1": 11, "sw2": 11, "sw3": 11, "wgi": 11, "wno": 11, "bf": 11, "bfdet": 11, "ddet": 11, "eo": 11, "h_tw": 11, "ho": 11, "rt": 11, "tf": 11, "tfdet": 11, "tw": 11, "twdet": 11, "twdet_2": 11, "cross": 11, "flexur": 11, "static": 11, "abov": [8, 11], "mid": 11, "top": [2, 11], "bottom": [2, 11], "special": 11, "workabl": 11, "gage": 11, "inner": [0, 11], "fasten": 11, "hole": 11, "normal": 11, "centroid": 11, "effect": [11, 13], "d_t": 11, "od": 11, "tde": 11, "tnom": 11, "hss": 11, "round": [2, 9, 11], "pipe": 11, "outsid": 11, "diamet": 11, "wall": 11, "flat": [2, 11], "separ": 11, "bout": 11, "ht": 11, "bin": 11, "b_tde": 11, "h_tde": 11, "typic": 11, "repres": [2, 3, 11], "hollow": 11, "structur": 11, "bf_2tf": 11, "deriv": 11, "m": [11, 12], "cut": 11, "2tf": 11, "half": 11, "k1": 11, "line": [1, 11], "a_bracket": 9, "arg": 9, "bracket": 9, "wrap": [9, 12], "absolut": 9, "arg0": 9, "summat": 9, "arg1": 9, "arglast": 9, "r_bracket": 9, "c_bracket": 9, "curli": [9, 12], "co": 9, "cosinu": 9, "cosh": 9, "hyperbol": 9, "div": 9, "divis": 9, "frac": 9, "div2": 9, "within": [1, 8, 9, 12], "exp": 9, "ln": 9, "natur": [9, 12], "logarithm": 9, "log": 9, "log_arg0": 9, "log10": 9, "log_10": 9, "max": 9, "argn": 9, "min": [9, 12], "sub": 9, "mul": 9, "neg": [2, 9], "negat": 9, "plu": 9, "sum_el": 9, "po": 9, "positivit": 9, "squar": 9, "s_bracket": 9, "sin": 9, "sinu": 9, "sinh": 9, "sqr": 9, "subtract": 9, "tan": 9, "tanh": 9, "zero": 3, "latexexpr_efficalc": 3, "mathemat": [1, 12], "phyical": [], "fundament": [], "overload": [], "numer": [], "throw": [], "except": [], "divsion": [], "consid": [], "4g": [], "3f": [], "unit_format": [], "mathrm": [], "non": [], "ital": [], "insid": [], "mode": [], "expon": [], "scientif": [], "v1": [], "a_": [], "22": [], "mm": [], "v2": [], "876934835": [], "kn": [], "87693": [], "v3": [], "434": [], "cdot": [], "10": 2, "v8": [], "unit_convers": 3, "deg_to_rad": 3, "degre": [2, 3], "radian": 3, "divid": 3, "revers": [2, 3], "180": 3, "deg": 3, "rad": 3, "pi": 3, "180deg": 3, "142": 3, "ft_to_in": 3, "24": 3, "k_to_lb": 3, "kip": 3, "1000": [3, 8], "2000": 3, "reportbuild": [1, 6, 8], "ONE": 3, "two": [2, 3], "call": 8, "possibl": 8, "sometim": 8, "workflow": 8, "compat": 5, "both": 8, "sinc": [], "That": [2, 8], "shown": 8, "easiest": [], "pythagorean_with_param": 8, "default_a": 8, "default_b": 8, "equival": 8, "pythagorean_without_param": 8, "810249675906654": 8, "still": [8, 13], "batch": [5, 8], "collect": 8, "Be": [], "abl": [], "come": [2, 5, 13], "soon": [], "github": [4, 8], "issu": [5, 8], "link": [], "graph": 8, "figur": [2, 7, 8], "matric": 8, "notat": 12, "help": [2, 8, 12], "tip": 12, "charact": 12, "underscor": 12, "_1": 12, "definit": 12, "min_a": 12, "m_2": 12, "must": [2, 12], "brace": 12, "after": [12, 13], "_": 12, "min_": 12, "abc": 12, "m_": 12, "123": 12, "caret": 12, "circumflex": 12, "combin": 12, "www": 12, "overleaf": 12, "com": [4, 8, 12], "learn": 12, "list_of_greek_letters_and_math_symbol": 12, "phi_m": 12, "phi": 12, "squash": 12, "escap": 12, "forward": 12, "slash": 12, "todo": [], "intro": [], "column": 8, "whatev": 8, "highlight": 8, "over": [8, 13], "solut": 8, "As": 8, "bonu": 8, "find": 8, "constraint": 8, "beam_strength": 8, "default_s": 8, "default_span": 8, "default_fi": 8, "fy": 8, "f_y": 8, "ksi": 8, "size_nam": 8, "z_x": 8, "strength": 8, "complex": [8, 13], "m_p": 8, "k": 8, "optim": 8, "moment_strength": 8, "lightest": 8, "strong": 8, "enough": 8, "find_lightest_beam_for_demand": 8, "size_opt": 8, "moment_demand": 8, "lightest_beam": 8, "999999": 8, "strength_info": 8, "size_is_strong_enough": 8, "size_is_lighter_than_best": 8, "certain": 8, "available_beam_sect": 8, "moment_demand_on_beam": 8, "lightest_beam_s": 8, "digest": 8, "summari": 8, "demand": 8, "Or": 8, "extract": 8, "util": 8, "anyth": 8, "extra": 8, "invisible_square_sum": 8, "simpli": 8, "49": 8, "calculate_square_sum": 8, "sum": 8, "sup": [], "begin": [], "align": [], "4pt": [], "therefor": [], "left": 2, "\u00b2": 8, "standalon": 8, "resourc": [5, 8], "appreci": 8, "huge": 8, "ecosystem": 8, "conjunct": 8, "varieti": 8, "everydai": 8, "concept": 8, "youandvern": [4, 8], "feel": 8, "propos": 8, "pull": 8, "ve": 8, "plugin": 8, "elimin": 8, "necess": 8, "intend": 8, "incorpor": 8, "output": 8, "loop": 8, "previous": 8, "get_results_as_dict": [], "toler": 13, "mistak": 13, "slim": 13, "becom": 13, "digit": 13, "opportun": 13, "hundr": 13, "propag": 13, "affect": 13, "part": [2, 13], "unexpect": 13, "accuraci": 13, "evolv": 13, "confid": 13, "expect": 13, "rest": [5, 13], "behav": 13, "world": 13, "softwar": 13, "establish": 13, "disciplin": 13, "appli": [2, 13], "similar": 13, "principl": 13, "straightforward": 13, "assert": 13, "verifi": 13, "test_calc_funct": 13, "calc_function_simpl": 13, "pytest": 13, "prefer": 13, "regularli": 13, "ongo": 13, "continu": 4, "publish": 4, "repo": 4, "tree": 4, "visit": 4, "return_typ": 1, "either": 1, "calculate_results_as_dict": [], "save_fold": 1, "filenam": 1, "open_on_sav": 1, "support": 5, "three": 5, "imag": [2, 5], "caption": [2, 5], "figurefromfil": 5, "lazi": 5, "file_path": 5, "pathlik": 5, "full_width": [0, 2, 5], "tag": 5, "png": 5, "jpg": 5, "svg": [2, 5], "gif": 5, "full": [0, 2, 5], "load_image_data": 5, "my": 5, "pictur": 5, "calc_imag": 5, "popular": 5, "plot": 5, "figurefrommatplotlib": 5, "wrapper": 5, "around": [2, 5], "easili": 5, "pyplot": 5, "plt": 5, "draw_figure_with_matplotlib": 5, "fig": 5, "subplot": 5, "draw": [5, 7], "figurefrombyt": 5, "greater": 5, "throughout": 5, "becaus": 5, "entir": 5, "memori": 5, "figure_byt": 5, "generate_figure_byt": 5, "cloud": 0, "displi": 0, "nor": 0, "pure": 0, "str_result_with_unit": 0, "__str__": 0, "written": 2, "hand": 2, "usual": 2, "graphic": 2, "illustr": 2, "aspect": 2, "geometri": 2, "programmat": 2, "height": 2, "reinforc": 2, "cover": 2, "num_long_bar": 2, "long_bar_radiu": 2, "875": 2, "stirrup_diamet": 2, "375": 2, "stirrup_bend_radiu": 2, "stirrup_hook": 2, "scale": [1, 2], "30": 2, "default_element_stroke_width": 2, "outlin": 2, "beam_outlin": 2, "rectangl": 2, "fill": 2, "bdbdbd": 2, "stirrup": 2, "transvers": 2, "hook": 2, "corner_radiu": 2, "stroke_width": 2, "stroke": 2, "black": 2, "longitudin": 2, "blue": 2, "circl": 2, "long_bar_starting_x": 2, "long_bar_spac": 2, "long_bar_i": 2, "rang": 2, "004aad": 2, "placement": 2, "bar": 2, "placement_bar": 2, "red": 2, "bf211e": 2, "pin": 2, "create_pin_support": 2, "arrow": 2, "create_load_arrow": 2, "marker_end": 2, "arrowmark": 2, "cap": 2, "create_load_cap_lin": 2, "x1": 2, "x2": 2, "100": 2, "20": 2, "60": 2, "40": 2, "80": 2, "diagram": 2, "arrow_count_per_sect": 2, "space": 2, "background_color": 2, "border_width": 2, "border_color": 2, "default_element_fil": 2, "default_element_strok": 2, "canvasel": 2, "ad": 2, "to_svg": 2, "convert": 2, "cx": 2, "cy": 2, "kwarg": 2, "ellips": 2, "y1": 2, "y2": 2, "marker_start": 2, "marker_mid": 2, "to_path_command": 2, "command": 2, "corner": 2, "orient": 2, "circlemark": 2, "get_common_svg_style_el": 2, "context": 2, "canva": 7, "hold": 2, "backdrop": 2, "drawn": 2, "color": 2, "white": 2, "border": 2, "coordin": 2, "param": [], "midpoint": 2, "fit": [1, 2], "match": 2, "connect": 2, "rel": 2, "system": 2, "down": [1, 2], "min_xi": 2, "display_typ": 2, "font_siz": 2, "rotat": 2, "horizontal_bas": 2, "vertical_bas": 2, "middl": 2, "render": 2, "font": [1, 2], "clockwis": 2, "gap": 2, "offset": 2, "text_posit": 2, "text_siz": 2, "being": 2, "parallel": 2, "upward": 2, "downward": 2, "factor": 2, "leader": 2, "marker_x": 2, "marker_i": 2, "text_x": 2, "text_i": 2, "landing_len": 2, "land": 2, "relationship": 2, "elementwithmark": 2, "subclass": 2, "implement": 2, "_get_mark": 2, "get_mark": 2, "header": 0, "stripe": 0, "2d": 0, "row": 0, "act": 0, "inputt": 0, "default_data": 0, "accept": 0, "dynam": 0, "identifi": 0, "long_calc_displai": 1, "longcalcdisplaytyp": 1, "alter": 1, "linebreak": 1, "break": 1, "enumer": 1, "numbered_row": 0, "plain_text_valu": 0, "plain": 0, "letex": 0}, "objects": {"efficalc": [[0, 0, 1, "", "Assumption"], [0, 0, 1, "", "Calculation"], [0, 0, 1, "", "Comparison"], [0, 0, 1, "", "ComparisonStatement"], [5, 0, 1, "", "FigureFromBytes"], [5, 0, 1, "", "FigureFromFile"], [5, 0, 1, "", "FigureFromMatplotlib"], [0, 0, 1, "", "Heading"], [0, 0, 1, "", "Input"], [0, 0, 1, "", "InputTable"], [0, 0, 1, "", "Symbolic"], [0, 0, 1, "", "Table"], [0, 0, 1, "", "TextBlock"], [0, 0, 1, "", "Title"], [9, 2, 1, "", "a_brackets"], [9, 2, 1, "", "absolute"], [9, 2, 1, "", "add"], [9, 2, 1, "", "brackets"], [9, 2, 1, "", "c_brackets"], [1, 2, 1, "", "clear_all_input_default_overrides"], [1, 2, 1, "", "clear_saved_objects"], [3, 3, 0, "-", "constants"], [9, 2, 1, "", "cos"], [9, 2, 1, "", "cosh"], [9, 2, 1, "", "div"], [9, 2, 1, "", "div2"], [9, 2, 1, "", "exp"], [1, 2, 1, "", "get_all_calc_objects"], [1, 2, 1, "", "get_override_or_default_value"], [9, 2, 1, "", "ln"], [9, 2, 1, "", "log"], [9, 2, 1, "", "log10"], [9, 2, 1, "", "maximum"], [9, 2, 1, "", "minimum"], [9, 2, 1, "", "minus"], [9, 2, 1, "", "mul"], [9, 2, 1, "", "neg"], [9, 2, 1, "", "plus"], [9, 2, 1, "", "pos"], [9, 2, 1, "", "power"], [9, 2, 1, "", "r_brackets"], [9, 2, 1, "", "root"], [9, 2, 1, "", "s_brackets"], [1, 2, 1, "", "save_calculation_item"], [1, 2, 1, "", "set_input_default_overrides"], [9, 2, 1, "", "sin"], [9, 2, 1, "", "sinh"], [9, 2, 1, "", "sqr"], [9, 2, 1, "", "sqrt"], [9, 2, 1, "", "sub"], [9, 2, 1, "", "tan"], [9, 2, 1, "", "tanh"], [9, 2, 1, "", "times"], [3, 3, 0, "-", "unit_conversions"]], "efficalc.Calculation": [[0, 1, 1, "", "estimate_display_length"], [0, 1, 1, "", "get_value"], [0, 1, 1, "", "result"], [0, 1, 1, "", "str_result_with_description"], [0, 1, 1, "", "str_substituted"], [0, 1, 1, "", "str_symbolic"]], "efficalc.Comparison": [[0, 1, 1, "", "get_message"], [0, 1, 1, "", "get_value"], [0, 1, 1, "", "is_passing"], [0, 1, 1, "", "result"], [0, 1, 1, "", "str_substituted"], [0, 1, 1, "", "str_symbolic"]], "efficalc.ComparisonStatement": [[0, 1, 1, "", "str_symbolic"]], "efficalc.FigureFromBytes": [[5, 1, 1, "", "load_image_data"]], "efficalc.FigureFromFile": [[5, 1, 1, "", "load_image_data"]], "efficalc.FigureFromMatplotlib": [[5, 1, 1, "", "load_image_data"]], "efficalc.Input": [[0, 1, 1, "", "get_value"], [0, 1, 1, "", "str_result_with_name"]], "efficalc.Symbolic": [[0, 1, 1, "", "estimate_display_length"], [0, 1, 1, "", "get_value"], [0, 1, 1, "", "result"], [0, 1, 1, "", "str_result_with_description"], [0, 1, 1, "", "str_result_with_unit"], [0, 1, 1, "", "str_substituted"], [0, 1, 1, "", "str_symbolic"]], "efficalc.calculation_runner": [[1, 0, 1, "", "CalculationRunner"]], "efficalc.calculation_runner.CalculationRunner": [[1, 1, 1, "", "calculate_all_items"], [1, 1, 1, "", "calculate_results"]], "efficalc.canvas": [[2, 0, 1, "", "ArrowMarker"], [2, 0, 1, "", "Canvas"], [2, 0, 1, "", "CanvasElement"], [2, 0, 1, "", "Circle"], [2, 0, 1, "", "CircleMarker"], [2, 0, 1, "", "Dimension"], [2, 0, 1, "", "ElementWithMarkers"], [2, 0, 1, "", "Ellipse"], [2, 0, 1, "", "Leader"], [2, 0, 1, "", "Line"], [2, 0, 1, "", "Marker"], [2, 0, 1, "", "Polyline"], [2, 0, 1, "", "Rectangle"], [2, 0, 1, "", "Text"]], "efficalc.canvas.ArrowMarker": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Canvas": [[2, 1, 1, "", "add"], [2, 1, 1, "", "to_svg"]], "efficalc.canvas.CanvasElement": [[2, 1, 1, "", "get_common_svg_style_elements"], [2, 1, 1, "", "to_svg"]], "efficalc.canvas.Circle": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.CircleMarker": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Dimension": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.ElementWithMarkers": [[2, 1, 1, "", "get_markers"]], "efficalc.canvas.Ellipse": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Leader": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Line": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Polyline": [[2, 1, 1, "", "to_path_commands"], [2, 1, 1, "", "to_svg"]], "efficalc.canvas.Rectangle": [[2, 1, 1, "", "to_svg"]], "efficalc.canvas.Text": [[2, 1, 1, "", "to_svg"]], "efficalc.constants": [[3, 4, 1, "", "E"], [3, 4, 1, "", "ONE"], [3, 4, 1, "", "PI"], [3, 4, 1, "", "TWO"], [3, 4, 1, "", "ZERO"]], "efficalc.report_builder": [[1, 0, 1, "", "LongCalcDisplayType"], [1, 0, 1, "", "ReportBuilder"]], "efficalc.report_builder.ReportBuilder": [[1, 1, 1, "", "get_html_as_str"], [1, 1, 1, "", "save_report"], [1, 1, 1, "", "view_report"]], "efficalc.sections": [[11, 0, 1, "", "AiscAngle"], [11, 0, 1, "", "AiscChannel"], [11, 0, 1, "", "AiscCircular"], [11, 0, 1, "", "AiscDoubleAngle"], [11, 0, 1, "", "AiscRectangular"], [11, 0, 1, "", "AiscTee"], [11, 0, 1, "", "AiscWideFlange"], [11, 2, 1, "", "get_aisc_angle"], [11, 2, 1, "", "get_aisc_channel"], [11, 2, 1, "", "get_aisc_circular"], [11, 2, 1, "", "get_aisc_double_angle"], [11, 2, 1, "", "get_aisc_rectangular"], [11, 2, 1, "", "get_aisc_tee"], [11, 2, 1, "", "get_aisc_wide_flange"]], "efficalc.unit_conversions": [[3, 4, 1, "", "deg_to_rad"], [3, 4, 1, "", "ft_to_in"], [3, 4, 1, "", "k_to_lb"]]}, "objtypes": {"0": "py:class", "1": "py:method", "2": "py:function", "3": "py:module", "4": "py:data"}, "objnames": {"0": ["py", "class", "Python class"], "1": ["py", "method", "Python method"], "2": ["py", "function", "Python function"], "3": ["py", "module", "Python module"], "4": ["py", "data", "Python data"]}, "titleterms": {"base": [0, 2], "class": [0, 2], "calcul": [1, 6, 8, 13], "helper": [1, 8], "constant": 3, "unit": 3, "convers": 3, "exampl": [2, 4, 5], "get": 7, "start": 7, "efficalc": [7, 8], "api": [2, 5, 7], "document": 7, "more": [7, 8], "indic": [], "tabl": [], "integr": 8, "extend": 8, "math": 9, "oper": 9, "purpos": 10, "section": [2, 11], "properti": 11, "style": 12, "report": [5, 6, 12], "test": 13, "your": 13, "about": [], "todo": [], "add": [], "simpl": 4, "graphic": [], "anim": [], "see": [], "librari": [], "action": [], "background": 10, "find": 10, "mistak": 10, "autom": 10, "scalabl": 10, "format": 10, "submitt": 10, "modern": 10, "workflow": 10, "A": 10, "new": 10, "era": 10, "instal": 6, "first": 6, "function": [6, 8], "view": 6, "run": [], "differ": [], "input": 6, "valu": [6, 8], "quickstart": 6, "chang": 6, "paramet": 8, "return": 8, "calc": [5, 8], "option": [], "1": [], "recommend": [], "2": [], "subscript": 12, "superscript": 12, "greek": 12, "letter": 12, "symbol": 12, "ad": 12, "space": 12, "hundr": 8, "One": 8, "invis": 8, "come": 8, "soon": 8, "why": 13, "matter": 13, "how": 13, "To": 13, "concret": [2, 4], "beam": [2, 4], "neutral": 4, "axi": 4, "advanc": 4, "steel": 4, "moment": 4, "strength": 4, "figur": 5, "from": 5, "file": 5, "doc": [2, 5], "matplotlib": 5, "raw": 5, "byte": 5, "draw": 2, "canva": 2, "cross": 2, "support": 2, "load": 2, "scheme": 2, "element": 2, "line": 2, "polylin": 2, "marker": 2}, "envversion": {"sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 60}, "alltitles": {"Base Classes": [[0, "base-classes"], [2, "base-classes"]], "Calculation Helpers": [[1, "calculation-helpers"], [8, "calculation-helpers"]], "Drawing on a Canvas": [[2, "drawing-on-a-canvas"]], "Examples": [[2, "examples"], [4, "examples"]], "Concrete beam cross-section": [[2, "concrete-beam-cross-section"]], "Beam support and loading scheme": [[2, "beam-support-and-loading-scheme"]], "API docs": [[2, "api-docs"], [5, "api-docs"], [5, "id1"], [5, "id3"]], "Canvas": [[2, "id1"]], "Canvas Elements": [[2, "canvas-elements"]], "Line/Polyline Markers": [[2, "line-polyline-markers"]], "Constants and Unit Conversions": [[3, "constants-and-unit-conversions"]], "Unit Conversions": [[3, "module-efficalc.unit_conversions"]], "Constants": [[3, "id1"]], "Simple": [[4, "simple"]], "Concrete Beam Neutral Axis": [[4, "concrete-beam-neutral-axis"]], "Advanced": [[4, "advanced"]], "Steel Beam Moment Strength": [[4, "steel-beam-moment-strength"]], "Figures in Calc Reports": [[5, "figures-in-calc-reports"]], "Figure from a file": [[5, "figure-from-a-file"]], "Example": [[5, "example"], [5, "id2"], [5, "id4"]], "Figure from a matplotlib figure": [[5, "figure-from-a-matplotlib-figure"]], "Figure from raw bytes": [[5, "figure-from-raw-bytes"]], "Quickstart": [[6, "quickstart"]], "Installation": [[6, "installation"]], "First Calculation Function": [[6, "first-calculation-function"]], "View Reports": [[6, "view-reports"]], "Change Input Values": [[6, "change-input-values"]], "efficalc": [[7, "efficalc"]], "Get Started": [[7, "get-started"]], "API Documentation": [[7, "api-documentation"]], "More": [[7, "more"]], "Integrating and Extending efficalc": [[8, "integrating-and-extending-efficalc"]], "Parameters and Return Values in Calc Functions": [[8, "parameters-and-return-values-in-calc-functions"]], "Hundreds of Calculations with One Function": [[8, "hundreds-of-calculations-with-one-function"]], "Helper Functions": [[8, "helper-functions"]], "Invisible Helpers": [[8, "invisible-helpers"]], "More coming soon": [[8, "more-coming-soon"]], "Math Operations": [[9, "math-operations"]], "Purpose and Background": [[10, "purpose-and-background"]], "Finding mistakes": [[10, "finding-mistakes"]], "Automation and Scalability": [[10, "automation-and-scalability"]], "Formatting and Submittal": [[10, "formatting-and-submittal"]], "Modern Workflows": [[10, "modern-workflows"]], "A New Era": [[10, "a-new-era"]], "Section Properties": [[11, "section-properties"]], "Styling Reports": [[12, "styling-reports"]], "Subscripts": [[12, "subscripts"]], "Superscripts": [[12, "superscripts"]], "Greek Letters and Symbols": [[12, "greek-letters-and-symbols"]], "Adding Spaces": [[12, "adding-spaces"]], "Testing Your Calculations": [[13, "testing-your-calculations"]], "Why Testing Matters": [[13, "why-testing-matters"]], "How To Test Your Calculations": [[13, "how-to-test-your-calculations"]]}, "indexentries": {"assumption (class in efficalc)": [[0, "efficalc.Assumption"]], "calculation (class in efficalc)": [[0, "efficalc.Calculation"]], "comparison (class in efficalc)": [[0, "efficalc.Comparison"]], "comparisonstatement (class in efficalc)": [[0, "efficalc.ComparisonStatement"]], "heading (class in efficalc)": [[0, "efficalc.Heading"]], "input (class in efficalc)": [[0, "efficalc.Input"]], "inputtable (class in efficalc)": [[0, "efficalc.InputTable"]], "symbolic (class in efficalc)": [[0, "efficalc.Symbolic"]], "table (class in efficalc)": [[0, "efficalc.Table"]], "textblock (class in efficalc)": [[0, "efficalc.TextBlock"]], "title (class in efficalc)": [[0, "efficalc.Title"]], "estimate_display_length() (efficalc.calculation method)": [[0, "efficalc.Calculation.estimate_display_length"]], "estimate_display_length() (efficalc.symbolic method)": [[0, "efficalc.Symbolic.estimate_display_length"]], "get_message() (efficalc.comparison method)": [[0, "efficalc.Comparison.get_message"]], "get_value() (efficalc.calculation method)": [[0, "efficalc.Calculation.get_value"]], "get_value() (efficalc.comparison method)": [[0, "efficalc.Comparison.get_value"]], "get_value() (efficalc.input method)": [[0, "efficalc.Input.get_value"]], "get_value() (efficalc.symbolic method)": [[0, "efficalc.Symbolic.get_value"]], "is_passing() (efficalc.comparison method)": [[0, "efficalc.Comparison.is_passing"]], "result() (efficalc.calculation method)": [[0, "efficalc.Calculation.result"]], "result() (efficalc.comparison method)": [[0, "efficalc.Comparison.result"]], "result() (efficalc.symbolic method)": [[0, "efficalc.Symbolic.result"]], "str_result_with_description() (efficalc.calculation method)": [[0, "efficalc.Calculation.str_result_with_description"]], "str_result_with_description() (efficalc.symbolic method)": [[0, "efficalc.Symbolic.str_result_with_description"]], "str_result_with_name() (efficalc.input method)": [[0, "efficalc.Input.str_result_with_name"]], "str_result_with_unit() (efficalc.symbolic method)": [[0, "efficalc.Symbolic.str_result_with_unit"]], "str_substituted() (efficalc.calculation method)": [[0, "efficalc.Calculation.str_substituted"]], "str_substituted() (efficalc.comparison method)": [[0, "efficalc.Comparison.str_substituted"]], "str_substituted() (efficalc.symbolic method)": [[0, "efficalc.Symbolic.str_substituted"]], "str_symbolic() (efficalc.calculation method)": [[0, "efficalc.Calculation.str_symbolic"]], "str_symbolic() (efficalc.comparison method)": [[0, "efficalc.Comparison.str_symbolic"]], "str_symbolic() (efficalc.comparisonstatement method)": [[0, "efficalc.ComparisonStatement.str_symbolic"]], "str_symbolic() (efficalc.symbolic method)": [[0, "efficalc.Symbolic.str_symbolic"]], "calculationrunner (class in efficalc.calculation_runner)": [[1, "efficalc.calculation_runner.CalculationRunner"]], "longcalcdisplaytype (class in efficalc.report_builder)": [[1, "efficalc.report_builder.LongCalcDisplayType"]], "reportbuilder (class in efficalc.report_builder)": [[1, "efficalc.report_builder.ReportBuilder"]], "calculate_all_items() (efficalc.calculation_runner.calculationrunner method)": [[1, "efficalc.calculation_runner.CalculationRunner.calculate_all_items"]], "calculate_results() (efficalc.calculation_runner.calculationrunner method)": [[1, "efficalc.calculation_runner.CalculationRunner.calculate_results"]], "clear_all_input_default_overrides() (in module efficalc)": [[1, "efficalc.clear_all_input_default_overrides"]], "clear_saved_objects() (in module efficalc)": [[1, "efficalc.clear_saved_objects"]], "get_all_calc_objects() (in module efficalc)": [[1, "efficalc.get_all_calc_objects"]], "get_html_as_str() (efficalc.report_builder.reportbuilder method)": [[1, "efficalc.report_builder.ReportBuilder.get_html_as_str"]], "get_override_or_default_value() (in module efficalc)": [[1, "efficalc.get_override_or_default_value"]], "save_calculation_item() (in module efficalc)": [[1, "efficalc.save_calculation_item"]], "save_report() (efficalc.report_builder.reportbuilder method)": [[1, "efficalc.report_builder.ReportBuilder.save_report"]], "set_input_default_overrides() (in module efficalc)": [[1, "efficalc.set_input_default_overrides"]], "view_report() (efficalc.report_builder.reportbuilder method)": [[1, "efficalc.report_builder.ReportBuilder.view_report"]], "arrowmarker (class in efficalc.canvas)": [[2, "efficalc.canvas.ArrowMarker"]], "canvas (class in efficalc.canvas)": [[2, "efficalc.canvas.Canvas"]], "canvaselement (class in efficalc.canvas)": [[2, "efficalc.canvas.CanvasElement"]], "circle (class in efficalc.canvas)": [[2, "efficalc.canvas.Circle"]], "circlemarker (class in efficalc.canvas)": [[2, "efficalc.canvas.CircleMarker"]], "dimension (class in efficalc.canvas)": [[2, "efficalc.canvas.Dimension"]], "elementwithmarkers (class in efficalc.canvas)": [[2, "efficalc.canvas.ElementWithMarkers"]], "ellipse (class in efficalc.canvas)": [[2, "efficalc.canvas.Ellipse"]], "leader (class in efficalc.canvas)": [[2, "efficalc.canvas.Leader"]], "line (class in efficalc.canvas)": [[2, "efficalc.canvas.Line"]], "marker (class in efficalc.canvas)": [[2, "efficalc.canvas.Marker"]], "polyline (class in efficalc.canvas)": [[2, "efficalc.canvas.Polyline"]], "rectangle (class in efficalc.canvas)": [[2, "efficalc.canvas.Rectangle"]], "text (class in efficalc.canvas)": [[2, "efficalc.canvas.Text"]], "add() (efficalc.canvas.canvas method)": [[2, "efficalc.canvas.Canvas.add"]], "get_common_svg_style_elements() (efficalc.canvas.canvaselement method)": [[2, "efficalc.canvas.CanvasElement.get_common_svg_style_elements"]], "get_markers() (efficalc.canvas.elementwithmarkers method)": [[2, "efficalc.canvas.ElementWithMarkers.get_markers"]], "to_path_commands() (efficalc.canvas.polyline method)": [[2, "efficalc.canvas.Polyline.to_path_commands"]], "to_svg() (efficalc.canvas.arrowmarker method)": [[2, "efficalc.canvas.ArrowMarker.to_svg"]], "to_svg() (efficalc.canvas.canvas method)": [[2, "efficalc.canvas.Canvas.to_svg"]], "to_svg() (efficalc.canvas.canvaselement method)": [[2, "efficalc.canvas.CanvasElement.to_svg"]], "to_svg() (efficalc.canvas.circle method)": [[2, "efficalc.canvas.Circle.to_svg"]], "to_svg() (efficalc.canvas.circlemarker method)": [[2, "efficalc.canvas.CircleMarker.to_svg"]], "to_svg() (efficalc.canvas.dimension method)": [[2, "efficalc.canvas.Dimension.to_svg"]], "to_svg() (efficalc.canvas.ellipse method)": [[2, "efficalc.canvas.Ellipse.to_svg"]], "to_svg() (efficalc.canvas.leader method)": [[2, "efficalc.canvas.Leader.to_svg"]], "to_svg() (efficalc.canvas.line method)": [[2, "efficalc.canvas.Line.to_svg"]], "to_svg() (efficalc.canvas.polyline method)": [[2, "efficalc.canvas.Polyline.to_svg"]], "to_svg() (efficalc.canvas.rectangle method)": [[2, "efficalc.canvas.Rectangle.to_svg"]], "to_svg() (efficalc.canvas.text method)": [[2, "efficalc.canvas.Text.to_svg"]], "e (in module efficalc.constants)": [[3, "efficalc.constants.E"]], "one (in module efficalc.constants)": [[3, "efficalc.constants.ONE"]], "pi (in module efficalc.constants)": [[3, "efficalc.constants.PI"]], "two (in module efficalc.constants)": [[3, "efficalc.constants.TWO"]], "zero (in module efficalc.constants)": [[3, "efficalc.constants.ZERO"]], "deg_to_rad (in module efficalc.unit_conversions)": [[3, "efficalc.unit_conversions.deg_to_rad"]], "efficalc.constants": [[3, "module-efficalc.constants"]], "efficalc.unit_conversions": [[3, "module-efficalc.unit_conversions"]], "ft_to_in (in module efficalc.unit_conversions)": [[3, "efficalc.unit_conversions.ft_to_in"]], "k_to_lb (in module efficalc.unit_conversions)": [[3, "efficalc.unit_conversions.k_to_lb"]], "module": [[3, "module-efficalc.constants"], [3, "module-efficalc.unit_conversions"]], "figurefrombytes (class in efficalc)": [[5, "efficalc.FigureFromBytes"]], "figurefromfile (class in efficalc)": [[5, "efficalc.FigureFromFile"]], "figurefrommatplotlib (class in efficalc)": [[5, "efficalc.FigureFromMatplotlib"]], "load_image_data() (efficalc.figurefrombytes method)": [[5, "efficalc.FigureFromBytes.load_image_data"]], "load_image_data() (efficalc.figurefromfile method)": [[5, "efficalc.FigureFromFile.load_image_data"]], "load_image_data() (efficalc.figurefrommatplotlib method)": [[5, "efficalc.FigureFromMatplotlib.load_image_data"]], "a_brackets() (in module efficalc)": [[9, "efficalc.a_brackets"]], "absolute() (in module efficalc)": [[9, "efficalc.absolute"]], "add() (in module efficalc)": [[9, "efficalc.add"]], "brackets() (in module efficalc)": [[9, "efficalc.brackets"]], "c_brackets() (in module efficalc)": [[9, "efficalc.c_brackets"]], "cos() (in module efficalc)": [[9, "efficalc.cos"]], "cosh() (in module efficalc)": [[9, "efficalc.cosh"]], "div() (in module efficalc)": [[9, "efficalc.div"]], "div2() (in module efficalc)": [[9, "efficalc.div2"]], "exp() (in module efficalc)": [[9, "efficalc.exp"]], "ln() (in module efficalc)": [[9, "efficalc.ln"]], "log() (in module efficalc)": [[9, "efficalc.log"]], "log10() (in module efficalc)": [[9, "efficalc.log10"]], "maximum() (in module efficalc)": [[9, "efficalc.maximum"]], "minimum() (in module efficalc)": [[9, "efficalc.minimum"]], "minus() (in module efficalc)": [[9, "efficalc.minus"]], "mul() (in module efficalc)": [[9, "efficalc.mul"]], "neg() (in module efficalc)": [[9, "efficalc.neg"]], "plus() (in module efficalc)": [[9, "efficalc.plus"]], "pos() (in module efficalc)": [[9, "efficalc.pos"]], "power() (in module efficalc)": [[9, "efficalc.power"]], "r_brackets() (in module efficalc)": [[9, "efficalc.r_brackets"]], "root() (in module efficalc)": [[9, "efficalc.root"]], "s_brackets() (in module efficalc)": [[9, "efficalc.s_brackets"]], "sin() (in module efficalc)": [[9, "efficalc.sin"]], "sinh() (in module efficalc)": [[9, "efficalc.sinh"]], "sqr() (in module efficalc)": [[9, "efficalc.sqr"]], "sqrt() (in module efficalc)": [[9, "efficalc.sqrt"]], "sub() (in module efficalc)": [[9, "efficalc.sub"]], "tan() (in module efficalc)": [[9, "efficalc.tan"]], "tanh() (in module efficalc)": [[9, "efficalc.tanh"]], "times() (in module efficalc)": [[9, "efficalc.times"]], "aiscangle (class in efficalc.sections)": [[11, "efficalc.sections.AiscAngle"]], "aiscchannel (class in efficalc.sections)": [[11, "efficalc.sections.AiscChannel"]], "aisccircular (class in efficalc.sections)": [[11, "efficalc.sections.AiscCircular"]], "aiscdoubleangle (class in efficalc.sections)": [[11, "efficalc.sections.AiscDoubleAngle"]], "aiscrectangular (class in efficalc.sections)": [[11, "efficalc.sections.AiscRectangular"]], "aisctee (class in efficalc.sections)": [[11, "efficalc.sections.AiscTee"]], "aiscwideflange (class in efficalc.sections)": [[11, "efficalc.sections.AiscWideFlange"]], "get_aisc_angle() (in module efficalc.sections)": [[11, "efficalc.sections.get_aisc_angle"]], "get_aisc_channel() (in module efficalc.sections)": [[11, "efficalc.sections.get_aisc_channel"]], "get_aisc_circular() (in module efficalc.sections)": [[11, "efficalc.sections.get_aisc_circular"]], "get_aisc_double_angle() (in module efficalc.sections)": [[11, "efficalc.sections.get_aisc_double_angle"]], "get_aisc_rectangular() (in module efficalc.sections)": [[11, "efficalc.sections.get_aisc_rectangular"]], "get_aisc_tee() (in module efficalc.sections)": [[11, "efficalc.sections.get_aisc_tee"]], "get_aisc_wide_flange() (in module efficalc.sections)": [[11, "efficalc.sections.get_aisc_wide_flange"]]}}) \ No newline at end of file diff --git a/docs/section_properties.html b/docs/section_properties.html index 6447342..16e347f 100644 --- a/docs/section_properties.html +++ b/docs/section_properties.html @@ -6,7 +6,7 @@ - Section Properties - efficalc 1.2.0 documentation + Section Properties - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    - + diff --git a/docs/styling.html b/docs/styling.html index 88d3da6..5d04807 100644 --- a/docs/styling.html +++ b/docs/styling.html @@ -6,7 +6,7 @@ - Styling Reports - efficalc 1.2.0 documentation + Styling Reports - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    diff --git a/docs/testing.html b/docs/testing.html index 6c351c6..6c914f4 100644 --- a/docs/testing.html +++ b/docs/testing.html @@ -6,7 +6,7 @@ - Testing Your Calculations - efficalc 1.2.0 documentation + Testing Your Calculations - efficalc 1.2.6 documentation @@ -126,7 +126,7 @@
    -
    efficalc 1.2.0 documentation
    +
    efficalc 1.2.6 documentation
    @@ -152,7 +152,7 @@
    - efficalc 1.2.0 documentation + efficalc 1.2.6 documentation @@ -169,6 +169,7 @@
    diff --git a/docs_src/base_classes.rst b/docs_src/base_classes.rst index 406713a..fe33d4b 100644 --- a/docs_src/base_classes.rst +++ b/docs_src/base_classes.rst @@ -31,6 +31,14 @@ Base Classes :members: +.. autoclass:: efficalc.Table + :members: + + +.. autoclass:: efficalc.InputTable + :members: + + .. autoclass:: efficalc.TextBlock :members: diff --git a/docs_src/calculation_helpers.rst b/docs_src/calculation_helpers.rst index 9b95518..7d664c6 100644 --- a/docs_src/calculation_helpers.rst +++ b/docs_src/calculation_helpers.rst @@ -12,6 +12,10 @@ Calculation Helpers :members: +.. autoclass:: efficalc.report_builder.LongCalcDisplayType + :members: + + .. autofunction:: efficalc.save_calculation_item diff --git a/docs_src/canvas.rst b/docs_src/canvas.rst index 3db5562..0b339d4 100644 --- a/docs_src/canvas.rst +++ b/docs_src/canvas.rst @@ -175,6 +175,15 @@ Canvas Elements .. autoclass:: efficalc.canvas.Rectangle :members: +.. autoclass:: efficalc.canvas.Text + :members: + +.. autoclass:: efficalc.canvas.Dimension + :members: + +.. autoclass:: efficalc.canvas.Leader + :members: + Line/Polyline Markers ********************* @@ -193,3 +202,6 @@ Base Classes .. autoclass:: efficalc.canvas.Marker :members: +.. autoclass:: efficalc.canvas.ElementWithMarkers + :members: + diff --git a/docs_src/conf.py b/docs_src/conf.py index c7bedd9..59b022b 100644 --- a/docs_src/conf.py +++ b/docs_src/conf.py @@ -14,7 +14,7 @@ project = "efficalc" copyright = "2024, Andrew Young" author = "Andrew Young" -release = "1.2.0" +release = "1.2.6" html_favicon = "_static/favicon.ico" # -- General configuration --------------------------------------------------- diff --git a/efficalc/__init__.py b/efficalc/__init__.py index 1ff0275..40aaf8f 100644 --- a/efficalc/__init__.py +++ b/efficalc/__init__.py @@ -55,6 +55,7 @@ set_input_default_overrides, ) from .base_definitions.symbolic import Symbolic +from .base_definitions.table import InputTable, Table from .base_definitions.text_block import TextBlock from .base_definitions.title import Title from .constants import ONE, PI, TWO, ZERO, E diff --git a/efficalc/base_definitions/calculation.py b/efficalc/base_definitions/calculation.py index 262cbbc..88465f3 100644 --- a/efficalc/base_definitions/calculation.py +++ b/efficalc/base_definitions/calculation.py @@ -129,6 +129,13 @@ def _get_symbolic_string(self): latex_code = self.operation.str_symbolic() return LatexNodes2Text().latex_to_text(latex_code) + def _get_result_string(self): + try: + latex_code = self.operation.str_result() + return LatexNodes2Text().latex_to_text(latex_code) + except (ValueError, ZeroDivisionError): + return "" + def estimate_display_length(self) -> CalculationLength: """Returns the estimated length of the LaTex formatted operation based on its symbolic and substituted representations. @@ -145,8 +152,15 @@ def estimate_display_length(self) -> CalculationLength: CalculationLength.SHORT """ if ( - self._get_symbolic_string().strip() - == self._get_substituted_string().strip() + ( + self._get_symbolic_string().strip() + == self._get_substituted_string().strip() + ) + or ( + self._get_substituted_string().strip() + == self._get_result_string().strip() + ) + or (self._get_symbolic_string().strip() == f"{self.result()}") ): return CalculationLength.NUMBER elif self._estimate_operation_length() <= 50: diff --git a/efficalc/base_definitions/comparison.py b/efficalc/base_definitions/comparison.py index 5d25068..abee779 100644 --- a/efficalc/base_definitions/comparison.py +++ b/efficalc/base_definitions/comparison.py @@ -28,7 +28,7 @@ class Comparison(CalculationItem): :type reference: str, optional :param result_check: This is used to indicate any :class:`.Comparison` that should be checked as a final result of your calculation template. When set to True, this :class:`.Comparison` will be displayed in the "Results" - section of your design portal in the cloud version of efficalc, defaults to False + section of your design portal in the cloud version of efficalc, defaults to True :type result_check: bool, optional .. code-block:: python diff --git a/efficalc/base_definitions/figure.py b/efficalc/base_definitions/figure.py index 82b4307..da3217f 100644 --- a/efficalc/base_definitions/figure.py +++ b/efficalc/base_definitions/figure.py @@ -1,9 +1,12 @@ import base64 from io import BytesIO from os import PathLike +from typing import Literal from .shared import CalculationItem, save_calculation_item +FigureDisplayType = Literal["report-only", "report-input", "report-result"] + class FigureBase(CalculationItem): """A base class for displaying figures in a calculation report. @@ -12,11 +15,19 @@ class FigureBase(CalculationItem): :type caption: str, optional :param full_width: Whether the figure should be full width, defaults to False :type full_width: bool, optional + :param display_type: Where the figure should be displayed, defaults to "report-only" + :type display_type: FigureDisplayType, optional """ - def __init__(self, caption: str = None, full_width: bool = False): + def __init__( + self, + caption: str = None, + full_width: bool = False, + display_type: FigureDisplayType = "report-only", + ): self.caption = caption self.full_width = full_width + self.display_type = display_type self._figure_bytes = None save_calculation_item(self) diff --git a/efficalc/base_definitions/input.py b/efficalc/base_definitions/input.py index 34546d9..8986529 100644 --- a/efficalc/base_definitions/input.py +++ b/efficalc/base_definitions/input.py @@ -48,6 +48,10 @@ class Input(Variable, CalculationItem): cloud version of efficalc; see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/step, defaults to "any" :type num_step: float, int, or None, optional + :param plain_text_value: Set to True if the input value should be displayed as plain text rather than formatted as + LeTex math, defaults to False + :type plain_text_value: bool, optional + .. code-block:: python @@ -69,6 +73,7 @@ def __init__( min_value: int | float = None, max_value: int | float = None, num_step: int | float | str = "any", + plain_text_value: bool = False, ): override_or_default_value = get_override_or_default_value( variable_name, default_value @@ -85,6 +90,7 @@ def __init__( self.max_value = max_value self.input_type = input_type self.select_options = select_options + self.plain_text_value = plain_text_value save_calculation_item(self) def str_result_with_name(self): @@ -120,6 +126,9 @@ def _get_display_type(self) -> InputDisplayType: def __str__(self): if self._get_display_type() != InputDisplayType.NUMBER: - return rf"\mathrm{{{self.name}}} = \mathrm{{{self.value}}} \ {self.unit}" + text_value = ( + rf"\text{{{self.value}}}" if self.plain_text_value else self.value + ) + return rf"\mathrm{{{self.name}}} = \mathrm{{{text_value}}} \ {self.unit}" else: return super().__str__() diff --git a/efficalc/base_definitions/table.py b/efficalc/base_definitions/table.py new file mode 100644 index 0000000..2334359 --- /dev/null +++ b/efficalc/base_definitions/table.py @@ -0,0 +1,93 @@ +from typing import Any, List, Optional + +from .shared import ( + CalculationItem, + get_override_or_default_value, + save_calculation_item, +) + + +class Table(CalculationItem): + """An object to display a table of data. + + :param data: The data in the table. If this is an input table, the data will act as default data to be overridden + by the calculation runner. + :type data: List[List[Any]] (a 2d list where each inner list is a row in the table) + :param headers: The headers for the table, defaults to None + :type headers: List[Any], optional + :param title: The table title, defaults to None + :type title: str, optional + :param striped: Whether the table should be striped, defaults to False + :type striped: bool, optional + :param full_width: Whether the table should be full width, defaults to False + :type full_width: bool, optional + :param result_check: This is used to indicate any :class:`.Table` that should be checked as a final result + of your calculation template. When set to True, this :class:`.Table` will be displayed in the "Results" + section of your design portal in the cloud version of efficalc, defaults to False + :type result_check: bool, optional + :param numbered_rows: Whether to add row numbers (starting at 1) to each row, defaults to False + :type numbered_rows: bool, optional + + """ + + def __init__( + self, + data: List[List[Any]], + headers: Optional[List[any]] = None, + title: Optional[str] = None, + striped: bool = False, + full_width: bool = False, + result_check: bool = False, + numbered_rows: bool = False, + ) -> None: + self.data = data + self.headers = headers + self.title = title + self.striped = striped + self.full_width = full_width + self.result_check = result_check + self.numbered_rows = numbered_rows + save_calculation_item(self) + + def __str__(self) -> str: + return f"{self.title}\n{self.headers}\n{self.data}" + + +class InputTable(Table): + """A table that can be used to accept dynamic input data with the calculation runner and cloud version of efficalc. + + :param default_data: The default data for the table. This will be overridden when explicit calculation inputs are + provided to the calculation runner or in the design portal on the cloud version of efficalc + :type default_data: List[List[Any]] + :param headers: The headers for the table. This will be used as the unique identifier for the input table + :type headers: List[Any] + :param title: The table title, defaults to None + :type title: str, optional + :param striped: Whether the table should be striped, defaults to False + :type striped: bool, optional + :param full_width: Whether the table should be full width, defaults to True + :type full_width: bool, optional + :param numbered_rows: Whether to add row numbers (starting at 1) to each row, defaults to False + :type numbered_rows: bool, optional + """ + + def __init__( + self, + default_data: List[List[Any]], + headers: List[any], + title: Optional[str] = None, + striped: bool = False, + full_width: bool = False, + numbered_rows: bool = False, + ) -> None: + super().__init__( + default_data, headers, title, striped, full_width, False, numbered_rows + ) + self.data = get_override_or_default_value(self.identifier, default_data) + + @property + def identifier(self): + return "input_table-" + "-".join(self.headers) + + def __str__(self) -> str: + return f"{self.title}\n{self.headers}\n{self.data}" diff --git a/efficalc/canvas/__init__.py b/efficalc/canvas/__init__.py index 08e75d2..6e68f4d 100644 --- a/efficalc/canvas/__init__.py +++ b/efficalc/canvas/__init__.py @@ -4,9 +4,13 @@ CanvasElement, Circle, CircleMarker, + Dimension, + ElementWithMarkers, Ellipse, + Leader, Line, Marker, Polyline, Rectangle, + Text, ) diff --git a/efficalc/canvas/canvas.py b/efficalc/canvas/canvas.py index 5ac437e..3e1f66d 100644 --- a/efficalc/canvas/canvas.py +++ b/efficalc/canvas/canvas.py @@ -1,8 +1,10 @@ import copy -from typing import List, Set +from typing import List, Literal, Set from efficalc import CalculationItem, save_calculation_item -from efficalc.canvas.canvas_elements import CanvasElement, Line, Marker, Polyline +from efficalc.canvas.canvas_elements import CanvasElement, ElementWithMarkers, Marker + +CanvasDisplayType = Literal["report-only", "report-input", "report-result"] class Canvas(CalculationItem): @@ -12,12 +14,14 @@ class Canvas(CalculationItem): :param width: Width of the canvas drawing space. :param height: Height of the canvas drawing space. - :param background_color: Background color of the canvas, defaults to "white". - :param border_width: Width of the border around the canvas, defaults to 0. - :param border_color: Color of the border around the canvas, defaults to "black". + :param min_xy: Minimum x and y values of the canvas drawing space, defaults to (0, 0). :param caption: Caption for the canvas, defaults to None. :param centered: Whether to center the canvas, defaults to True. :param full_width: Whether to make the canvas full width, defaults to False. + :param display_type: Where the canvas should be displayed, defaults to "report-only" + :param background_color: Background color of the canvas, defaults to "white". + :param border_width: Width of the border around the canvas, defaults to 0. + :param border_color: Color of the border around the canvas, defaults to "black". :param scale: Scale the display size of the canvas, defaults to 1. :param default_element_fill: Default fill color for elements, defaults to "none". :param default_element_stroke: Default stroke color for elements, defaults to "black". @@ -28,9 +32,11 @@ def __init__( self, width: float, height: float, + min_xy: tuple[float, float] = (0, 0), caption: str = None, centered: bool = True, full_width: bool = False, + display_type: CanvasDisplayType = "report-only", background_color: str = None, border_width: float = None, border_color: str = None, @@ -42,10 +48,12 @@ def __init__( self.width = width self.height = height + self.min_xy = min_xy self.elements: List[CanvasElement] = [] self.caption = caption self.centered = centered self.full_width = full_width + self.display_type = display_type self.background_color = background_color self.border_width = border_width self.border_color = border_color @@ -55,49 +63,15 @@ def __init__( self.default_stroke_width = default_element_stroke_width save_calculation_item(self) - @classmethod - def _apply_context_marker_styles( - cls, element: CanvasElement, marker: Marker - ) -> None: - """ - Applies context fill and stroke to marker if they are set to "context-fill" or "context-stroke". - :param element: The element to get context fill and stroke from. - :param marker: The marker to apply context fill and stroke to if context styles are requested. - - .. note:: - This method modifies the marker in place. - """ - if marker.fill == "context-fill": - marker.fill = element.fill - if marker.stroke == "context-stroke": - marker.stroke = element.stroke - @classmethod def _process_markers(cls, element: CanvasElement) -> list[Marker]: """ - Apply context marker properties to markers if they are set to "context-fill" or "context-stroke" and return all - markers for the element. + Get formatted markers from an element if it includes them. - :param element: The element to get context fill and stroke from. - - .. note:: - This method modifies the markers in place. + :param element: The element to get markers from. """ - markers = [] - - def process_marker(marker: Marker): - cls._apply_context_marker_styles(element, marker) - markers.append(marker) - - if isinstance(element, Line | Polyline): - if element.marker_start is not None: - process_marker(element.marker_start) - if element.marker_end is not None: - process_marker(element.marker_end) - if element.marker_mid is not None: - process_marker(element.marker_mid) - return markers + return element.get_markers() if isinstance(element, ElementWithMarkers) else [] def _set_defaults(self, element: CanvasElement): """ @@ -168,8 +142,9 @@ def to_svg(self) -> str: ) elements_svg = "\n".join([element.to_svg() for element in processed_elements]) + min_x, min_y = self.min_xy return ( - f'\n' + f'\n' f"{self._generate_marker_defs(included_markers)} {elements_svg}" f"\n " ) diff --git a/efficalc/canvas/canvas_elements.py b/efficalc/canvas/canvas_elements.py index 5bff4df..77e00b3 100644 --- a/efficalc/canvas/canvas_elements.py +++ b/efficalc/canvas/canvas_elements.py @@ -82,6 +82,40 @@ def __repr__(self): return self.id +class ElementWithMarkers(CanvasElement): + """ + Base class for elements with markers. Subclasses must implement the _get_markers method. + """ + + def _get_markers(self) -> List[Marker]: + """ + Returns a list of all unformatted markers contained by the element. + """ + raise NotImplementedError("Must be implemented by subclasses") + + def get_markers(self) -> list[Marker]: + """ + Returns a list of all markers contained by the element with formatting applied. + """ + markers = self._get_markers() + for marker in markers: + self._apply_context_marker_styles(marker) + return markers + + def _apply_context_marker_styles(self, marker: Marker) -> Marker: + """ + Applies context fill and stroke to marker if they are set to "context-fill" or "context-stroke". + :param marker: The marker to apply context fill and stroke to if context styles are requested. + + """ + if marker.fill == "context-fill": + marker.fill = self.fill + if marker.stroke == "context-stroke": + marker.stroke = self.stroke + + return marker + + class Rectangle(CanvasElement): """ Represents a rectangle. @@ -162,7 +196,7 @@ def to_svg(self) -> str: return f'' -class Line(CanvasElement): +class Line(ElementWithMarkers): """ Represents a line. @@ -192,7 +226,14 @@ def __init__( self.y2 = y2 self.marker_start = marker_start self.marker_end = marker_end - self.marker_mid = None + + def _get_markers(self) -> List[Marker]: + markers = [] + if self.marker_start: + markers.append(self.marker_start) + if self.marker_end: + markers.append(self.marker_end) + return markers def to_svg(self) -> str: starting_marker = ( @@ -204,7 +245,7 @@ def to_svg(self) -> str: return f'' -class Polyline(CanvasElement): +class Polyline(ElementWithMarkers): """ Represents a polyline with optional corner rounding. @@ -353,10 +394,350 @@ def _get_marker_assignments(self): assignments += f' marker-mid="url(#{self.marker_mid.id})"' return assignments + def _get_markers(self) -> List[Marker]: + markers = [] + if self.marker_start: + markers.append(self.marker_start) + if self.marker_end: + markers.append(self.marker_end) + if self.marker_mid: + markers.append(self.marker_mid) + return markers + def to_svg(self) -> str: return f'' +class Text(CanvasElement): + """ + Represents a text element in the canvas. + + :param text: The text content to be rendered. + :param x: The x-coordinate of the text base point. + :param y: The y-coordinate of the text base point. + :param font_size: The font size of the text. + :param rotate: The rotation angle of the text about the base point (clockwise in degrees). + :param horizontal_base: The horizontal base point location of the text. + :param vertical_base: The vertical base point location of the text. + :param fill: The fill color of the text. + :param stroke: The stroke color of the text. + :param stroke_width: The stroke width of the text. + """ + + def __init__( + self, + text: str, + x: float, + y: float, + font_size: float | str = "auto", + rotate: float = 0, + horizontal_base: Literal["start", "center", "end"] = "start", + vertical_base: Literal["auto", "top", "middle", "bottom"] = "auto", + fill: str = "black", + stroke: str = "none", + stroke_width: float = 0, + ): + self.text = text + self.x = x + self.y = y + self.font_size = font_size + self.rotate = rotate + self.horizontal_base = horizontal_base + self.vertical_base = vertical_base + + super().__init__(fill=fill, stroke=stroke, stroke_width=stroke_width) + + def _get_horizontal_base_prop(self) -> str: + default_text_anchor = "start" + horizontal_base_to_text_anchor = { + "start": "start", + "center": "middle", + "end": "end", + } + if ( + self.horizontal_base == default_text_anchor + or self.horizontal_base not in horizontal_base_to_text_anchor + ): + return "" + return f' text-anchor="{horizontal_base_to_text_anchor[self.horizontal_base]}"' + + def _get_vertical_base_prop(self) -> str: + default_dominant_baseline = "auto" + vertical_base_to_dominant_baseline = { + "auto": "auto", + "top": "hanging", + "middle": "middle", + "bottom": "text-top", + } + if ( + self.vertical_base == default_dominant_baseline + or self.vertical_base not in vertical_base_to_dominant_baseline + ): + return "" + return f' dominant-baseline="{vertical_base_to_dominant_baseline[self.vertical_base]}"' + + def to_svg(self) -> str: + rotate = f' transform="translate({self.x}, {self.y}) rotate({self.rotate})"' + font_size = "" if self.font_size == "auto" else f' font-size="{self.font_size}"' + return ( + f'{self.text}" + ) + + +class Dimension(ElementWithMarkers): + """ + Represents a dimension line between two points. + + :param x1: X coordinate of the start point. + :param y1: Y coordinate of the start point. + :param x2: X coordinate of the end point. + :param y2: Y coordinate of the end point. + :param text: The text to display as the dimension, defaults to the length of the dimension line. + :param gap: The gap between the points being dimensioned and the start of the extension lines, defaults to 2. + :param offset: Offset distance from the parallel dimension line to the dimensioned points. Positive offset will + result in the dimension extending upward, negative offset will result in the dimension extending downward. + Defaults to 10. + :param unit: The unit of the dimension, defaults to None. + :param text_position: The position of the text relative to the dimension line. Defaults to 'top'. + :param text_size: Scaling factor for text size. Defaults to 1. + :param kwargs: Additional properties such as fill, stroke, and stroke_width. + """ + + def __init__( + self, + x1: float, + y1: float, + x2: float, + y2: float, + text: Optional[str] = None, + gap: float = 0, + offset: float = 10, + unit: Optional[str] = None, + text_position: Literal["top", "bottom"] = "top", + text_size: float = 1.0, + **kwargs, + ): + super().__init__(**kwargs) + normalize_direction = x1 > x2 + self.x1 = x2 if normalize_direction else x1 + self.y1 = y2 if normalize_direction else y1 + self.x2 = x1 if normalize_direction else x2 + self.y2 = y1 if normalize_direction else y2 + self.text = text + self.gap = gap + self.offset = offset + self.unit = unit + self.text_position = text_position + self.text_size = text_size + self.additional_props = kwargs + + def _calc_length(self) -> float: + return math.sqrt((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) + + @staticmethod + def _calc_unit_vector( + x1: float, y1: float, x2: float, y2: float + ) -> tuple[float, float]: + dx, dy = x2 - x1, y2 - y1 + length = math.sqrt(dx**2 + dy**2) + return dx / length, dy / length + + def _calc_perpendicular_vector(self) -> tuple[float, float]: + # Calculate direction vector for dimension line + ux, uy = self._calc_unit_vector(self.x1, self.y1, self.x2, self.y2) + + # Return perpendicular vector for offset + return -uy, ux + + @property + def _stroke_width(self) -> float: + return self.stroke_width or 1 + + @property + def _scaled_offset(self) -> float: + return self.offset * -1 + + def _get_dimension_line(self) -> Line: + offset = self._scaled_offset + perp_ux, perp_uy = self._calc_perpendicular_vector() + marker_start = ArrowMarker(orientation="auto-start-reverse", base="point") + marker_end = ArrowMarker(base="point") + # Calculate dimension line points + d1_x, d1_y = self.x1 + offset * perp_ux, self.y1 + offset * perp_uy + d2_x, d2_y = self.x2 + offset * perp_ux, self.y2 + offset * perp_uy + return Line( + x1=d1_x, + y1=d1_y, + x2=d2_x, + y2=d2_y, + marker_start=marker_start, + marker_end=marker_end, + stroke=self.stroke, + stroke_width=self._stroke_width, + ) + + def _get_markers(self) -> List[Marker]: + return self._get_dimension_line().get_markers() + + def to_svg(self) -> str: + perp_ux, perp_uy = self._calc_perpendicular_vector() + stroke_width = self._stroke_width + offset = self._scaled_offset + offset_sign = -1 if offset < 0 else 1 + + # Calculate points for extension lines + gap = self.gap * offset_sign + ex1_x_start, ex1_y_start = self.x1 + gap * perp_ux, self.y1 + gap * perp_uy + ex2_x_start, ex2_y_start = self.x2 + gap * perp_ux, self.y2 + gap * perp_uy + + ext_len = offset + stroke_width * 4 * offset_sign + ex1_x_end, ex1_y_end = self.x1 + ext_len * perp_ux, self.y1 + ext_len * perp_uy + ex2_x_end, ex2_y_end = self.x2 + ext_len * perp_ux, self.y2 + ext_len * perp_uy + + dimension_line = self._get_dimension_line() + + # Calculate position for dimension text + text_offset_sign = -1 if self.text_position == "top" else 1 + text_gap = 2 * stroke_width * text_offset_sign + dim_x1, dim_y1 = dimension_line.x1, dimension_line.y1 + dim_x2, dim_y2 = dimension_line.x2, dimension_line.y2 + text_x = (dim_x1 + dim_x2) / 2 + text_gap * perp_ux + text_y = (dim_y1 + dim_y2) / 2 + text_gap * perp_uy + + # Calculate rotation angle for dimension text + ux, uy = self._calc_unit_vector(dim_x1, dim_y1, dim_x2, dim_y2) + angle_rad = math.atan2(uy, ux) + text_rotation = math.degrees(angle_rad) + + # Use provided text or default to length + dimension_text = ( + self.text if self.text is not None else f"{self._calc_length():.2f}" + ) + + # Create SVG elements with scaling + extension_line1 = Line( + x1=ex1_x_start, + y1=ex1_y_start, + x2=ex1_x_end, + y2=ex1_y_end, + stroke=self.stroke, + stroke_width=stroke_width, + ) + extension_line2 = Line( + x1=ex2_x_start, + y1=ex2_y_start, + x2=ex2_x_end, + y2=ex2_y_end, + stroke=self.stroke, + stroke_width=stroke_width, + ) + text_element = Text( + text=( + f"{dimension_text}{self.unit}" + if self.unit is not None + else dimension_text + ), + x=text_x, + y=text_y, + rotate=text_rotation, + font_size=stroke_width * 7 * self.text_size, + horizontal_base="center", + vertical_base="bottom" if self.text_position == "top" else "top", + fill=self.stroke, + ) + + # Combine SVG elements into one group + svg_elements = ( + extension_line1.to_svg() + + extension_line2.to_svg() + + dimension_line.to_svg() + + text_element.to_svg() + ) + return f"{svg_elements}" + + +class Leader(ElementWithMarkers): + """ + Represents a leader text with a polyline leader. + + :param marker_x: X coordinate of the marker point. + :param marker_y: Y coordinate of the marker point. + :param text_x: X coordinate of the text position. + :param text_y: Y coordinate of the text position. + :param text: The text content to display. + :param marker: The marker at the end of the leader, defaults to None. + :param landing_len: The length of the landing line. + :param direction: Relative position of the text in relationship to the landing line ('right' or 'left'). + :param text_size: Scaling factor for text size. Defaults to 1. + :param kwargs: Additional properties such as fill, stroke, and stroke_width. + """ + + def __init__( + self, + marker_x: float, + marker_y: float, + text_x: float, + text_y: float, + text: str, + marker: Marker = None, + landing_len: float = 5, + direction: Literal["right", "left"] = "right", + text_size: float = 1.0, + **kwargs, + ): + super().__init__(**kwargs) + self.marker_x = marker_x + self.marker_y = marker_y + self.text_x = text_x + self.text_y = text_y + self.text = text + self.marker = marker + self.landing_distance = landing_len + self.direction = direction + self.text_size = text_size + self.additional_props = kwargs + + @property + def _stroke_width(self) -> float: + return self.stroke_width or 1 + + def _get_leader_line(self) -> Polyline: + direction = -1 if self.direction == "right" else 1 + leader_gap = direction * 2 * self._stroke_width + points = [ + (self.text_x + leader_gap, self.text_y), + (self.text_x + direction * self.landing_distance + leader_gap, self.text_y), + (self.marker_x, self.marker_y), + ] + return Polyline( + points=points, + marker_end=self.marker, + stroke=self.stroke, + stroke_width=self._stroke_width, + fill="none", + ) + + def _get_markers(self) -> List[Marker]: + return self._get_leader_line().get_markers() + + def to_svg(self) -> str: + leader_line = self._get_leader_line() + + text_element = Text( + text=self.text, + x=self.text_x, + y=self.text_y, + font_size=7 * self._stroke_width * self.text_size, + horizontal_base="start" if self.direction == "right" else "end", + vertical_base="middle", + fill=self.stroke, + ) + + svg_elements = leader_line.to_svg() + text_element.to_svg() + return f"{svg_elements}" + + MarkerOrientation = Union[Literal["auto", "auto-start-reverse"], float] @@ -370,23 +751,37 @@ class ArrowMarker(Marker): """ def __init__( - self, reverse: bool = False, orientation: MarkerOrientation = "auto", **kwargs + self, + reverse: bool = False, + orientation: MarkerOrientation = "auto", + base: Literal["point", "center", "flat"] = "center", + **kwargs, ): self.reversed = reverse self.orientation = orientation + self.base_point = base super().__init__(**kwargs) @property def id(self) -> str: - return f"{self.__class__.__name__}-{self.fill}-{self.stroke}-{self.stroke_width}-{self.size}-{self.reversed}-{self.orientation}" + return f"{self.__class__.__name__}-{self.fill}-{self.stroke}-{self.stroke_width}-{self.size}-{self.reversed}-{self.orientation}-{self.base_point}" + + @property + def marker_size(self) -> float: + return self.size * 4 def to_svg(self) -> str: - marker_size = self.size * 4 + marker_size = self.marker_size stroke_width = self.stroke_width if self.stroke_width is not None else 0 view_size = marker_size + 2 * stroke_width min_position = stroke_width max_position = marker_size + min_position half_position = view_size / 2 + ref_x = ( + half_position + if self.base_point == "center" + else 0 if self.base_point == "flat" else view_size + ) path = ( f"M {min_position} {half_position} L {max_position} {min_position} L {max_position} {max_position} z" @@ -395,7 +790,7 @@ def to_svg(self) -> str: ) return ( f'' ) diff --git a/efficalc/generate_html.py b/efficalc/generate_html.py index 7b67cf7..74b4cdf 100644 --- a/efficalc/generate_html.py +++ b/efficalc/generate_html.py @@ -10,6 +10,7 @@ Heading, Input, Symbolic, + Table, TextBlock, Title, ) @@ -84,6 +85,9 @@ def _generate_html_for_calc_item(calculation_item, header_numbers: list[int]) -> elif isinstance(calculation_item, Symbolic): return _generate_symbolic_html(calculation_item) + elif isinstance(calculation_item, Table): + return _generate_result_table_html(calculation_item) + elif isinstance(calculation_item, TextBlock): return _wrap_with_reference( _wrap_p(_esc(calculation_item.text)), _esc(calculation_item.reference) @@ -210,6 +214,32 @@ def _generate_comparison_html(item: Comparison) -> str: return _wrap_div(comp_html, class_name=CALC_ITEM_WRAPPER_CLASS) +def _generate_result_table_html(item: Table) -> str: + full_width = " width:100%;" if item.full_width else "" + striped_class = ' class="striped"' if item.striped else "" + style = f' style="margin:auto;{full_width}"' + table_html = f"" + if item.title: + table_html += f"{item.title}" + if item.headers: + table_html += "" + if item.numbered_rows: + table_html += "" + for header in item.headers: + table_html += f"" + table_html += "" + table_html += "" + for row_num, row in enumerate(item.data): + table_html += "" + if item.numbered_rows: + table_html += f"" + for cell in row: + table_html += f"" + table_html += "" + table_html += "
    {header}
    {row_num+1}{cell}
    " + return _wrap_div(table_html, CALC_MARGIN, class_name=CALC_ITEM_WRAPPER_CLASS) + + def _generate_comparison_statement_html(item: ComparisonStatement) -> str: comp_html = "" description = _esc(item.description) @@ -275,11 +305,15 @@ def _generate_canvas_html(item: Canvas) -> str: def _wrap_math(content: str) -> str: - return rf"\[ {content} \]" + return rf"\[ {_escape_tex_characters(content)} \]" def _wrap_math_inline(content: str) -> str: - return rf"\( {content} \)" + return rf"\( {_escape_tex_characters(content)} \)" + + +def _escape_tex_characters(content: str) -> str: + return content.replace("#", "\\#") def _wrap_with_reference(primary_content: str, reference: str | None) -> str: @@ -288,8 +322,10 @@ def _wrap_with_reference(primary_content: str, reference: str | None) -> str: "display: flex; flex-direction: column; justify-content: center;" ) + ref_only_styles = "margin-left:1rem;text-wrap:no-wrap;" + return _wrap_div( - f"{_wrap_div(primary_content, vertical_justified)} {_wrap_div(ref, vertical_justified)}", + f"{_wrap_div(primary_content, vertical_justified)} {_wrap_div(ref, vertical_justified+ref_only_styles)}", f"display:flex; flex-direction:row; justify-content:space-between; {CALC_MARGIN}", ) diff --git a/efficalc/report_builder.py b/efficalc/report_builder.py index 7f0df52..0902ca5 100644 --- a/efficalc/report_builder.py +++ b/efficalc/report_builder.py @@ -1,12 +1,24 @@ import os import tempfile import webbrowser +from enum import Enum from typing import Callable from efficalc.calculation_runner import CalculationRunner from efficalc.generate_html import generate_html_for_calc_items +class LongCalcDisplayType(Enum): + """An enumeration for controlling how to display long mathematical expressions in reports. + + :cvar SCALE: Scale the expression display and font size down to fit within the report width + :cvar LINEBREAK: Break the expression into multiple lines to fit within the report width + """ + + SCALE = "scale" + LINEBREAK = "linebreak" + + class ReportBuilder(object): """ A helper class to run calculation functions and generate reports based on the calculations. @@ -23,17 +35,21 @@ class ReportBuilder(object): be the names of the input objects in the calculation function and values should be the desired values for the input. :type input_vals: dict[str, any], optional + :param long_calc_display: How long expressions should be altered to fit within the calculation report width. + This can be either SCALE for scaling down the display size of the expression or LINEBREAK to break the + expression into multiple lines. defaults to SCALE + :type long_calc_display: LongCalcDisplayType, optional """ def __init__( self, calc_function: Callable, input_vals: dict[str, any] = None, + long_calc_display: LongCalcDisplayType = LongCalcDisplayType.SCALE, ): - self.calc_function: Callable = calc_function - self.input_default_overrides: dict[str, any] = ( - input_vals if input_vals is not None else {} - ) + self.calc_function = calc_function + self.input_default_overrides = input_vals if input_vals is not None else {} + self.long_calc_display = long_calc_display def view_report(self) -> str: """Runs the calculation function with the provided input overrides and opens up the calculation report in the @@ -109,7 +125,7 @@ def __generate_report_html(self): all_items = calculation.calculate_all_items() report_items_html = generate_html_for_calc_items(all_items) - return _wrap_report_in_html_page(report_items_html) + return _wrap_report_in_html_page(report_items_html, self.long_calc_display) def _create_folder_if_not_exists(folder_path): @@ -126,32 +142,56 @@ def _create_temp_html_file(html_content): return temp_file.name -def _wrap_report_in_html_page(content: str) -> str: - start = """ +def _wrap_report_in_html_page( + content: str, long_calc_display: LongCalcDisplayType +) -> str: + + start = f""" + + src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/4.0.0-beta.7/tex-mml-chtml.min.js"> diff --git a/examples/conc_col_pmm/README.md b/examples/conc_col_pmm/README.md new file mode 100644 index 0000000..0d24760 --- /dev/null +++ b/examples/conc_col_pmm/README.md @@ -0,0 +1,281 @@ +# Scope +This program supports the analysis of rectangular, symmetric, perimeter-reinforced concrete columns in accordance with ACI 318-19 and using the exact capacity method. + +# Run Guide + +## Getting Started +The `conc_col_pmm` folder is meant to be a standalone design tool. To get set up for running it on your local machine, follow these steps: + +1. Download or copy the entire `conc_col_pmm` folder into your desired working directory. +2. Open a command line shell in the working directory. Make sure you have python installed and accessible. +3. Initialize a virtual environment (e.g. `python -m venv .venv && .venv/Scripts/activate.ps1 ` ) +4. Install requirements (e.g. `pip install -r requirements.txt`) +5. Run tests to make sure setup is complete: `pytest conc_col_pmm/tests` + +## Designing a column +The `conc_col_pmm/tests/visual_tests` folder has examples for running various parts of the concrete column tool including + +* `visual_test_calculation_report.py` for viewing a complete calculation report +* `visual_test_pmm.py` for viewing a 3D PMM plot +* `visual_test_point_plotter.py` for viewing 2D PM plots for specific load cases + +These example files can be run and viewed with `python -m conc_col_pmm.tests.visual_tests.`. For example: +`python -m conc_col_pmm.tests.visual_tests.visual_test_calculation_report` + + +# Program Features +- **PMM Diagrams**: Creates 3D Axial-Moment-Moment interaction diagrams. +- **PM Diagrams**: For each load case entered, creates a 2D PM interaction diagram including the location of the load case on the PM axes relative to the capacity curve. +- **Demand-to-Capacity Ratios (DCRs)**: Calculates the PM Vector DCR (defined below) for each load case entered. +- **Calculation Reports**: Generates a calc report for the column showing the full capacity calculation for all selected load cases, including cross-section diagrams, code references, and DCR calculations. + +# Definitions and Theory +- **Axial force ($P$)**: The force in the column parallel to its length, where a positive value indicates compression. +- **Ultimate axial force ($P_u$)**: The axial force applied to a column (typically calculated using a factored load combination). +- **Nominal axial capacity ($P_n$)**: The calculated axial load capacity, without a safety factor. +- **Moments ($M_x$, $M_y$)**: These measure moments about axes indicated by their subscripts, i.e., positive $M_{x}$ indicates compression in the +y region and positive $M_{y}$ indicates compression in the +x region. +- **Ultimate moments ($M_{ux}$, $M_{uy}$)**: The moments applied to a column (typically calculated using a factored load combination). +- **Nominal moment capacity ($M_{nx}$, $M_{ny}$)**: The calculated moment capacities, without a safety factor. +- **Load case**: A given combination of axial load and moments ($P_{u}$, $M_{ux}$, $M_{uy}$). +- **Eccentricity ($e_x$, $e_y$)**: This is the location that a load would have to have on the x or y axis to produce an equivalent moment to the moment at the given load case with the same axial load. + +$$ +e_x=\frac{M_x}{P} +$$ + +$$ +e_y=\frac{M_y}{P} +$$ +- **Eccentricity angle (λ)**: The angle taken clockwise from the positive y-axis to a vector from the column centroid to the location where an equivalent axial load could be applied that would be equivalent to the combination of $P$, $M_{x}$, and $M_{y}$ for a given load case. + +$$ +\lambda = \arctan\left(\frac{e_x}{e_y}\right) = \arctan\left(\frac{M_{ny}}{M_{nx}}\right) +$$ +- **Neutral axis**: The line across the column section on which strain is assumed to be 0. +- **Neutral axis angle (θ)**: The angle from the positive x-axis to the neutral axis. Counter-clockwise is taken as positive, and the region of compression is on the side of the neutral axis further counter-clockwise than θ. The neutral axis angle can be understood in the graphic below, which shows the neutral axis as a dashed line and the equivalent compression zone as gray. + + +- **Neutral axis depth (c)**: The distance from the neutral axis to a parallel line passing through the column corner in maximum compression. +- **PMM diagram**: The 3D plot of the PMM surface, which describes the capacity of a given column in $P$, $M_x$, $M_y$ (any load case whose plot falls inside the PMM surface is within capacity, and any load case falling outside the PMM surface exceeds capacity). +- **PM diagram**: The 2D plot of a cut of the PMM surface at a given eccentricity angle (λ). This diagram is useful for visualizing the capacity of the column relative to demand for a given load case. +- **PM Vector DCR**: The ratio of the length of the demand vector (in PMM space) to the length of a parallel vector beginning at the origin and continuing until it reaches the capacity surface. +- **Axial to Moment Angle (α)**: This is a custom-defined variable used in this program that describes the height of a load case above the $M_x-M_y$ plane. It is a proxy for the inverse of the eccentricity resultant. + +$$ +\alpha = \arctan\left(\frac{P_n}{\sqrt{M_{nx}^2+M_{ny}^2}}\right) +$$ + +It seems intuitive that the neutral axis should be parallel to the axis of the resultant moment, which would mean the relation λ=-θ would hold. However, this holds only in special cases, which means determining the neutral axis angle required to produce a given eccentricity is not straightforward. For more information see [1]. + +# How It Works +- **PMM Diagrams**: The PMM diagrams created by this program compose a mesh of capacity points which are evenly spaced in both the vertical load ($P$) direction and in their angles about the origin (λ). To achieve this even spacing, it is necessary to find points on the PMM surface that have a given combination of λ and $P$. $P$ tends to increase as the neutral axis depth increases and λ tends to increase as θ decreases, but neither of the output variables (λ and $P$) can be calculated in closed form. This means that the domain of the two input variables (θ and c) must be searched to find the target point on the PMM diagram. The search algorithm is [below](#search-algorithm). +- **PM Diagrams**: This program uses sets of control points interpolated from the points on the PMM diagram to create PM diagrams for each load case. +- **DCRs**: These are calculated by finding a point on the PMM surface such that a vector from the origin to that point is parallel to a vector from the origin to the PMM point for the given load case and then taking a ratio of the lengths of the two vectors. To find the capacity point, a search of the PMM surface is again required, but in this case, the target variables are λ and α. Searching this domain is equivalent to searching in the domain of spherical coordinates. Unlike in the case of the point search for the PMM diagram, this search is performed on the fully-factored PMM surface (including the plateau). The search algorithm is [below](#search-algorithm). +- **Calculation Reports**: The program uses the efficalc library to generate calc reports. + +# Search Algorithm +## Summary +In both search problems, there are two input and two output variables and the target output variables are known. The algorithm is given a starting point, and it proceeds as follows: +1. Calculate first derivatives—the full 4x4 Jacobian—at the current input point using finite differences. +2. Use the derivatives as linear approximations for the two output variables as functions of the two variables and solve for the input point at which both output variables are expected to equal their target values. +3. Move to the input point calculated in (2) and repeat from (1). + +## Update Method +For simplicity, assume that the two input variables are x and y and the two output functions are f and g, where f and g have been shifted so that the target outputs are f=0 and g=0. Then the Jacobian is as follows: + +$$ +J = +\begin{bmatrix} +\frac{\partial f}{\partial x} & \frac{\partial f}{\partial y} \\ +\frac{\partial g}{\partial x} & \frac{\partial g}{\partial y} +\end{bmatrix} +$$ + +Then the linear approximator can be written: + +$$ +\begin{bmatrix} +f_1 \\ +g_1 +\end{bmatrix}= +\begin{bmatrix} +\frac{\partial f}{\partial x} & \frac{\partial f}{\partial y} \\ +\frac{\partial g}{\partial x} & \frac{\partial g}{\partial y} +\end{bmatrix} +\begin{bmatrix} +x \\ +y +\end{bmatrix}+ +\begin{bmatrix} +f_0 \\ +g_0 +\end{bmatrix} +$$ + +Since the target is f=0, g=0, this is equivalent to the following system: + +$$ +-\begin{bmatrix} +f_0 \\ +g_0 +\end{bmatrix}= +\begin{bmatrix} +\frac{\partial f}{\partial x} & \frac{\partial f}{\partial y} \\ +\frac{\partial g}{\partial x} & \frac{\partial g}{\partial y} +\end{bmatrix} +\begin{bmatrix} +x \\ +y +\end{bmatrix} +$$ + +This equation yields solutions for x and y: + +$$ +x = -\frac{\frac{\partial g}{\partial y} f_0 - \frac{\partial f}{\partial y} g_0}{\frac{\partial f}{\partial x} \frac{\partial g}{\partial y} - \frac{\partial f}{\partial y} \frac{\partial g}{\partial x}} +$$ + +$$ +y = -\frac{-\frac{\partial g}{\partial x} f_0 + \frac{\partial f}{\partial x} g_0}{\frac{\partial f}{\partial x} \frac{\partial g}{\partial y} - \frac{\partial f}{\partial y} \frac{\partial g}{\partial x}} +$$ + +This program uses the formulas above to calculate the next guess of both inputs at each iteration. The situation can be visualized with the following plot of the projected zero-contours of the two functions f and g, where both zero-contours are estimated by calculating the gradient at the current point. The solution to the linear system above effectively finds the intersection between the two zero-contours, which is the target point. + + + + +# Summary of Program Structure by Package and Module + +## 1. `calc_document` +Contains modules linked to the `efficalc` package which are used for generating the calc report. + +### 1.1. `add_col_inputs_document` +Adds the column inputs and assumptions to the calc report. + +### 1.2. `col_inputs` +Collects information from the user about the column and loads. Calls `full_calc_document` to begin creating the calc report. + +### 1.3. `dcr_calc_runner` +Calculates DCRs for all load cases and adds calculations to the calc report for applicable load cases. + +### 1.4. `document_wrapper` +Creates the calc report. It calls `col_inputs`, and from there, all information is added to the calc report. + +### 1.5. `full_calc_document` +Receives a column and load cases, then runs the calculations for the column capacity and DCRs for all load cases. + +### 1.6. `results_summary` +Creates a table showing the DCRs for all load cases and shows the max DCR. + +### 1.7. `show_dcr_calc` +Adds the calculation of a particular DCR to the calc report. Optionally called depending on whether the user selects a given load case to be shown. + +### 1.8. `try_axis_document` +Adds the calculations for the reaction of a column to bending on a given neutral axis to the calc report. + +### 1.9. `plotting` (sub-package) +Contains plotting functions. + +#### 1.9.1. `get_capacity` +Accepts parameters like the quarter PMM mesh and a loading point, then returns a list of resultant moment and axial points which form the PM diagram at the angle of the given load point. + +#### 1.9.2. `get_pmm_data` +Creates an instance of the PMM class containing the data for a given column's PMM diagram. + +#### 1.9.3. `PMM` +Defines a dataclass for storing the data needed for plotting the PMM diagram. + +#### 1.9.4. `pmm_mesh` +Creates the mesh for the PMM diagram by iterating over the range of axial loads and λ. + +#### 1.9.5. `pmm_plotter_plotly` +Creates a Plotly figure for the column’s PMM diagram. + +#### 1.9.6. `point_plotter` +Creates a Matplotlib figure showing the PM diagram for a given load case, along with the point for the given load case. + +#### 1.9.7. `pure_mx_my_plotter` +Adds PM diagrams to the calc report showing cuts of the PMM diagram that align with both the x and y axes (indicating moment purely about the x and y axes). + +## 2. `col` +Contains functions related to the definition of a given concrete column. + +### 2.1. `assign_max_min` +Performs calculations for the maximum and minimum axial capacity of a given column, adds them to the calc report, and assigns the calculated values to the given `Column` object. + +### 2.2. `column` +Contains the class defining a `Column` object, including various properties. + +### 2.3. `col_canvas` (sub-package) +Contains plotting functions. + +#### 2.3.1. `draw_column_comp_zone` +Draws the cross-section of the column with the full equivalent compression zone labeled. + +#### 2.3.2. `draw_column_with dimensions` +Draws the cross-section of the column with dimensions and rebar information shown. + +#### 2.3.3. `draw_column_with_triangle` +Draws the cross-section of the column with a triangular compression area labeled. + +#### 2.3.4. `draw_plain_column` +Draws the cross-section of the column on an `efficalc` Canvas, including rebar. + +## 3. `constants` +Contains constants used by other packages. + +### 3.1. `rebar_data` +Stores data on rebar properties. + +## 4. `pmm_search` +Contains functions used for searching the PMM surface for target points. + +### 4.1. `ecc_search` +Used to find a PMM point for DCR calculation. + +#### 4.1.1. `change_ecc` +Calculates the next iteration point in the search. + +#### 4.1.2. `get_dcr_ecc` +Runs the point search for finding the capacity point for a given load point and calculates the DCR. Optionally adds the DCR calculation to the calc report. + +#### 4.1.3. `get_error_ecc` +Calculates the distance of the current iteration point from the target point in normalized λ-α space. + +#### 4.1.4. `limit_comp_ecc` +Calculates the results for a given iteration point by running `try_axis`. Also limits the axial load to a range in which nonzero derivatives can be calculated. + +#### 4.1.5. `point_search_ecc` +Controls the convergence of the search for a point for a DCR calculation. + +### 4.2. `load_search` +Used to find a PMM point for the PMM diagram. + +#### 4.2.1. `bisect_load` +Searches for a point aligned with the Mx or My axes by changing only the neutral axis depth while holding λ constant. + +#### 4.2.2. `change_load` +Calculates the next iteration point in the search. + +#### 4.2.3. `get_error_load` +Calculates the distance of the current iteration point from the target point in normalized λ-P space. + +#### 4.2.4. `limit_comp_load` +Calculates the results for a given iteration point by running `try_axis`. Also limits the axial load to a range in which nonzero derivatives can be calculated. + +#### 4.2.5. `point_search_load` +Controls the convergence of the search for a point for creating a PMM diagram. + +#### 4.2.6. `starting_pts` +Selects starting points for the `bisect_load` algorithm. Both starting points are near the initial guess point. + +## 5. `struct_analysis` + +### 5.1. `triangles` +Contains functions for calculating the area and centroid of a triangle, for use in `try_axis`. + +### 5.2. `try_axis` +Calculates the reaction of a column (axial load and moments) due to bending on a given neutral axis angle and depth. + +# Reference +[1] Design of Concrete Structures, 15th ed. Darwin, Dolan, and Nilson. McGraw, 2016. diff --git a/examples/conc_col_pmm/__init__.py b/examples/conc_col_pmm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/calc_document/__init__.py b/examples/conc_col_pmm/calc_document/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/calc_document/calculation.py b/examples/conc_col_pmm/calc_document/calculation.py new file mode 100644 index 0000000..6119fa9 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/calculation.py @@ -0,0 +1,158 @@ +from efficalc import Assumption, Calculation, Heading, Input, InputTable, Title + +from ..col.column import Column +from ..constants.concrete_data import MAX_CONCRETE_STRAIN +from ..constants.rebar_data import REBAR_SIZES, REBAR_STRENGTHS, STEEL_E, rebar_area +from ..pmm_search.load_combo import LoadCombination, is_yes +from .column_inputs import ColumnInputs +from .full_calc_document import calculation as full_calc +from .plotting.get_pmm_data import get_pmm_data + + +# this function accepts inputs from the user and passes them to "full_calc_document" +def calculation( + default_loads: list[list] = [[3000, -200, 100, "yes"]], col=ColumnInputs() +): + Title("Concrete Column Biaxial Bending Calculation Report") + + Heading("Column Inputs") + w = Input("w", col.w, "in", description="Column section width (x dimension)") + h = Input("h", col.h, "in", description="Column section height (y dimension)") + + # zero spaces + bar_size = Input( + "", + col.bar_size, + "", + description="Longitudinal rebar size (Imperial)", + input_type="select", + select_options=REBAR_SIZES, + ) + + # one space + bar_cover = Input(" ", col.bar_cover, "in", description="Longitudinal rebar cover") + + # TODO: should clear cover account for the shear reinforcement? + cover_options = [ + "Center", + "Edge", + ] + + # 2 spaces + cover_type = Input( + " ", + default_value=cover_options[0] if col.cover_to_center else cover_options[1], + unit="", + description="Cover is to bar center or bar edge (clear cover)", + input_type="select", + select_options=cover_options, + ) + transverse_options = ["Spiral", "Tied"] + # 3 spaces + transverse_type = Input( + " ", + default_value=( + transverse_options[0] if col.spiral_reinf else transverse_options[1] + ), + unit="", + description="Transverse reinforcement type", + input_type="select", + select_options=transverse_options, + ) + + # 4 spaces + bars_x = Input( + "n_x", + col.bars_x, + "", + description="Number of bars on the top/bottom edges", + num_step=1, + ) + + # 5 spaces + bars_y = Input( + "n_y", + col.bars_y, + "", + description="Number of bars on the left/right edges", + num_step=1, + ) + fc = Input("f^{\prime}_c", 8000, "psi", description="Concrete strength") + + fy = Input( + "f_y", + col.fy, + "ksi", + description="Steel strength", + input_type="select", + select_options=REBAR_STRENGTHS, + ) + + headers = [ + "Pu (kip)", + "Mux (kip-ft)", + "Muy (kip-ft)", + "Show in Calc Report (yes/no)", + ] + load_table = InputTable( + default_loads, headers, "Load Cases", False, False, numbered_rows=True + ) + + load_combos = [ + LoadCombination(idx + 1, load[0], load[1], load[2], is_yes(load[3])) + for idx, load in enumerate(load_table.data) + ] + + # above were the efficalc Inputs from the user, and below, some additional inputs and assumptions + # are created and added to the calc report + + A_b = Calculation( + "A_{\\mathrm{bar}}", + rebar_area(bar_size.get_value()), + "in^2", + description="Area of one bar", + ) + + E_s = Calculation( + "E_s", STEEL_E, "ksi", "Steel modulus of elasticity", "ACI 318-19 20.2.2.2" + ) + e_c = Calculation( + "\\epsilon_u", + MAX_CONCRETE_STRAIN, + "", + "Concrete strain at f'c", + "ACI 318-19 22.2.2.1", + ) + + Heading("Assumptions") + Assumption("ACI 318-19 controls the design") + Assumption("Reinforcement is non-prestressed") + Assumption( + "Lap splices of longitudinal reinforcement are in accordance with ACI 318-19 Table 10.7.5.2.2" + ) + Assumption( + "Strain in concrete and reinforcement is proportional to distance from the neutral axis, per ACI 318-19 22.2.1.2 " + ) + + cover_to_center = cover_type == "Center" + spiral_reinf = transverse_type == "Spiral" + + column = Column( + w, + h, + bar_size, + bar_cover, + bars_x, + bars_y, + fc, + fy, + cover_to_center, + spiral_reinf, + A_b, + E_s, + e_c, + ) + + axial_limits = full_calc(column, load_combos) + + return get_pmm_data(column, 36, 12, load_combos, axial_limits) diff --git a/examples/conc_col_pmm/calc_document/column_capacities.py b/examples/conc_col_pmm/calc_document/column_capacities.py new file mode 100644 index 0000000..f4d5084 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/column_capacities.py @@ -0,0 +1,12 @@ +import dataclasses + +from latexexpr_efficalc import Variable + +from efficalc import Calculation + + +@dataclasses.dataclass +class ColumnCapacities: + Mx: Calculation | Variable + My: Calculation | Variable + P: Calculation | Variable diff --git a/examples/conc_col_pmm/calc_document/column_inputs.py b/examples/conc_col_pmm/calc_document/column_inputs.py new file mode 100644 index 0000000..3c76faa --- /dev/null +++ b/examples/conc_col_pmm/calc_document/column_inputs.py @@ -0,0 +1,17 @@ +import dataclasses + +from ..constants.rebar_data import REBAR_SIZES, REBAR_STRENGTHS + + +@dataclasses.dataclass +class ColumnInputs: + w: float = 24 + h: float = 36 + bar_size: str = REBAR_SIZES[5] + bar_cover: float = 2 + bars_x: int = 6 + bars_y: int = 8 + fc: float = 8000 + fy: float = REBAR_STRENGTHS[1] + cover_to_center: bool = False + spiral_reinf: bool = False diff --git a/examples/conc_col_pmm/calc_document/dcr_calc_runner.py b/examples/conc_col_pmm/calc_document/dcr_calc_runner.py new file mode 100644 index 0000000..10213ca --- /dev/null +++ b/examples/conc_col_pmm/calc_document/dcr_calc_runner.py @@ -0,0 +1,32 @@ +from efficalc import FigureFromMatplotlib, Heading + +from ..calc_document.plotting import get_capacity, point_plotter +from ..col.axial_limits import AxialLimits +from ..col.column import Column +from ..pmm_search.ecc_search.get_dcr_ecc import get_dcr_ecc +from ..pmm_search.load_combo import LoadCombination + + +def calc_dcrs( + load_combos: list[LoadCombination], mesh, col: Column, axial_limits: AxialLimits +): + dcr_results = [] + for load in load_combos: + if load.show_in_report: # show the full calculations for this load case + Heading( + "DCR Calculation for Load Case P=" + + str(round(load.p, 1)) + + ", Mx=" + + str(round(load.mx, 1)) + + ", My=" + + str(round(load.my, 1)) + ) + capacity_pts = get_capacity.get_capacity(mesh, load) + # plot the PM diagram for this point + pm_figure = point_plotter.plot(capacity_pts, load, False) + FigureFromMatplotlib( + pm_figure, "PM interaction diagram for this load case. " + ) + + dcr_results.append(get_dcr_ecc(col, load, axial_limits)) + return dcr_results diff --git a/examples/conc_col_pmm/calc_document/document_wrapper.py b/examples/conc_col_pmm/calc_document/document_wrapper.py new file mode 100644 index 0000000..f141062 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/document_wrapper.py @@ -0,0 +1,42 @@ +from efficalc.report_builder import ReportBuilder + +from .calculation import calculation as col_input_calc + +# parameters: "override_inputs" is boolean and indicates whether the values +# provided in the next two arguments (column parameters, then load data) +# should override the default/user-input Input values + + +def run(override_inputs: bool, col_data: list, loads: list[list]): + if override_inputs: + new_inputs = {} + + input_to_name = { + "w": "w", + "h": "h", + "bar_size": "", + "bar_cover": " ", + "bars_x": " ", + "bars_y": " ", + "fc": r"f^{\prime}_c", + "fy": "f_y", + "cover_type": " ", + "transverse_type": " ", + } + for i in range(len(input_to_name)): + new_inputs[input_to_name["w"]] = col_data[0] + new_inputs[input_to_name["h"]] = col_data[1] + new_inputs[input_to_name["bar_size"]] = col_data[2] + new_inputs[input_to_name["bar_cover"]] = col_data[3] + new_inputs[input_to_name["bars_x"]] = col_data[4] + new_inputs[input_to_name["bars_y"]] = col_data[5] + new_inputs[input_to_name["fc"]] = col_data[6] + new_inputs[input_to_name["fy"]] = col_data[7] + new_inputs[input_to_name["cover_type"]] = col_data[8] + new_inputs[input_to_name["transverse_type"]] = col_data[9] + + builder = ReportBuilder(lambda: col_input_calc(default_loads=loads), new_inputs) + builder.view_report() + else: + builder = ReportBuilder(col_input_calc) + builder.view_report() diff --git a/examples/conc_col_pmm/calc_document/full_calc_document.py b/examples/conc_col_pmm/calc_document/full_calc_document.py new file mode 100644 index 0000000..afc4c01 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/full_calc_document.py @@ -0,0 +1,33 @@ +from ..calc_document.plotting import pure_mx_my_plotter +from ..col import assign_max_min +from ..col.col_canvas import draw_column_with_dimensions +from ..col.column import Column +from ..pmm_search.load_combo import LoadCombination +from .dcr_calc_runner import calc_dcrs +from .plotting.pmm_mesh import get_mesh +from .results_summary import results_summarizer + + +def calculation( + col: Column, + load_combos: list[LoadCombination], +): + # draw the column cross-section with dimensions and callouts + draw_column_with_dimensions.draw(col, "Section of Column") + + # calculate_axial_load_limits the max tension and compression to this column + axial_limits = assign_max_min.calculate_axial_load_limits(col) + + # Retrieve the quarter PMM mesh, which has points + # in the format (Mx, My, P). + _, _, _, mesh = get_mesh(col, 48, 18, axial_limits) + + # show the PM curves for bending purely about the x and y axes + pure_mx_my_plotter.plot(mesh) + + # calculate DCRs for all load cases + dcr_results = calc_dcrs(load_combos, mesh, col, axial_limits) + + results_summarizer(load_combos, dcr_results) + + return axial_limits diff --git a/examples/conc_col_pmm/calc_document/plotting/PMM.py b/examples/conc_col_pmm/calc_document/plotting/PMM.py new file mode 100644 index 0000000..ed34bff --- /dev/null +++ b/examples/conc_col_pmm/calc_document/plotting/PMM.py @@ -0,0 +1,13 @@ +import dataclasses + +from numpy import ndarray + +from ...pmm_search.load_combo import LoadCombination + + +@dataclasses.dataclass +class PMM: + X: ndarray + Y: ndarray + Z: ndarray + load_combos: list[LoadCombination] diff --git a/examples/conc_col_pmm/calc_document/plotting/__init__.py b/examples/conc_col_pmm/calc_document/plotting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/calc_document/plotting/get_capacity.py b/examples/conc_col_pmm/calc_document/plotting/get_capacity.py new file mode 100644 index 0000000..9b62a9b --- /dev/null +++ b/examples/conc_col_pmm/calc_document/plotting/get_capacity.py @@ -0,0 +1,54 @@ +import math + +from ...pmm_search.load_combo import LoadCombination + +""" +The function below interpolates between points on the PMM diagram to construct +the PM diagram for a given load point. It returns two lists which contain the +resultant moment and axial load for the points of the PM diagram at the lambda +for the given load point. +Parameters: "mesh" is the quarter PMM mesh consisting of (Mx, My, P) points and +"point" is the load point in the form (P, Mx, My). +""" + + +def get_capacity(mesh, point: LoadCombination): + pt_count = len(mesh) # the number of rows of points vertically + quarter = len(mesh[0]) - 1 # the number of angle spaces between points in a + # quadrant + + angle_space = (math.pi / 2) / quarter # the horizontal space between points + + lambda_transform = math.atan2(abs(point.my), abs(point.mx)) # the angle for + # the current point transformed to the range 0 to 90, where 0 must + # correspond to My=0 + + index = int(lambda_transform // angle_space) # the position of the mesh point in + # each row of "mesh" just below the lambda of the load point + + # this is how far beyond the chosen interval the load point lies + angle_extra = lambda_transform % angle_space + + # define factors that can be multiplied by the load values at two + # adjacent points to interpolate between those points + factors = [(angle_space - angle_extra) / angle_space, angle_extra / angle_space] + + phi_Mn = [0] * pt_count + phi_Pn = [0] * pt_count + + for i in range(pt_count): + # calculate the estimate of the biaxial moment and axial force capacity + # at the current point. The index for the angle of the current point must be + # limited to "quarter" to prevent it from exceeding the size of the + # mesh array, even if "index" is the max element due to rounding. + phi_Pn[i] = sum( + (mesh[i][min(index + j, quarter)][2] * factors[j] for j in range(2)) + ) + + for j in range(2): + # calculate the moment resultant for the given capacity point + moment = math.sqrt( + sum((mesh[i][min(index + j, quarter)][k] ** 2 for k in range(2))) + ) + phi_Mn[i] += moment * factors[j] + return [phi_Mn, phi_Pn] diff --git a/examples/conc_col_pmm/calc_document/plotting/get_pmm_data.py b/examples/conc_col_pmm/calc_document/plotting/get_pmm_data.py new file mode 100644 index 0000000..1c79a57 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/plotting/get_pmm_data.py @@ -0,0 +1,33 @@ +import numpy as np + +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from ...pmm_search.load_combo import LoadCombination +from .PMM import PMM +from .pmm_mesh import get_mesh + +""" +This function takes inputs for a column and creates a +dataclass instance containing all the information for the +PMM diagram for the given column. That information is used +for plotting the PMM diagram. +""" + + +def get_pmm_data( + col: Column, + intervals: int, + load_spaces: int, + load_combos: list[LoadCombination], + axial_limits: AxialLimits, +): + # get the capacity point mesh for plotting the PMM surface. x, y, + # and z correspond to Mx, My, and P, respectively. "quarter_mesh" + # has just one quarter of the PMM mesh (including points aligned + # with the x and y axes) + x, y, z, _ = get_mesh(col, intervals, load_spaces, axial_limits) + X = np.array(x) + Y = np.array(y) + Z = np.array(z) + + return PMM(X=X, Y=Y, Z=Z, load_combos=load_combos) diff --git a/examples/conc_col_pmm/calc_document/plotting/pmm_mesh.py b/examples/conc_col_pmm/calc_document/plotting/pmm_mesh.py new file mode 100644 index 0000000..3e4b1c5 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/plotting/pmm_mesh.py @@ -0,0 +1,108 @@ +import math + +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from ...pmm_search.load_search import bisect_load +from ...pmm_search.load_search.point_search_load import search + + +def get_mesh( + col: Column, intervals, load_spaces, axial_limits: AxialLimits +) -> tuple[list[list[int]], list[list[int]], list[list[int]], list[list[float]]]: + # "intervals" is the number of spaces in the angle of eccentricity, + # "load_spaces" is the number of vertical spaces in the PMM diagram + # Returns a mesh containing all the points of the PMM diagram, plus + # a quarter of that mesh + + # vectors containing the points to plot + x: list[list[int]] = [] + y: list[list[int]] = [] + z: list[list[int]] = [] + + # the bottom point must be added intervals+1 times because each level of + # the mesh needs intervals+1 points in order to form a closed surface + x.append([0] * (intervals + 1)) + y.append([0] * (intervals + 1)) + z.append([axial_limits.min_phi_pn] * (intervals + 1)) + + vert_space = axial_limits.load_span / load_spaces + c_guess = 0 # the starting guess for neutral axis + + quarter = math.floor(intervals / 4) # number of angle spaces in one quadrant + lambda_space = 2 * math.pi / intervals # the change in angle between points + theta_guesses = [-lambda_space * i for i in range(1, quarter)] # the guess for + # what theta should be at each point between the x and y axes, to be + # updated at each vertical level + count = 0 + + quarter_mesh = [[0] * (quarter + 1) for i in range(load_spaces + 2)] # a matrix + # to contain the output points, with height (outer dimension) spanning all + # the axial load interpolation points and width (inner dimension) spanning + # from lambda=0 to lambda=90 + quarter_mesh[0] = [(0, 0, axial_limits.min_phi_pn) for i in range(quarter + 1)] + quarter_mesh[load_spaces + 1] = [ + (0, 0, axial_limits.max_phi_pn) for i in range(quarter + 1) + ] + + for vert_count in range(1, load_spaces + 1): + count += 1 + c_guess += (col.w + col.h) / (2 * load_spaces) # increment the guess for + # the neutral axis depth + load_target = axial_limits.min_phi_pn + vert_space * vert_count + coords = [[0] * (intervals + 1) for i in range(3)] + + # get the output for theta=0 and apply it in 3 points + out = bisect_load.bisect(col, [0, load_target], [0, c_guess], axial_limits) + quarter_mesh[vert_count][0] = out[:3] + for pos in (0, quarter * 2, quarter * 4): + mult = [1 if (pos != quarter * 2) else -1, 1, 1] + for j in range(3): + coords[j][pos] = out[j] * mult[j] + + # get the output for theta=-90 and apply it in 2 points + out = bisect_load.bisect( + col, [math.pi / 2, load_target], [-math.pi / 2, c_guess], axial_limits + ) + quarter_mesh[vert_count][quarter] = out[:3] + for pos in (quarter, quarter * 3): + mult = [1, 1 if pos == quarter else -1, 1] + for j in range(3): + coords[j][pos] = out[j] * mult[j] + + # iterate over the remaining possible neutral axis angles between 0 + # and -90 and for each one, add its point to the four quadrants of the + # PMM diagram + lambda_target = lambda_space # the current target lambda + for i in range(1, quarter): + out = search( + col, + [lambda_target, load_target], + [theta_guesses[i - 1], c_guess], + axial_limits, + ) + quarter_mesh[vert_count][i] = out[:3] + theta_guesses[i - 1] = out[3] + + c_guess = out[4] + indices = [ + i, + quarter * 2 - i, + quarter * 2 + i, + intervals - i, + ] # the indices in + # the vector "coords" corresponding to the current point + for pos, index in enumerate(indices): + mult = [1 if (pos == 0 or pos == 3) else -1, 1 if pos < 2 else -1, 1] + for j in range(3): + coords[j][index] = out[j] * mult[j] + lambda_target += lambda_space + + x.append(coords[0]) + y.append(coords[1]) + z.append(coords[2]) + + x.append([0] * (intervals + 1)) + y.append([0] * (intervals + 1)) + z.append([axial_limits.max_phi_pn] * (intervals + 1)) + + return x, y, z, quarter_mesh diff --git a/examples/conc_col_pmm/calc_document/plotting/pmm_plotter_plotly.py b/examples/conc_col_pmm/calc_document/plotting/pmm_plotter_plotly.py new file mode 100644 index 0000000..4ca3c35 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/plotting/pmm_plotter_plotly.py @@ -0,0 +1,218 @@ +import numpy as np +import plotly.graph_objects as go + +from .PMM import PMM + +""" +This function plots the factored load capacity diagram for the column +""" + + +def plot(pmm_data: PMM): + X = pmm_data.X + Y = pmm_data.Y + Z = pmm_data.Z + + axis_colors = {"x": "#FF0000", "y": "#00CC00", "z": "#0000FF"} + ax_labels = {"x": "${\phi}M_{nx}$", "y": "${\phi}M_{ny}$", "z": "${\phi}P_n$"} + + # factors for how far the axes should extend past the PMM surface + z_factor = 0.12 + xy_factor = 0.05 + + data = {} + # list for min, then max, then difference for each of X, Y, Z + min_max = [] + for dir in (X, Y, Z): + min_max.append([dir.min(), dir.max()]) + for i in range(3): + min_max[i].append(min_max[i][1] - min_max[i][0]) + min1, max1 = min_max[2][:2] + range1 = min_max[2][2] + data["z"] = {} + data["z"]["range"] = range1 * (1 + 2 * z_factor) + data["z"]["min"] = min1 - range1 * z_factor + data["z"]["max"] = max1 + range1 * z_factor + + # set the maximum aspect ratio (should be low so that the PMM + # surface won't be super long/short in any dimension + min_aspect = 2 + max_xy = max(min_max[0][2], min_max[1][2]) + for i, dir in enumerate(("x", "y")): + data[dir] = {} + length = min((1 + xy_factor) * max_xy, min_aspect * min_max[i][2]) + data[dir]["range"] = length + data[dir]["min"] = -length / 2 + data[dir]["max"] = length / 2 + + # creates an arrow which is used as the axis for whichever axis + # is input as the parameter + def get_arrow(axisname="x"): + ax_color = axis_colors[axisname] + scale = [[0, ax_color], [1, ax_color]] + # Create arrow body + body = go.Scatter3d( + marker=dict(size=1, color=ax_color), + line=dict(color=ax_color, width=5), + showlegend=False, # hide the legend + hoverinfo="skip", + ) + + head = go.Cone( + sizemode="raw", + sizeref=1, + autocolorscale=None, + colorscale=scale, + showscale=False, # disable additional colorscale for arrowheads + hoverinfo="skip", + ) + for ax, direction in zip(("x", "y", "z"), ("u", "v", "w")): + if ax == axisname: + body[ax] = data[ax]["min"], data[ax]["max"] + head[ax] = [data[ax]["max"]] + head[direction] = [0.05 * data[axisname]["range"]] + else: + body[ax] = 0, 0 + head[ax] = [0] + head[direction] = [0] + + return [body, head] + + def add_axis_arrows(fig): + for ax in ("x", "y", "z"): + for item in get_arrow(ax): + fig.add_trace(item) + + # returns a dictionary which forms the axis label for one of the axes + def get_annotation_for_ax(ax): + d = dict( + showarrow=False, + text=ax_labels[ax], + xanchor="left", + font=dict(color="#1f1f1f", size=18), + ) + for ax_ in ("x", "y", "z"): + if ax_ == ax: + d[ax_] = data[ax]["max"] - data[ax]["range"] * 0.025 + else: + d[ax_] = 0 + + if ax in {"x", "y"}: + d["xshift"] = 12 + else: + d["xshift"] = 2 + + return d + + def get_axis_names(): + return [get_annotation_for_ax(ax) for ax in ("x", "y", "z")] + + # returns the Plotly axes (should be empty since these default axes + # are not used) + def get_scene_axis(): + return dict( + title="", # remove axis label (x,y,z) + showbackground=False, + visible=False, + showticklabels=False, # hide numeric values of axes + showgrid=False, # Show box around plot + ) + + # plot the 3D PMM surface + fig = go.Figure( + layout=dict( + title={ + "text": "PMM Diagram", + "x": 0.5, + "y": 0.98, + "xanchor": "center", + "yanchor": "top", + "font": {"color": "#1f1f1f", "size": 18}, + }, + autosize=True, + width=550, + height=550, + margin=dict(l=5, r=5, b=5, t=5), + scene=dict( + xaxis=get_scene_axis(), + yaxis=get_scene_axis(), + zaxis=get_scene_axis(), + annotations=get_axis_names(), + aspectmode="cube", + ), + ), + ) + + add_axis_arrows(fig) + + # convert the PMM data to a numpy array for plotting + load_data = np.array([[ld.p, ld.mx, ld.my] for ld in pmm_data.load_combos]) + + # define a color (dark blue) for the load points + pt_color = "#002095" + + # Add points for the load cases. Note that because + # the load cases are passed in the form (P, Mx, My), + # the order must be changed for plotting + fig.add_trace( + go.Scatter3d( + mode="markers", + showlegend=False, # hide the legend + x=load_data[:, 1], + y=load_data[:, 2], + z=load_data[:, 0], + marker=dict( + color=pt_color, + size=4, + ), + ) + ) + + # define a color (orange) for the PMM surface + surface_color = "#ffbb0f" + surface_scale = [[0, surface_color], [1, surface_color]] + fig.add_trace( + go.Surface( + z=Z, + x=X, + y=Y, + opacity=0.5, + colorscale=surface_scale, + showscale=False, # Set to True to show colorscale + name="", + ) + ) + + # define a color (near-white) for the plotted mesh + line_color = "#f7f7f7" + + # plot a mesh on the PMM surface between the capacity points + line_size = 1.5 + for i in range(X.shape[0]): + fig.add_trace( + go.Scatter3d( + mode="lines", + line=dict(color=line_color, width=line_size), + showlegend=False, # hide the legend + x=X[i, :], + y=Y[i, :], + z=Z[i, :], + ) + ) + for i in range(X.shape[1]): + fig.add_trace( + go.Scatter3d( + mode="lines", + line=dict(color=line_color, width=line_size), + showlegend=False, # hide the legend + x=X[:-1, i], + y=Y[:-1, i], + z=Z[:-1, i], + ) + ) + fig.update_yaxes( + scaleanchor="x", + scaleratio=1, + ) + + return fig diff --git a/examples/conc_col_pmm/calc_document/plotting/point_plotter.py b/examples/conc_col_pmm/calc_document/plotting/point_plotter.py new file mode 100644 index 0000000..7e82102 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/plotting/point_plotter.py @@ -0,0 +1,90 @@ +import math + +import matplotlib.pyplot as plt + +from ...pmm_search.load_combo import LoadCombination + +""" +The function "plot" creates and returns a matplotlib figure showing a PM diagram. + +Parameters: + +capacity_pts: a list of the capacity points on the PM curve, in the format +(Mxy, P). + +point: the load point if this plot is for a DCR calculation, or None if this plot +is simply to show the PM curve aligned with the Mx or My axis. The point is in +the format (P, Mx, My). + +only_Mx: boolean, and only used when point=None, True means this is for bending +in the x-direction. +""" + + +def plot(capacity_pts, point: LoadCombination | None, only_Mx): + [phi_Mn, phi_Pn] = capacity_pts + + if point: + # get the lamdba for the load point + pt_lambda = math.atan2(point.my, point.mx) # the angle for the current point + else: + # set the lambda to the desired axis + pt_lambda = 0 if only_Mx else math.pi / 2 + + pt_count = len(phi_Mn) # how many points are plotted + + fig, ax = plt.subplots() + ax.grid(True, which="both", zorder=0, linewidth=0.4) + # set the x-spine (see below for more info on `set_position`) + ax.spines["left"].set_position("zero") + + # turn off the right spine/ticks + ax.spines["right"].set_color("none") + ax.yaxis.tick_left() + + # set the y-spine + ax.spines["bottom"].set_position("zero") + + # turn off the top spine/ticks + ax.spines["top"].set_color("none") + ax.xaxis.tick_bottom() + + plot_angle = str(round(pt_lambda * 180 / math.pi)) + "$\degree$" + ax.set_title("P-M Interaction Diagram at $\lambda=$" + plot_angle) + + plt.plot(phi_Mn, phi_Pn, linewidth=2, zorder=1) + + ax.set_xlabel("${\phi}M_{nxy}$ (kip-ft)", fontsize=12, zorder=2) + ax.set_ylabel("${\phi}P_{n}$ (kip)", fontsize=12, zorder=2) + + ax.set_axisbelow(True) + + load_span = phi_Pn[-1] - phi_Pn[0] + + # define offset distances for axis labels depending on point + label_offset_x = max(phi_Mn) * 0.008 + label_offsets_y=-load_span * 0.05, load_span * 0.02 + # label the intersections with the y-axis + for i in (0, pt_count - 1): + pos = (phi_Mn[i], phi_Pn[i]) + label = str(round(phi_Pn[i], 1)) + plt.plot(pos[0], pos[1], marker="+", ms=12, mew=1.2, c="black", zorder=3) + label_offset_y=label_offsets_y[0] if i==0 else label_offsets_y[1] + plt.text(pos[0] + label_offset_x, pos[1] + label_offset_y, label, zorder=3) + + if point: + Muxy = math.sqrt(point.mx**2 + point.my**2) # the biaxial moment + + # plot and label the load point + pos = (Muxy, point.p) + moment_label = "($M_{uxy}=$" + str(round(Muxy, 1)) + " kip-ft, " + axial_label = "$P_u=$" + str(round(point.p, 1)) + " kip)" + label = moment_label + "\n" + axial_label + + plt.plot(pos[0], pos[1], marker="+", ms=12, mew=1.2, c="red", zorder=4) + label_offset_y=label_offsets_y[0] if i==0 else label_offsets_y[1] + plt.text(pos[0] + label_offset_x, pos[1] + label_offset_y, label, zorder=5) + + fig = ax.get_figure() + + return fig diff --git a/examples/conc_col_pmm/calc_document/plotting/pure_mx_my_plotter.py b/examples/conc_col_pmm/calc_document/plotting/pure_mx_my_plotter.py new file mode 100644 index 0000000..5a490cc --- /dev/null +++ b/examples/conc_col_pmm/calc_document/plotting/pure_mx_my_plotter.py @@ -0,0 +1,27 @@ +from efficalc import FigureFromMatplotlib, Heading + +from . import point_plotter + +""" +This function plots the intersection of the PMM surface with the vertical planes +Mx-P and My-P. +""" + + +def plot(mesh): + Heading("PM Diagrams for Pure Mx and My") + n = len(mesh) + m = len(mesh[0]) + capacity_pts = [ + [mesh[i][0][0] for i in range(n)], + [mesh[i][0][2] for i in range(n)], + ] + pm_figure = point_plotter.plot(capacity_pts, None, True) + FigureFromMatplotlib(pm_figure, "PM interaction diagram for pure Mx.") + + capacity_pts = [ + [mesh[i][m - 1][1] for i in range(n)], + [mesh[i][m - 1][2] for i in range(n)], + ] + pm_figure = point_plotter.plot(capacity_pts, None, False) + FigureFromMatplotlib(pm_figure, "PM interaction diagram for pure My.") diff --git a/examples/conc_col_pmm/calc_document/results_summary.py b/examples/conc_col_pmm/calc_document/results_summary.py new file mode 100644 index 0000000..595ea21 --- /dev/null +++ b/examples/conc_col_pmm/calc_document/results_summary.py @@ -0,0 +1,41 @@ +from efficalc import Calculation, Comparison, Heading, Table + +from ..pmm_search.load_combo import LoadCombination + +""" +Creates a table with the DCRs for all load cases. Also calculates the max +DCR, checks whether it is less than 1, and adds this to the result check. +""" + + +def results_summarizer(load_combos: list[LoadCombination], dcr_results): + Heading("Summary of Results") + data = [ + [ + ld.p, + ld.mx, + ld.my, + round(dcr_results[i], 2), + "O.K." if dcr_results[i] < 1 else "N.G.", + ] + for i, ld in enumerate(load_combos) + ] + + headers = ["Pu (kip)", "Mux (kip-ft)", "Muy (kip-ft)", "PM Vector DCR", "Passing?"] + Table(data, headers, "DCRs For All Load Cases", False, False) + + # calculate the max DCR and show + max_dcr = Calculation( + "DCR_{max}", + round(max(dcr_results), 2), + description="Maximum of all Load Case DCRs", + ) + Comparison( + max_dcr, + "<", + 1.0, + true_message="O.K.", + false_message="N.G.", + description="Max DCR check", + result_check=True, + ) diff --git a/examples/conc_col_pmm/calc_document/show_dcr_calc.py b/examples/conc_col_pmm/calc_document/show_dcr_calc.py new file mode 100644 index 0000000..813f3af --- /dev/null +++ b/examples/conc_col_pmm/calc_document/show_dcr_calc.py @@ -0,0 +1,68 @@ +from latexexpr_efficalc import Variable + +from efficalc import Calculation, Comparison, Heading, TextBlock, absolute, maximum + +from ..pmm_search.load_combo import LoadCombination +from .column_capacities import ColumnCapacities + + +def show(load: LoadCombination, capacity: ColumnCapacities): + + Heading(f"DCR Calculation - Load Case #{load.id}", 2) + + only_axial = capacity.Mx.result() == 0 and capacity.My.result() == 0 + if only_axial: + TextBlock( + "Since the load point is on the P axis, the DCR can be calculated by comparing the applied axial" + " load to the axial capacity calculated above:" + ) + p = Variable("P_u", load.p, "kip") + dcr = Calculation( + f"DCR_{{{load.id}}}", + absolute(p / capacity.P) if capacity.P.result() != 0 else 0, + ) + Comparison( + dcr, + "<", + 1.0, + true_message="O.K.", + false_message="N.G.", + description=f"Design check for load case #{load.id}", + ) + + else: + TextBlock( + "Compare the ratios of demand to capacity for Mx, My, and P to show that the calculated capacity" + " point is on the same PMM vector as the demand point. Note that the absolute value for the" + " moment DCRs is because the column has equal moment capacity in opposite directions by symmetry." + ) + + mux = Variable("M_{ux}", load.mx, "kip-ft") + dcr_mx = Calculation( + "DCR_{Mx}", absolute(mux / capacity.Mx) if capacity.Mx.result() != 0 else 0 + ) + + my = Variable("M_{uy}", load.my, "kip-ft") + dcr_my = Calculation( + "DCR_{My}", absolute(my / capacity.My) if capacity.My.result() != 0 else 0 + ) + + p = Variable("P_u", load.p, "kip") + dcr_p = Calculation( + "DCR_{P}", absolute(p / capacity.P) if capacity.P.result() != 0 else 0 + ) + + dcr = Calculation( + f"DCR_{{{load.id}}}", + maximum(dcr_mx, dcr_my, dcr_p), + "", + "The final DCR is:", + ) + Comparison( + dcr, + "<", + 1.0, + true_message="O.K.", + false_message="N.G.", + description=f"Design check for load case #{load.id}", + ) diff --git a/examples/conc_col_pmm/calc_document/try_axis_document.py b/examples/conc_col_pmm/calc_document/try_axis_document.py new file mode 100644 index 0000000..3d2c4ea --- /dev/null +++ b/examples/conc_col_pmm/calc_document/try_axis_document.py @@ -0,0 +1,626 @@ +import math + +from latexexpr_efficalc import Variable + +from efficalc import ( + PI, + Calculation, + ComparisonStatement, + Heading, + Symbolic, + Table, + TextBlock, + cos, + maximum, + minimum, + r_brackets, + sin, + tan, +) + +from ..col.axial_limits import AxialLimits +from ..col.col_canvas import draw_column_comp_zone, draw_column_with_triangle +from ..col.column import Column +from .column_capacities import ColumnCapacities + + +# Make sure that small numbers like 5.684e−14 get rounded to 0 +def _round(x: float): + return round(x, 5) + + +def try_axis_document( + col: Column, + axial_limits: AxialLimits, + theta_input=0, + c_input=10, +) -> ColumnCapacities: + + w = col.w_input + h = col.h_input + bar_area = col.rebar_area_input + fc = col.fc_input + fy = col.fy_input + E_s = col.steel_modulus_input + conc_epsilon = col.concrete_strain_input + + TextBlock( + "The neutral axis angle and depth below are iteratively determined to produce a capacity point aligning " + "exactly with the PMM vector of the applied load. \n" + ) + theta = Calculation( + "\\theta", _round(theta_input), "rad", description="Neutral axis angle" + ) + c = Calculation("c", _round(c_input), "in", description="Neutral axis depth") + + if c.get_value() == 0: + # This is dead code, this function doesn't get called for pure axial load cases, can probably remove + TextBlock( + "Because the neutral axis depth is zero, the column is in pure tension." + ) + return ColumnCapacities( + Variable("{\\phi}M_{nx}", 0, "kip-ft"), + Variable("{\\phi}M_{ny}", 0, "kip-ft"), + axial_limits.min_phi_pn_calculation, + ) + Heading("Forces in the Concrete", 2) + + if fc.get_value() <= 4000: + ComparisonStatement(2500, "<=", fc, "<=", 4000) + beta1 = Calculation( + "\\beta_1", + 0.85, + "", + "", + "ACI 318-19 Table 22.2.2.4.3(a)", + ) + elif fc.get_value() < 8000: + ComparisonStatement(4000, "<", fc, "<", 8000) + beta1 = Calculation( + "\\beta_1", + 0.85 - 0.05 * r_brackets(fc - 4000) / 1000, + "", + "", + "ACI 318-19 Table 22.2.2.4.3(b)", + ) + else: + ComparisonStatement(fc, ">=", 8000) + beta1 = Calculation( + "\\beta_1", + 0.65, + "", + "", + "ACI 318-19 Table 22.2.2.4.3(c)", + ) + a = Calculation( + "a", + beta1 * c, + "in", + "Depth of equivalent compression zone:", + "ACI 318-19 22.2.2.4.1", + ) + fc = Calculation( + "f^{\prime}_c", + fc / 1000, + "ksi", + description="Concrete strength converted to ksi:", + ) + epsilon = 1e-6 # acceptable error for considering the neutral axis to + # be vertical or horizontal + + intersects = [False] * 4 # whether line of concrete compression intersects the + # left, top, right, and bottom edges of the section + if theta.get_value() > -math.pi / 2 + epsilon: + left_y_temp = ( + col.half_h + - a.get_value() / math.cos(theta.get_value()) + - col.w * math.tan(theta.get_value()) + ) + intersects[0] = -col.half_h < left_y_temp < col.half_h + if intersects[0]: + left_y = Calculation( + "y_{\\mathrm{left}}", + h / 2 - a / cos(theta) - w * tan(theta), + "in", + " y coordinate of equivalent compression zone intersection with left edge:", + ) + + right_y = col.half_h - a.get_value() / math.cos(theta.get_value()) + intersects[2] = -col.half_h < right_y < col.half_h + if intersects[2]: + right_y = Calculation( + "y_{\\mathrm{right}}", + h / 2 - a / cos(theta), + "in", + " y coordinate of equivalent compression zone intersection with right edge:", + ) + + if theta.get_value() < -epsilon: + top_x_temp = col.half_w + a.get_value() / math.sin(theta.get_value()) + intersects[1] = -col.half_w < top_x_temp < col.half_w + if intersects[1]: + top_x = Calculation( + "x_{\\mathrm{top}}", + w / 2 + a / sin(theta), + "in", + " x coordinate of equivalent compression zone intersection with top edge:", + ) + + # if theta is -90, take the tan of 0 and get a change + # of zero, and if theta is -45, take the tan of 45 + # and get somewhat of an increase in x + bot_x_temp = ( + col.half_w + + a.get_value() / math.sin(theta.get_value()) + + col.h * math.tan(PI / 2 + theta.get_value()) + ) + intersects[3] = -col.half_w < bot_x_temp < col.half_w + if intersects[3]: + bot_x = Calculation( + "x_{\\mathrm{bottom}}", + w / 2 + a / sin(theta) + h * tan(PI / 2 + theta), + "in", + " x coordinate of equivalent compression zone intersection with bottom edge:", + ) + + # define accumulator variables + pn_tot = 0 + mnx_tot = 0 + mny_tot = 0 + + if not any(intersects): # the whole concrete section is in compression + TextBlock("The equivalent compression zone covers the whole concrete section. ") + pn_conc = Calculation("P_{\\mathrm{n, conc.}}", 0.85 * fc * w * h, "kips") + mnx_conc = Calculation( + "M_{\\mathrm{nx, conc.}}", + 0, + "kip-in", + ) + mny_conc = Calculation( + "M_{\\mathrm{ny, conc.}}", + 0, + "kip-in", + ) + else: + conc_area_num = 0 + + def add_axial_moment(pt_a, pt_b, pt_c): + nonlocal pn_tot, mnx_tot, mny_tot, conc_area_num + conc_area_num += 1 + Heading("Forces in Concrete Area " + str(conc_area_num), 3) + TextBlock( + "Below are the coordinates of the three points A, B, and C which define compression area" + " number " + str(conc_area_num) + "." + ) + pt_a = ( + Calculation("A_x", pt_a[0]), + Calculation("A_y", pt_a[1]), + ) + pt_b = ( + Calculation("B_x", pt_b[0]), + Calculation("B_y", pt_b[1]), + ) + pt_c = ( + Calculation("C_x", pt_c[0]), + Calculation("C_y", pt_c[1]), + ) + points = (pt_a, pt_b, pt_c) + points = [((pt[0]).get_value(), (pt[1]).get_value()) for pt in points] + draw_column_with_triangle.draw( + col, "Compression Area Outline", conc_area_num, points + ) + tri_area = Calculation( + "A_{\\mathrm{triangle}}", + 0.5 + * abs( + pt_a[0] * pt_b[1] + + pt_b[0] * pt_c[1] + + pt_c[0] * pt_a[1] + - pt_a[0] * pt_c[1] + - pt_b[0] * pt_a[1] + - pt_c[0] * pt_b[1] + ), + "in^2", + "Calculate the area of this triangular compression zone:", + ) + + centr_x = Calculation( + "x_{\\mathrm{centroid}}", + (pt_a[0] + pt_b[0] + pt_c[0]) / 3, + "in", + "x coordinate of the centroid of this zone:", + ) + centr_y = Calculation( + "y_{\\mathrm{centroid}}", + (pt_a[1] + pt_b[1] + pt_c[1]) / 3, + "in", + "y coordinate of the centroid of this zone:", + ) + return tri_area, centr_x, centr_y + + pt1 = (-w / 2, left_y) if intersects[0] else (top_x, h / 2) + pt2 = (bot_x, -h / 2) if intersects[3] else (w / 2, right_y) + points_block = [(col.w / 2, col.h / 2)] + if intersects[2]: + points_block.append((col.w / 2, right_y.get_value())) + else: + points_block.append((col.w / 2, -col.h / 2)) + points_block.append((bot_x.get_value(), -col.h / 2)) + if intersects[1]: + points_block.append((top_x.get_value(), col.h / 2)) + else: + points_block.append((-col.w / 2, left_y.get_value())) + points_block.append((-col.w / 2, col.h / 2)) + draw_column_comp_zone.draw( + col, "Equivalent compression zone outlined in red. ", points_block + ) + TextBlock( + "The equivalent stress block is now broken down into triangular areas and the forces are calculated for each." + ) + + (tri_area, centr_x, centr_y) = add_axial_moment( + pt1, pt2, (w / 2, h / 2) + ) # compression triangle to + # top right corner + pn_top_right = Calculation( + "P_{\\mathrm{n,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area, + "kips", + ) + mnx_top_right = Calculation( + "M_{\\mathrm{nx,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area * centr_y, + "kip-in", + ) + mny_top_right = Calculation( + "M_{\\mathrm{ny,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area * centr_x, + "kip-in", + ) + if intersects[0]: # there is a compression triangle to top left corner + (tri_area, centr_x, centr_y) = add_axial_moment( + (-w / 2, h / 2), (w / 2, h / 2), pt1 + ) + pn_top_left = Calculation( + "P_{\\mathrm{n,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area, + "kips", + ) + mnx_top_left = Calculation( + "M_{\\mathrm{nx,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area * centr_y, + "kip-in", + ) + mny_top_left = Calculation( + "M_{\\mathrm{ny,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area * centr_x, + "kip-in", + ) + if intersects[3]: # there is a compression triangle to bot right corner + (tri_area, centr_x, centr_y) = add_axial_moment( + (w / 2, -h / 2), (w / 2, h / 2), pt2 + ) + pn_bot_right = Calculation( + "P_{\\mathrm{n,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area, + "kips", + ) + mnx_bot_right = Calculation( + "M_{\\mathrm{nx,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area * centr_y, + "kip-in", + ) + mny_bot_right = Calculation( + "M_{\\mathrm{ny,\ Area\ " + str(conc_area_num) + "}}", + 0.85 * fc * tri_area * centr_x, + "kip-in", + ) + Heading("Total Forces in Concrete", 3) + if intersects[0] and intersects[3]: # take a sum for all 3 areas + pn_conc = Calculation( + "P_{\\mathrm{n, conc.}}", + pn_top_right + pn_top_left + pn_bot_right, + "kips", + ) + mnx_conc = Calculation( + "M_{\\mathrm{nx, conc.}}", + mnx_top_right + mnx_top_left + mnx_bot_right, + "kip-in", + ) + mny_conc = Calculation( + "M_{\\mathrm{ny, conc.}}", + mny_top_right + mny_top_left + mny_bot_right, + "kip-in", + ) + elif intersects[0]: + pn_conc = Calculation( + "P_{\\mathrm{n, conc.}}", pn_top_right + pn_top_left, "kips" + ) + mnx_conc = Calculation( + "M_{\\mathrm{nx, conc.}}", + mnx_top_right + mnx_top_left, + "kip-in", + ) + mny_conc = Calculation( + "M_{\\mathrm{ny, conc.}}", + mny_top_right + mny_top_left, + "kip-in", + ) + elif intersects[3]: + pn_conc = Calculation( + "P_{\\mathrm{n, conc.}}", pn_top_right + pn_bot_right, "kips" + ) + mnx_conc = Calculation( + "M_{\\mathrm{nx, conc.}}", + mnx_top_right + mnx_bot_right, + "kip-in", + ) + mny_conc = Calculation( + "M_{\\mathrm{ny, conc.}}", + mny_top_right + mny_bot_right, + "kip-in", + ) + else: + pn_conc = Calculation("P_{\\mathrm{n, conc.}}", pn_top_right, "kips") + mnx_conc = Calculation( + "M_{\\mathrm{nx, conc.}}", + mnx_top_right, + "kip-in", + ) + mny_conc = Calculation( + "M_{\\mathrm{ny, conc.}}", + mny_top_right, + "kip-in", + ) + + Heading("Equations for Rebar Axial and Moment Calculations", 2) + + TextBlock( + "Each bar is at coordinates (x,y) relative to the column centroid. For example, the top right bar" + " is located at the coordinates below:" + ) + right_bar_x = col.half_w - col.edge_to_bar_center # x coordinate of bars on the + # right edge + top_bar_y = col.half_h - col.edge_to_bar_center # y coordinate of bars on the + # top edge + + x = Calculation("x_{\mathrm{bar}}", right_bar_x) + y = Calculation("y_{\mathrm{bar}}", top_bar_y) + + d_bar = Symbolic( + "d_{\mathrm{bar}}", + r_brackets(w / 2 - x) * cos(theta + PI / 2) + + r_brackets(h / 2 - y) * sin(theta + PI / 2), + "Effective depth:", + ) + strain_sym = Symbolic( + "\epsilon_{\mathrm{bar}}", conc_epsilon * (d_bar - c) / c, "Strain:" + ) + stress_sym = Symbolic( + "\sigma_{\mathrm{bar}}", + minimum(fy, maximum(-fy, conc_epsilon * strain_sym)), + "Stress:", + ) + Symbolic( + "\sigma_{\mathrm{bar}}", + stress_sym + 0.85 * fc, + "If the bar is in the equivalent compression zone, add the concrete stress to avoid double-counting:", + ) + TextBlock( + "Note that the sign of the following expressions is reversed because positive stress in the rebar " + "is defined as tension while positive axial moment in the column is defined as compression." + ) + Symbolic( + "P_{\mathrm{bar}}", + -bar_area * stress_sym * y, + "Contribution to moment about the " "x axis:", + ) + Symbolic( + "M_{\mathrm{x, bar}}", + -bar_area * stress_sym * y, + "Contribution to moment about " "the x axis:", + ) + Symbolic( + "M_{\mathrm{y, bar}}", + -bar_area * stress_sym * x, + "Contribution to moment about " "the y axis:", + ) + # define new accumulators + pn = 0 + mnx = 0 + mny = 0 + + bar_num = 0 + + def add_bar(coords): + nonlocal pn, mnx, mny, bar_num + bar_num += 1 + bar_calc = [bar_num] + bar_calc.extend([round(val, 2) for val in coords]) + # "offset" is the distance from the center of the bar to the line + # passing through the top right corner of the section and parallel to + # the neutral axis + offset = (col.w / 2 - coords[0]) * math.cos(theta.get_value() + math.pi / 2) + ( + col.h / 2 - coords[1] + ) * math.sin(theta.get_value() + math.pi / 2) + bar_calc.append(round(offset, 2)) + strain = conc_epsilon.get_value() * (offset - c.get_value()) / c.get_value() + bar_calc.append(round(strain, 4)) + stress = min( + fy.get_value(), + max( + -fy.get_value(), + conc_epsilon.get_value() + * E_s.get_value() + * (offset - c.get_value()) + / c.get_value(), + ), + ) + bar_calc.append(round(stress, 2)) + in_comp_zone = offset < a.get_value() + if in_comp_zone: + stress += 0.85 * fc.get_value() + bar_calc.append(round(0.85 * fc.get_value(), 2)) + else: + bar_calc.append(0) + # since negative strain and negative stress are defined as + # compression for rebar but compression is positive in the conc. + # the sign of everything needs to be changed + pn -= bar_area.get_value() * stress + bar_calc.append(round(-bar_area.get_value() * stress, 1)) + mnx -= bar_area.get_value() * stress * coords[1] + # the +0 is to avoid rounding to -0 + bar_calc.append(round(-bar_area.get_value() * stress * coords[1], 1) + 0) + mny -= bar_area.get_value() * stress * coords[0] + bar_calc.append(round(-bar_area.get_value() * stress * coords[0], 1) + 0) + + rebar_matrix.append(bar_calc) + + y = col.y_start + # iterate over the bars along the left and right lines + # (this includes corner bars) + bar_count = 0 + + headers = [ + "Bar Number", + "X Coord. (in)", + "Y Coord. (in)", + "Effective Depth d (in)", + "Strain (unitless)", + "Stress (ksi)", + "Stress Correction for Displaced Concrete (ksi)", + "Axial Force (kips)", + "Contribution to Mx (kip-in)", + "Contribution to My (kip-in)", + ] + rebar_matrix = [] + for i in range(col.bars_y): + for x in (-right_bar_x, right_bar_x): + coords = [x, y] + bar_count += 1 + add_bar(coords) + y += col.y_space + + x = col.x_start + for i in range(col.bars_x - 2): + # iterate over the bars along the top and bottom lines, and add the + # force for each one + for y in (-top_bar_y, top_bar_y): + coords = [x, y] + add_bar(coords) + x += col.x_space + Heading("Tabulated Rebar Results", 2) + + Table(rebar_matrix, headers) + + Heading("Force Totals", 2) + pn_steel = Calculation("P_{\\mathrm{n, steel}}", pn, "kips") + mnx_steel = Calculation( + "M_{\\mathrm{nx, steel}}", + _round(mnx), + "kip-in", + ) + mny_steel = Calculation( + "M_{\\mathrm{ny, steel}}", + _round(mny), + "kip-in", + ) + pn = Calculation("P_{\\mathrm{n, tot}}", pn_conc + pn_steel, "kips") + mnx = Calculation( + "M_{\\mathrm{nx, tot}}", + mnx_conc + mnx_steel, + "kip-in", + ) + mny = Calculation( + "M_{\\mathrm{ny, tot}}", + mny_conc + mny_steel, + "kip-in", + ) + + Heading("Capacity Calculation", 2) + + TextBlock("The extreme tension reinforcement is centered at these coordinates:") + coords = [0, 0] + coords[0] = Calculation("x_{\\mathrm{bar}}", -right_bar_x, "in") + coords[1] = Calculation("y_{\\mathrm{bar}}", -top_bar_y, "in") + offset = Calculation( + "d_t", + r_brackets(w / 2 - coords[0]) * cos(theta + PI / 2) + + r_brackets(h / 2 - coords[1]) * sin(theta + PI / 2), + "in", + ) + max_strain = Calculation( + "\\epsilon_y", conc_epsilon * r_brackets(offset - c) / c, "" + ) + yield_strain = Calculation("\\epsilon_{ty}", fy / E_s, "") + if max_strain.get_value() <= yield_strain.get_value(): + ComparisonStatement(max_strain, "<=", yield_strain) + strain_level = 0 + elif ( + yield_strain.get_value() + < max_strain.get_value() + < yield_strain.get_value() + 0.003 + ): + ComparisonStatement(yield_strain, "<", max_strain, "<", yield_strain + 0.003) + strain_level = 1 + else: + ComparisonStatement(max_strain, ">=", yield_strain + 0.003) + strain_level = 2 + if col.spiral_reinf: + if strain_level == 0: + phi = Calculation( + "\\phi", + 0.75, + "", + "Failure is compression-controlled and transverse reinforcement is spiral:", + "ACI 318-19 Table 21.2.2(a)", + ) + elif strain_level == 1: + phi = Calculation( + "\\phi", + 0.75 + 0.15 * r_brackets(max_strain - yield_strain) / 0.003, + "", + "Failure is transition and transverse reinforcement is spiral:", + "ACI 318-19 Table 21.2.2(c)", + ) + else: + phi = Calculation( + "\\phi", + 0.90, + "", + "Failure is tension-controlled and transverse reinforcement is spiral:", + "ACI 318-19 Table 21.2.2(e)", + ) + else: + if strain_level == 0: + phi = Calculation( + "\\phi", + 0.65, + "", + "Failure is compression-controlled and transverse reinforcement is tied:", + "ACI 318-19 Table 21.2.2(b)", + ) + elif strain_level == 1: + phi = Calculation( + "\\phi", + 0.65 + 0.25 * r_brackets(max_strain - yield_strain) / 0.003, + "", + "Failure is transition and transverse reinforcement is tied:", + "ACI 318-19 Table 21.2.2(d)", + ) + else: + phi = Calculation( + "\\phi", + 0.90, + "", + "Failure is tension-controlled and transverse reinforcement is tied:", + "ACI 318-19 Table 21.2.2(f)", + ) + TextBlock("Factored axial and moment capacities:") + phi_pn = Calculation( + "{\\phi}P_n", minimum(phi * pn, axial_limits.max_phi_pn_calculation), "kips" + ) + phi_mnx = Calculation("{\\phi}M_{nx}", phi * mnx / 12, "kip-ft") + phi_mny = Calculation("{\\phi}M_{ny}", phi * mny / 12, "kip-ft") + + return ColumnCapacities(phi_mnx, phi_mny, phi_pn) diff --git a/examples/conc_col_pmm/col/__init__.py b/examples/conc_col_pmm/col/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/col/assign_max_min.py b/examples/conc_col_pmm/col/assign_max_min.py new file mode 100644 index 0000000..b2f00cc --- /dev/null +++ b/examples/conc_col_pmm/col/assign_max_min.py @@ -0,0 +1,95 @@ +from efficalc import Calculation, Heading, TextBlock, r_brackets + +from ..struct_analysis import try_axis +from .axial_limits import AxialLimits +from .column import Column + + +def calculate_axial_load_limits(col: Column) -> AxialLimits: + + w = col.w_input + h = col.h_input + bar_area = col.rebar_area_input + bars_x = col.bars_x_input + bars_y = col.bars_y_input + fc = col.fc_input + fy = col.fy_input + + Heading("Axial Capacity Calculations") + + steel_area = Calculation( + "A_{st}", + bar_area * r_brackets(2 * bars_x + 2 * bars_y - 4), + "in^2", + "Total area of longitudinal reinforcement:", + ) + tot_area = Calculation("A_g", w * h, "in^2", "Gross section area") + Heading("Compressive Capacity", 2) + max_pn = Calculation( + "P_0", + 0.85 * fc / 1000 * r_brackets(tot_area - steel_area) + fy * steel_area, + "kips", + "", + "ACI 318-19 22.4.2.2", + ) + if col.spiral_reinf: + TextBlock("Because the transverse reinforcement is spiral:") + max_pn_limit = Calculation( + "P_{\mathrm{n,max}}", + 0.85 * max_pn, + "kips", + "Maximum axial strength", + "ACI 318-19 22.4.2.1(b)", + ) + phi = Calculation( + "\\phi", + 0.75, + "", + "", + "ACI 318-19 Table 21.2.2(a)", + ) + else: + TextBlock("Because the transverse reinforcement is tied:") + max_pn_limit = Calculation( + "P_{\mathrm{n,max}}", + 0.80 * max_pn, + "kips", + "", + "ACI 318-19 22.4.2.1(a)", + ) + phi = Calculation( + "\\phi", + 0.65, + "", + "", + "ACI 318-19 Table 21.2.2(b)", + ) + + max_phi_pn = Calculation("{\\phi}P_{\mathrm{n,max}}", phi * max_pn_limit, "kips") + + Heading("Tensile Capacity", 2) + min_pn = Calculation( + "P_{\mathrm{nt,max}}", + -1 * fy * steel_area, + "kips", + "", + "ACI 318-19 22.4.3.1", + ) + + phi = Calculation( + "\\phi", + try_axis.PHI_FLEXURE, + "", + "Because failure is tension-controlled:", + "ACI 318-19 21.2.2(e)", + ) + min_phi_pn = Calculation("{\\phi}P_{\mathrm{nt,max}}", phi * min_pn, "kips") + + return AxialLimits( + max_pn.result(), + max_phi_pn.result(), + max_phi_pn, + min_pn.result(), + min_phi_pn.result(), + min_phi_pn, + ) diff --git a/examples/conc_col_pmm/col/axial_limits.py b/examples/conc_col_pmm/col/axial_limits.py new file mode 100644 index 0000000..8249c64 --- /dev/null +++ b/examples/conc_col_pmm/col/axial_limits.py @@ -0,0 +1,19 @@ +import dataclasses + +from efficalc import Calculation + + +@dataclasses.dataclass +class AxialLimits: + max_pn: float + max_phi_pn: float + max_phi_pn_calculation: Calculation + min_pn: float + min_phi_pn: float + min_phi_pn_calculation: Calculation + + # difference between the maximum and minimum allowable loads, + # to be used for normalizing error + @property + def load_span(self) -> float: + return self.max_phi_pn - self.min_phi_pn diff --git a/examples/conc_col_pmm/col/col_canvas/__init__.py b/examples/conc_col_pmm/col/col_canvas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/col/col_canvas/draw_column_comp_zone.py b/examples/conc_col_pmm/col/col_canvas/draw_column_comp_zone.py new file mode 100644 index 0000000..c6f1341 --- /dev/null +++ b/examples/conc_col_pmm/col/col_canvas/draw_column_comp_zone.py @@ -0,0 +1,76 @@ +import math + +from efficalc.canvas import Canvas, Dimension, Polyline, Rectangle, Text + +from ..column import Column +from .draw_plain_column import draw as draw_base + + +def draw(col: Column, caption_input: str, points) -> Canvas: + # number is the zone number with which to label this triangle, points are the three + # corners of the triangle as a tuple of 2-element tuples + canvas = draw_base(col) + canvas.caption = caption_input + + points = [(pt[0] + col.w / 2, col.h / 2 - pt[1]) for pt in points] + n = len(points) + centr = [sum((points[i][j] for i in range(n))) / n for j in range(2)] + points.append(points[0]) + + canvas.add(Polyline(points, stroke_width=0.2, stroke="#c80c00")) + + canvas.add( + Rectangle( + centr[0] - 0.3, + centr[1] - 1, + 10, + 1.4, + 0.2, + 0.2, + stroke_width=0.12, + stroke="#c80c00", + fill="white", + ) + ) + canvas.add( + Text( + "Compression Zone", + centr[0], + centr[1], + font_size=1.2, + ) + ) + + # margin around the section + m = 8 + scale_factor = 0.37817187 * math.log((col.w + col.h) / 2) + 0.03808133 + # add dimensions + common_dim_styles = { + "unit": '"', + "gap": 0.15, + "stroke_width": 0.08 * scale_factor, + "text_size": 1.2, + } + for pt in points[:-1]: + if pt[0] == 0 and 0 < pt[1] < col.h: + # this point is on the left edge, add dimension + canvas.add(Dimension(0, 0, 0, pt[1], offset=-0.5 * m, **common_dim_styles)) + if pt[0] == col.w and 0 < pt[1] < col.h: + # this point is on the right edge, add dimension + canvas.add( + Dimension(col.w, 0, col.w, pt[1], offset=0.5 * m, **common_dim_styles) + ) + if pt[1] == 0 and 0 < pt[0] < col.w: + # this point is on the top edge, add dimension + canvas.add( + Dimension(col.w, 0, pt[0], 0, offset=0.5 * m, **common_dim_styles) + ) + if pt[1] == col.h and 0 < pt[0] < col.w: + # this point is on the bottom edge, add dimension + canvas.add( + Dimension( + col.w, col.h, pt[0], col.h, offset=-0.5 * m, **common_dim_styles + ) + ) + + return canvas diff --git a/examples/conc_col_pmm/col/col_canvas/draw_column_with_dimensions.py b/examples/conc_col_pmm/col/col_canvas/draw_column_with_dimensions.py new file mode 100644 index 0000000..7e089e0 --- /dev/null +++ b/examples/conc_col_pmm/col/col_canvas/draw_column_with_dimensions.py @@ -0,0 +1,118 @@ +import math + +from efficalc.canvas import ArrowMarker, Canvas, Dimension, Leader, Line, Text + +from ...constants.rebar_data import rebar_diameter +from ..column import Column +from .draw_plain_column import draw as draw_base + + +def draw(col: Column, caption_input: str, unit: str = '"') -> Canvas: + canvas = draw_base(col) + canvas.display_type = "report-input" + canvas.caption = caption_input + scale_factor = 0.37817187 * math.log((col.w + col.h) / 2) + 0.03808133 + + # margin around the section + m = 8 + + # x and y axes + canvas.add( + Line( + col.w / 2, + col.h / 2, + col.w + m / 4, + col.h / 2, + stroke="black", + stroke_width=0.08, + marker_end=ArrowMarker(), + ) + ) + canvas.add( + Line( + col.w / 2, + col.h / 2, + col.w / 2, + -m / 4, + stroke="black", + stroke_width=0.08, + marker_end=ArrowMarker(), + ) + ) + + # define reinforcement properties + bars_x = col.bars_x + bars_y = col.bars_y + long_bar_radius = rebar_diameter(col.bar_size) / 2 + + # define "cover" to be the cover to center of bar (regardless of whether the user specified the + # cover to be clear or to center) + cover = col.bar_cover if col.cover_to_center else col.bar_cover + long_bar_radius + + canvas.add(Text("x", col.w + m / 4 + 0.25, col.h / 2, font_size=1)) + canvas.add(Text("y", col.w / 2, -m / 4 - 0.25, font_size=1)) + + # add dimensions + common_dim_styles = { + "unit": unit, + "gap": 0.15, + "stroke_width": 0.08 * scale_factor, + "text_size": 1.2, + } + canvas.add(Dimension(0, 0, col.w, 0, offset=0.5 * m, **common_dim_styles)) + canvas.add(Dimension(col.w, 0, col.w, col.h, offset=0.5 * m, **common_dim_styles)) + canvas.add( + Dimension( + 0, + cover, + col.bar_cover, + cover, + offset=0.25 * m + cover, + **common_dim_styles, + ) + ) + + # add leaders + common_leader_styles = { + "marker": ArrowMarker(), + "landing_len": 1, + "stroke_width": 0.08 * scale_factor, + "text_size": 1.2, + } + + x_bar_starting_x = cover + x_bar_spacing = (col.w - 2 * cover) / (bars_x - 1) + x_bar_y_bot = col.h - cover + + y_bar_starting_y = x_bar_starting_x + y_bar_spacing = (col.h - 2 * cover) / (bars_y - 1) + y_bar_x_left = x_bar_starting_x + + bottom_bar_x = x_bar_starting_x + (col.bars_x - 2) * x_bar_spacing + bottom_bar_y = x_bar_y_bot + canvas.add( + Leader( + bottom_bar_x + long_bar_radius * 0.85, + bottom_bar_y + long_bar_radius * 0.85, + bottom_bar_x + cover + m / 8, + col.h + m / 8, + f"({col.bars_x}){col.bar_size} x-direction, E.S.", + **common_leader_styles, + ) + ) + left_bar_x = y_bar_x_left + long_bar_radius * 0.85 + left_bar_y = ( + y_bar_starting_y + (col.bars_y - 2) * y_bar_spacing + long_bar_radius * 0.85 + ) + canvas.add( + Leader( + left_bar_x, + left_bar_y, + cover + 2 * long_bar_radius + x_bar_spacing, + col.h + m / 5, + f"({col.bars_y}){col.bar_size} y-direction, N.S.", + **common_leader_styles, + ) + ) + + return canvas diff --git a/examples/conc_col_pmm/col/col_canvas/draw_column_with_triangle.py b/examples/conc_col_pmm/col/col_canvas/draw_column_with_triangle.py new file mode 100644 index 0000000..9473a31 --- /dev/null +++ b/examples/conc_col_pmm/col/col_canvas/draw_column_with_triangle.py @@ -0,0 +1,100 @@ +import math + +from efficalc.canvas import Canvas, CircleMarker, Dimension, Polyline, Rectangle, Text + +from ...struct_analysis import triangles +from ..column import Column +from .draw_plain_column import draw as draw_base + + +def draw(col: Column, caption_input: str, number, points) -> Canvas: + # number is the zone number with which to label this triangle, points are the three + # corners of the triangle as a tuple of 2-element tuples + canvas = draw_base(col) + canvas.caption = caption_input + + points = [(pt[0] + col.w / 2, col.h / 2 - pt[1]) for pt in points] + points.append(points[0]) + + marker = CircleMarker() + canvas.add( + Polyline( + points, + stroke_width=0.2, + stroke="#c80c00", + marker_mid=marker, + marker_end=marker, + ) + ) + + centr = triangles.triangle_centroid(*points[:3]) + canvas.add( + Rectangle( + centr[0] - 0.3, + centr[1] - 1, + 3.8, + 1.4, + 0.2, + 0.2, + stroke_width=0.12, + stroke="#c80c00", + fill="white", + ) + ) + canvas.add( + Text( + "Area " + str(number), + centr[0], + centr[1], + font_size=1.2, + ) + ) + + pt_labels = ["Point A", "Point B", "Point C"] + for i in range(3): + offsets = ( + [0.2] * 2 + if (points[i][0] > 1e-6 and points[i][1] < col.h - 1e-6) + else [-3, -1.1] + ) + canvas.add( + Text( + pt_labels[i], + points[i][0] + offsets[0], + points[i][1] - offsets[1], + font_size=1, + ) + ) + + # margin around the section + m = 8 + scale_factor = 0.37817187 * math.log((col.w + col.h) / 2) + 0.03808133 + # add dimensions + common_dim_styles = { + "unit": '"', + "gap": 0.15, + "stroke_width": 0.08 * scale_factor, + "text_size": 1.2, + } + for pt in points: + if pt[0] == 0 and 0 < pt[1] < col.h: + # this point is on the left edge, add dimension + canvas.add(Dimension(0, 0, 0, pt[1], offset=-0.5 * m, **common_dim_styles)) + if pt[0] == col.w and 0 < pt[1] < col.h: + # this point is on the right edge, add dimension + canvas.add( + Dimension(col.w, 0, col.w, pt[1], offset=0.5 * m, **common_dim_styles) + ) + if pt[1] == 0 and 0 < pt[0] < col.w: + # this point is on the top edge, add dimension + canvas.add( + Dimension(col.w, 0, pt[0], 0, offset=0.5 * m, **common_dim_styles) + ) + if pt[1] == col.h and 0 < pt[0] < col.w: + # this point is on the bottom edge, add dimension + canvas.add( + Dimension( + col.w, col.h, pt[0], col.h, offset=-0.5 * m, **common_dim_styles + ) + ) + return canvas diff --git a/examples/conc_col_pmm/col/col_canvas/draw_plain_column.py b/examples/conc_col_pmm/col/col_canvas/draw_plain_column.py new file mode 100644 index 0000000..74cd71b --- /dev/null +++ b/examples/conc_col_pmm/col/col_canvas/draw_plain_column.py @@ -0,0 +1,106 @@ +from efficalc.canvas import Canvas, Circle, Rectangle + +from ...constants.rebar_data import rebar_diameter + + +def draw(col) -> Canvas: + # margin around the section + m = 8 + + # define reinforcement properties + bars_x = col.bars_x + bars_y = col.bars_y + long_bar_radius = rebar_diameter(col.bar_size) / 2 + stirrup_diameter = rebar_diameter(col.shear_bar_size) + stirrup_bend_radius = 3 * stirrup_diameter + + # define "cover" to be the cover to center of bar (regardless of whether the user specified the + # cover to be clear or to center) + cover = col.bar_cover if col.cover_to_center else col.bar_cover + long_bar_radius + + # set up the canvas + canvas = Canvas( + col.w + 2 * m, + col.h + 2 * m, + min_xy=(-m, -m), + scale=20, + default_element_stroke_width=0, + ) + + # Draw the beam outline + column_outline = Rectangle(0, 0, col.w, col.h, fill="rgb(0 0 0 / 30%)") + canvas.add(column_outline) + + # add transverse reinforcement + stirrups = Rectangle( + cover - long_bar_radius - stirrup_diameter / 2, + cover - long_bar_radius - stirrup_diameter / 2, + col.w - 2 * cover + 2 * long_bar_radius + stirrup_diameter, + col.h - 2 * cover + 2 * long_bar_radius + stirrup_diameter, + rx=stirrup_bend_radius, + ry=stirrup_bend_radius, + stroke_width=stirrup_diameter, + stroke="#004aad", + ) + + canvas.add(stirrups) + + # add longitudinal reinforcement + x_bar_starting_x = cover + x_bar_spacing = (col.w - 2 * cover) / (bars_x - 1) + x_bar_y_bot = col.h - cover + x_bar_y_top = x_bar_starting_x + + y_bar_starting_y = x_bar_starting_x + y_bar_spacing = (col.h - 2 * cover) / (bars_y - 1) + y_bar_x_left = x_bar_starting_x + y_bar_x_right = col.w - cover + + for i in range(bars_x): + # bottom bar + canvas.add( + Circle( + x_bar_starting_x + i * x_bar_spacing, + x_bar_y_bot, + long_bar_radius, + fill="black", + ) + ) + + # top bar + canvas.add( + Circle( + x_bar_starting_x + i * x_bar_spacing, + x_bar_y_top, + long_bar_radius, + fill="black", + ) + ) + + last_y_bar = bars_y - 1 + for i in range(bars_y): + if i == 0 or i == last_y_bar: + # corner bars are drawn as x bar + pass + + # left bar + canvas.add( + Circle( + y_bar_x_left, + y_bar_starting_y + i * y_bar_spacing, + long_bar_radius, + fill="black", + ) + ) + + # right bar + canvas.add( + Circle( + y_bar_x_right, + y_bar_starting_y + i * y_bar_spacing, + long_bar_radius, + fill="black", + ) + ) + + return canvas diff --git a/examples/conc_col_pmm/col/column.py b/examples/conc_col_pmm/col/column.py new file mode 100644 index 0000000..1f55879 --- /dev/null +++ b/examples/conc_col_pmm/col/column.py @@ -0,0 +1,104 @@ +from efficalc import Calculation, Input + +from ..constants.rebar_data import STEEL_E, rebar_area, rebar_diameter + +""" +The class Column takes the following parameters: + w = section width (x-dir) (in.) + h = section height (y-dir) (in.) + bar_size = rebar number (Imperial) + bar_cover = concrete cover to the longitudinal rebar (in.) + bars_x = number of bars on the top/bottom edge + bars_y = number of bars on the left/right edge + fc = concrete f'c (psi) + fy = steel yield strength (ksi) + cover_to_center = boolean: concrete cover is to the center of the rebar, + false means it is clear cover (to edge of bars) + spiral_reinf = boolean: the shear reinforcement is spiral + rebar_area = area of one longitudinal bar (in^2) + steel_modulus = steel modulus of elasticity (ksi) + concrete_strain = max concrete strain at f'c +""" + + +class Column: + # a constant for now + shear_bar_size = "#4" + + def __init__( + self, + w_input: Input, + h_input: Input, + bar_size_input: Input, + bar_cover_input: Input, + bars_x_input: Input, + bars_y_input: Input, + fc_input: Input, + fy_input: Input, + cover_to_center: bool, + spiral_reinf: bool, + rebar_area_input: Calculation, + steel_modulus_input: Calculation, + concrete_strain_input: Calculation, + ): + + self.w_input = w_input + self.h_input = h_input + self.bar_size_input = bar_size_input + self.bar_cover_input = bar_cover_input + self.bars_x_input = bars_x_input + self.bars_y_input = bars_y_input + self.fc_input = fc_input + self.fy_input = fy_input + self.rebar_area_input = rebar_area_input + self.steel_modulus_input = steel_modulus_input + self.concrete_strain_input = concrete_strain_input + + self.cover_to_center = cover_to_center + self.spiral_reinf = spiral_reinf + + self.w = w_input.get_value() + self.h = h_input.get_value() + self.bar_size = bar_size_input.get_value() + self.bar_cover = bar_cover_input.get_value() + self.bars_x = bars_x_input.get_value() + self.bars_y = bars_y_input.get_value() + self.fc = fc_input.get_value() + self.fy = fy_input.get_value() + + self.half_w = self.w / 2 + self.half_h = self.h / 2 + self.area = self.w * self.h + + # coordinates of three corner points + self.bot_right = (self.half_w, -self.half_h) + self.top_right = (self.half_w, self.half_h) + self.top_left = (-self.half_w, self.half_h) + + self.bar_area = rebar_area(self.bar_size) + self.bar_dia = rebar_diameter(self.bar_size) + if cover_to_center: # the concrete cover is to center of bars + self.edge_to_bar_center = self.bar_cover + else: + self.edge_to_bar_center = self.bar_cover + self.bar_dia / 2 + + # the center-to-center spacing of the top/bottom bars + self.x_space = (self.w - 2 * self.edge_to_bar_center) / (self.bars_x - 1) + + # the center-to-center spacing of the left/right bars + self.y_space = (self.h - 2 * self.edge_to_bar_center) / (self.bars_y - 1) + + # the y-coordinate of the bottom left corner bar + self.y_start = -self.half_h + self.edge_to_bar_center + + # the x-coordinate of the second-from-left bar on the bottom edge + self.x_start = -self.half_w + self.edge_to_bar_center + self.x_space + + self.beta1 = max( + 0.65, min(0.85, 0.85 - 0.05 / 1000 * (self.fc - 4000)) + ) # the ratio + # of a/c for this column + self.steel_yield = self.fy / STEEL_E # strain in steel at yielding + + # safety factor for compression-controlled column + self.PHI_COMP = 0.75 if spiral_reinf else 0.65 diff --git a/examples/conc_col_pmm/constants/__init__.py b/examples/conc_col_pmm/constants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/constants/concrete_data.py b/examples/conc_col_pmm/constants/concrete_data.py new file mode 100644 index 0000000..1b7b275 --- /dev/null +++ b/examples/conc_col_pmm/constants/concrete_data.py @@ -0,0 +1 @@ +MAX_CONCRETE_STRAIN = 0.003 diff --git a/examples/conc_col_pmm/constants/rebar_data.py b/examples/conc_col_pmm/constants/rebar_data.py new file mode 100644 index 0000000..ea3f09a --- /dev/null +++ b/examples/conc_col_pmm/constants/rebar_data.py @@ -0,0 +1,17 @@ +from typing import Literal + +REBAR_SIZES = ["#3", "#4", "#5", "#6", "#7", "#8", "#9", "#10", "#11", "#14", "#18"] +REBAR_DIAMETERS = [0.38, 0.50, 0.63, 0.75, 0.88, 1.00, 1.13, 1.27, 1.41, 1.69, 2.26] +REBAR_AREAS = [0.11, 0.20, 0.31, 0.44, 0.60, 0.79, 1.00, 1.27, 1.56, 2.25, 4.00] +STEEL_E = 29000 # steel modulus of elasticity in ksi +REBAR_STRENGTHS = [40, 60, 80] + +BarSize = Literal["#3", "#4", "#5", "#6", "#7", "#8", "#9", "#10", "#11", "#14", "#18"] + + +def rebar_area(size: BarSize): + return REBAR_AREAS[REBAR_SIZES.index(size)] + + +def rebar_diameter(size: BarSize): + return REBAR_DIAMETERS[REBAR_SIZES.index(size)] diff --git a/examples/conc_col_pmm/images/ColumnNA-cropped.svg b/examples/conc_col_pmm/images/ColumnNA-cropped.svg new file mode 100644 index 0000000..546c07b --- /dev/null +++ b/examples/conc_col_pmm/images/ColumnNA-cropped.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + x + + + + + + y + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + θ + N.A. + + + \ No newline at end of file diff --git a/examples/conc_col_pmm/images/minimizer-cropped.svg b/examples/conc_col_pmm/images/minimizer-cropped.svg new file mode 100644 index 0000000..c26db80 --- /dev/null +++ b/examples/conc_col_pmm/images/minimizer-cropped.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + f=0 + g=0 + f + + + + + + g + + + + + + Next iteration point + + + + + + Current iteration point + + + + + + + + \ No newline at end of file diff --git a/examples/conc_col_pmm/pmm_search/__init__.py b/examples/conc_col_pmm/pmm_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/pmm_search/ecc_search/__init__.py b/examples/conc_col_pmm/pmm_search/ecc_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/pmm_search/ecc_search/change_ecc.py b/examples/conc_col_pmm/pmm_search/ecc_search/change_ecc.py new file mode 100644 index 0000000..c0cc836 --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/ecc_search/change_ecc.py @@ -0,0 +1,38 @@ +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from ...struct_analysis import try_axis +from .get_error_ecc import get_error + +delta = 1e-8 # small change to be used for finite differences + + +# this function is the same as "change" except that it finds a direction for +# eccentricity rather than for c +def change(col: Column, guess, target, output, axial_limits: AxialLimits): + error = get_error(output, target) + # A small positive value "delta" is added to both inputs in order to test + # the effect on the results of "try_axis". It may have to be negative for + # theta to avoid exceeding 0. + delta0 = delta if guess[0] < -delta else -delta + + output2 = try_axis.try_axis(col, guess[0] + delta0, guess[1], axial_limits) + a = (output2[0] - output[0]) / delta0 + c = (output2[1] - output[1]) / delta0 + + output2 = try_axis.try_axis(col, guess[0], guess[1] + delta, axial_limits) + b = (output2[0] - output[0]) / delta + d = (output2[1] - output[1]) / delta + + e = target[0] - output[0] + f = target[1] - output[1] + + change = [0] * 2 + det = a * d - b * c + + # avoid divide by zero + if det != 0: + # set the planned changes in theta and c to try to reach the point at + # which lambda and load are both their target values + change[0] = (d * e - b * f) / det + change[1] = (a * f - c * e) / det + return change, error diff --git a/examples/conc_col_pmm/pmm_search/ecc_search/get_dcr_ecc.py b/examples/conc_col_pmm/pmm_search/ecc_search/get_dcr_ecc.py new file mode 100644 index 0000000..57cbb77 --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/ecc_search/get_dcr_ecc.py @@ -0,0 +1,75 @@ +import math + +from latexexpr_efficalc import Variable + +from ...calc_document.column_capacities import ColumnCapacities +from ...calc_document.show_dcr_calc import show as show_dcr_calc +from ...calc_document.try_axis_document import try_axis_document +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from ..load_combo import LoadCombination +from . import point_search_ecc + + +# accepts as arguments the column and the load point and returns +# the dcr for this particular load point. +def get_dcr_ecc(col: Column, load: LoadCombination, axial_limits: AxialLimits): + target_M = math.sqrt(load.mx**2 + load.my**2) + target_ecc = math.atan2(load.p, target_M) + + tol = 0.01 * math.pi / 180 + # Load case is in pure tension + if target_ecc > math.pi / 2 - tol: + if load.show_in_report: + capacities = ColumnCapacities( + Variable("{\\phi}M_{nx}", 0, "kip-ft"), + Variable("{\\phi}M_{ny}", 0, "kip-ft"), + axial_limits.max_phi_pn_calculation, + ) + show_dcr_calc(load, capacities) + return load.p / axial_limits.max_phi_pn + + # Load case is in pure compression + if target_ecc < -math.pi / 2 + tol: + if load.show_in_report: + capacities = ColumnCapacities( + Variable("{\\phi}M_{nx}", 0, "kip-ft"), + Variable("{\\phi}M_{ny}", 0, "kip-ft"), + axial_limits.min_phi_pn_calculation, + ) + show_dcr_calc(load, capacities) + return load.p / axial_limits.min_phi_pn + + # Load has a bending moment component + target_lambda = math.atan2(abs(load.my), abs(load.mx)) + target = (target_lambda, target_ecc) + + # the best guess of theta is -lambda + guess = [ + -target_lambda, + (col.w + col.h) / 2, + ] + + # find the point on the PMM diagram that is on the same vector as the + # applied load + Mx, My, P, final_guess = point_search_ecc.search(col, target, guess, axial_limits) + + dcrs = [float("inf")] * 3 + dcr_candidates = [] + if P != 0: + dcrs[0] = load.p / P + dcr_candidates.append(load.p / P) + if Mx != 0: + dcrs[1] = abs(load.mx / Mx) + dcr_candidates.append(abs(load.mx / Mx)) + if My != 0: + dcrs[2] = abs(load.my / My) + dcr_candidates.append(abs(load.my / My)) + dcr = max(dcr_candidates) if len(dcr_candidates) > 0 else 0 + if load.show_in_report: + capacities = try_axis_document( + col, axial_limits, final_guess[0], final_guess[1] + ) + + show_dcr_calc(load, capacities) + return dcr diff --git a/examples/conc_col_pmm/pmm_search/ecc_search/get_error_ecc.py b/examples/conc_col_pmm/pmm_search/ecc_search/get_error_ecc.py new file mode 100644 index 0000000..94a448a --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/ecc_search/get_error_ecc.py @@ -0,0 +1,11 @@ +import math + + +def get_error(output, target): + # find the difference between both outputs and their target values + # print("in get error") + # print(output) + # print(target) + lambda_diff = output[0] - target[0] + ecc_diff = output[1] - target[1] + return math.sqrt(lambda_diff**2 + ecc_diff**2) diff --git a/examples/conc_col_pmm/pmm_search/ecc_search/limit_comp_ecc.py b/examples/conc_col_pmm/pmm_search/ecc_search/limit_comp_ecc.py new file mode 100644 index 0000000..185d77e --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/ecc_search/limit_comp_ecc.py @@ -0,0 +1,28 @@ +import math + +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from ...struct_analysis import try_axis +from . import get_error_ecc + + +def limit_comp(col: Column, guess, target, axial_limits: AxialLimits): + guess[0] = min(0, max(guess[0], -math.pi / 2)) + guess[1] = max(1e-6, guess[1]) + # guess[1]=max(c_lims[0],max(c_lims[1],guess[1])) + output = try_axis.try_axis(col, guess[0], guess[1], axial_limits) + lim_factor = 0.999 + while output[3] > lim_factor * col.PHI_COMP * axial_limits.max_pn: + # the current phi_pn (without the 0.8) is at or almost at its + # maximum value, which means the column is probably in full + # compression, which must be avoided or derivatives will be zero + guess[1] /= 2 + output = try_axis.try_axis(col, guess[0], guess[1], axial_limits) + while output[3] < lim_factor * axial_limits.min_phi_pn: + # the current phi_pn is at or almost at its minimum value, so c must + # b increased + guess[1] *= 10 + output = try_axis.try_axis(col, guess[0], guess[1], axial_limits) + # update the distance from the target point + error = get_error_ecc.get_error(output, target) + return output, error diff --git a/examples/conc_col_pmm/pmm_search/ecc_search/point_search_ecc.py b/examples/conc_col_pmm/pmm_search/ecc_search/point_search_ecc.py new file mode 100644 index 0000000..5024636 --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/ecc_search/point_search_ecc.py @@ -0,0 +1,66 @@ +import math + +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from . import change_ecc, limit_comp_ecc + +""" +Searches for a point on the PMM diagram with a certain lambda and eccentricity. +Parameters: the column object, the target (as a tuple of lambda, then axial load), and the +initial guess (as a tuple of theta, then neutral axis depth) +Returns: Mx, My, P, and the final guess as a two-value list +*the angle of eccentricity guess must be between 0 and -pi/2 and the target lambda +must be between 0 and pi/2 +""" + + +# the purpose of this function is to search for a point with a particular lambda and eccentricity +def search(col: Column, target, guess, axial_limits: AxialLimits): + tol = 0.001 # error accepted as the actual point + + # get output for the initial guess point + output, error = limit_comp_ecc.limit_comp(col, guess, target, axial_limits) + + count = 1 # count of iterations (calls to "try_axis") + count_lim = 100 # iteration limit + + while error > tol and count < count_lim: + # get a descent direction for the current point + direction, error = change_ecc.change(col, guess, target, output, axial_limits) + + # since finding the direction requires two "try_axis" calls + count += 2 + + # the factor to be applied to the change direction + factor = 1 + + # try the guess point and decrease "factor" until the error is less + # than the error from the last point + error2 = error + 1 + while error2 > error and factor > 0.01: + guess2 = [guess[i] + factor * direction[i] for i in range(2)] + + output, error2 = limit_comp_ecc.limit_comp( + col, guess2, target, axial_limits + ) + # if "limit_comp" resulted in a change in the guess of c, update + # the change factor to save calls to "try_axis" in the next + # iteration. Since "guess2" was passed to "limit_comp", it is + # already updated + if guess2[1] != guess[1] + factor * direction[1] and direction[1] != 0: + factor = (guess2[1] - guess[1]) / direction[1] + + count += 1 + factor *= 0.6 + + guess = guess2 + error = error2 + + # return the forces at the final trial point + Mx = output[4] * math.cos(output[0]) + My = output[4] * math.sin(output[0]) + + # it is possible that the point will be on the top plateau, so the + # axial force must be limited + P = min(axial_limits.max_phi_pn, output[3]) + return Mx, My, P, guess diff --git a/examples/conc_col_pmm/pmm_search/load_combo.py b/examples/conc_col_pmm/pmm_search/load_combo.py new file mode 100644 index 0000000..e5360cb --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/load_combo.py @@ -0,0 +1,15 @@ +import dataclasses + + +@dataclasses.dataclass +class LoadCombination: + id: int + p: float + mx: float + my: float + show_in_report: bool + + +def is_yes(show: str | float): + trimmed_yes = f"{show}".strip().lower() + return trimmed_yes == "yes" or trimmed_yes == "y" diff --git a/examples/conc_col_pmm/pmm_search/load_search/__init__.py b/examples/conc_col_pmm/pmm_search/load_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/pmm_search/load_search/bisect_load.py b/examples/conc_col_pmm/pmm_search/load_search/bisect_load.py new file mode 100644 index 0000000..27560d7 --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/load_search/bisect_load.py @@ -0,0 +1,89 @@ +import math + +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from .limit_comp_load import limit_comp +from .starting_pts import starting_pts + + +def bisect(col: Column, target, guess, axial_limits: AxialLimits): + # This function returns a point on the PMM diagram (Mx, My, and P) plus the + # two inputs that produced that point (theta and c) as a tuple. The point + # returned is intended to match the values in "target," which is a + # list containing the target lambda and target factored axial load in that + # order. "col" is the column being analyzed and "guess" contains the + # starting guess for c. In this function, c is the only input varied, + # and theta is held constant. This is because this function is intended + # for points on the PMM diagram where it is known that lambda=-theta, + # which occur on the positive and negative x and y axes. + + tol = 0.005 # normalized error accepted as the actual point + + depth = col.w * math.sin(target[0]) + col.h * math.cos(target[0]) # the distance + # between the section corners perpendicular to the neutral axis + + # set the two initial guess points + pts = starting_pts(col, guess, depth, target, axial_limits) + + error = float("inf") # normalized distance of the current point from the + # target + + guess = 0 # the guess for c at each iteration + change = 0 # the change in c between iterations + best_error = 10 # record for normalized error, initialize to a large number + best = [] # the point encountered so far with smallest normalized error + + # debug: + points = [] + points.append(pts[0]) + points.append(pts[1]) + + counter = 0 + while error > tol and counter < 50: + slope = (pts[1][3] - pts[0][3]) / (pts[1][1] - pts[0][1]) + + # calculate the distance of this output from its target + dist = target[1] - pts[1][3] + if slope != 0: # check slope to avoid dividing by zero + # move by the amount estimated to get to zero, minus a small + # reduction for stability + change = dist / slope + else: + change = 0 + + error = float("inf") + factor = 1 # factor for reducing the change between iterations + + # if the error increases after the first guess, the change should be + # reduced to try to improve the guess + while error > best_error and factor >= 0.1 and counter < 50: + # calculate the next guess based on the current point + guess = pts[1][1] + change * factor + factor /= 10 + # set the floor to avoid guesses that are zero or negative and to + # avoid setting the guess equal to the current point + guess_floor = 1e-6 if pts[1][1] != 1e-6 else 0.1 + guess = max(guess_floor, guess) + + # get the output for this guess, reducing c if the compression + # is too high to + output, error = limit_comp( + col, [-target[0]] + [guess], target, axial_limits + ) + counter += 1 + + # update the current and previous point + pts[0] = pts[1] + pts[1] = [-target[0]] + [guess] + [output[0]] + [output[3]] + points.append(pts[1]) + + # update the lowest error and the best point if this guess is a record + if error < best_error: + best_error = error + # the factored moments and axial force, plus the best guess inputs + Mx = output[4] * math.cos(output[0]) + My = output[4] * math.sin(output[0]) + P = output[3] + best = (Mx, My, P, pts[1][0], pts[1][1]) + + return best diff --git a/examples/conc_col_pmm/pmm_search/load_search/change_load.py b/examples/conc_col_pmm/pmm_search/load_search/change_load.py new file mode 100644 index 0000000..054e29b --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/load_search/change_load.py @@ -0,0 +1,36 @@ +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from ...struct_analysis import try_axis +from .get_error_load import get_error + +delta = 1e-8 # small change to be used for finite differences + + +def change(col: Column, guess, target, output, axial_limits: AxialLimits): + error = get_error(output, target, axial_limits.load_span) + # A small positive value "delta" is added to both inputs in order to test + # the effect on the results of "try_axis". It may have to be negative for + # theta to avoid exceeding 0. + delta0 = delta if guess[0] < -delta else -delta + + output2 = try_axis.try_axis(col, guess[0] + delta0, guess[1], axial_limits) + a = (output2[0] - output[0]) / delta0 + c = (output2[3] - output[3]) / delta0 + + output2 = try_axis.try_axis(col, guess[0], guess[1] + delta, axial_limits) + b = (output2[0] - output[0]) / delta + d = (output2[3] - output[3]) / delta + + e = target[0] - output[0] + f = target[1] - output[3] + + change = [0] * 2 + det = a * d - b * c + + # avoid divide by zero + if det != 0: + # set the planned changes in theta and c to try to reach the point at + # which lambda and load are both their target values + change[0] = (d * e - b * f) / det + change[1] = (a * f - c * e) / det + return change, error diff --git a/examples/conc_col_pmm/pmm_search/load_search/get_error_load.py b/examples/conc_col_pmm/pmm_search/load_search/get_error_load.py new file mode 100644 index 0000000..8b11c81 --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/load_search/get_error_load.py @@ -0,0 +1,8 @@ +import math + + +def get_error(output, target, load_span): + # find the difference between both outputs and their target values + lambda_diff = output[0] - target[0] + load_diff = output[3] - target[1] + return math.sqrt((lambda_diff / (math.pi / 2)) ** 2 + (load_diff / load_span) ** 2) diff --git a/examples/conc_col_pmm/pmm_search/load_search/limit_comp_load.py b/examples/conc_col_pmm/pmm_search/load_search/limit_comp_load.py new file mode 100644 index 0000000..0516a70 --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/load_search/limit_comp_load.py @@ -0,0 +1,28 @@ +import math + +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from ...struct_analysis import try_axis +from ..load_search.get_error_load import get_error + + +def limit_comp(col: Column, guess, target, axial_limits: AxialLimits): + guess[0] = min(0, max(guess[0], -math.pi / 2)) + guess[1] = max(1e-6, guess[1]) + # guess[1]=max(c_lims[0],max(c_lims[1],guess[1])) + output = try_axis.try_axis(col, guess[0], guess[1], axial_limits) + lim_factor = 0.999 + while output[3] > lim_factor * col.PHI_COMP * axial_limits.max_pn: + # the current phi_pn (without the 0.8) is at or almost at its + # maximum value, which means the column is probably in full + # compression, which must be avoided or derivatives will be zero + guess[1] /= 2 + output = try_axis.try_axis(col, guess[0], guess[1], axial_limits) + while output[3] < lim_factor * axial_limits.min_phi_pn: + # the current phi_pn is at or almost at its minimum value, so c must + # b increased + guess[1] *= 10 + output = try_axis.try_axis(col, guess[0], guess[1], axial_limits) + # update the distance from the target point + error = get_error(output, target, axial_limits.load_span) + return output, error diff --git a/examples/conc_col_pmm/pmm_search/load_search/point_search_load.py b/examples/conc_col_pmm/pmm_search/load_search/point_search_load.py new file mode 100644 index 0000000..3a199c7 --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/load_search/point_search_load.py @@ -0,0 +1,62 @@ +import math + +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from .change_load import change +from .limit_comp_load import limit_comp + +""" +Searches for a point on the PMM diagram with a certain axial load capacity and lambda. +Parameters: the column object, the target (as a tuple of lambda, then axial load), and the +initial guess (as a tuple of theta, then neutral axis depth) +Returns: Mx, My, P, and the two final iteration values for the final guess +*the angle of eccentricity guess must be between 0 and -pi/2 and the target lambda +must be between 0 and pi/2 +""" + + +def search(col: Column, target, guess, axial_limits: AxialLimits): + tol = 0.001 # error accepted as the actual point + + # get output for the initial guess point + output, error = limit_comp(col, guess, target, axial_limits) + + count = 1 # count of iterations (calls to "try_axis") + count_lim = 100 # iteration limit + + while error > tol and count < count_lim: + # get a descent direction for the current point + direction, error = change(col, guess, target, output, axial_limits) + + # since finding the direction requires two "try_axis" calls + count += 2 + + # the factor to be applied to the change direction + factor = 1 + + # try the guess point and decrease "factor" until the error is less + # than the error from the last point + error2 = error + 1 + while error2 > error and factor > 0.01: + guess2 = [guess[i] + factor * direction[i] for i in range(2)] + + output, error2 = limit_comp(col, guess2, target, axial_limits) + + # if "limit_comp" resulted in a change in the guess of c, update + # the change factor to save calls to "try_axis" in the next + # iteration. Since "guess2" was passed to "limit_comp", it is + # already updated + if guess2[1] != guess[1] + factor * direction[1] and direction[1] != 0: + factor = (guess2[1] - guess[1]) / direction[1] + + count += 1 + factor *= 0.6 + + guess = guess2 + error = error2 + + # return the forces at the final trial point + Mx = output[4] * math.cos(output[0]) + My = output[4] * math.sin(output[0]) + P = output[3] + return Mx, My, P, guess[0], guess[1] diff --git a/examples/conc_col_pmm/pmm_search/load_search/starting_pts.py b/examples/conc_col_pmm/pmm_search/load_search/starting_pts.py new file mode 100644 index 0000000..3f56d0d --- /dev/null +++ b/examples/conc_col_pmm/pmm_search/load_search/starting_pts.py @@ -0,0 +1,29 @@ +from ...col.axial_limits import AxialLimits +from ...col.column import Column +from .limit_comp_load import limit_comp + +reduction = 0.005 # the fraction of the total estimated span of both inputs +# that should be added/subtracted to the starting guess points + + +# Returns a list of two points which are the starting guesses for a gradient +# descent problem, where the two points are close together, centered on the +# supplied guess "guess," and different in both their theta and c values. +# "depth" is an estimate of the maximum c, and load_only is boolean, where +# True indicates that only the load should be varied +def starting_pts(col: Column, guess, depth, target, axial_limits: AxialLimits): + # calculate the starting differences in theta and c between two points + c_change = reduction * depth + change_factors = (-1, 1) # factors used to decrease parameters for A and to + # increase parameters for B + + pts = [guess.copy() for i in range(2)] + + for i, factor in enumerate(change_factors): + pts[i][1] += c_change * factor + guess[1] = max(1e-6, guess[1]) + + # calculate and store the load output for the current guess point + output, error = limit_comp(col, pts[i], target, axial_limits) + pts[i].extend([output[0], output[3]]) + return pts diff --git a/examples/conc_col_pmm/requirements.txt b/examples/conc_col_pmm/requirements.txt new file mode 100644 index 0000000..7c5f19b --- /dev/null +++ b/examples/conc_col_pmm/requirements.txt @@ -0,0 +1,5 @@ +pytest==8.0.2 +plotly==5.24.1 +efficalc +matplotlib +pandas \ No newline at end of file diff --git a/examples/conc_col_pmm/struct_analysis/__init__.py b/examples/conc_col_pmm/struct_analysis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/struct_analysis/triangles.py b/examples/conc_col_pmm/struct_analysis/triangles.py new file mode 100644 index 0000000..54195d0 --- /dev/null +++ b/examples/conc_col_pmm/struct_analysis/triangles.py @@ -0,0 +1,13 @@ +def triangle_area(pt_a, pt_b, pt_c): + return 0.5 * abs( + pt_a[0] * pt_b[1] + + pt_b[0] * pt_c[1] + + pt_c[0] * pt_a[1] + - pt_a[0] * pt_c[1] + - pt_b[0] * pt_a[1] + - pt_c[0] * pt_b[1] + ) + + +def triangle_centroid(pt_a, pt_b, pt_c): + return ((pt_a[0] + pt_b[0] + pt_c[0]) / 3, (pt_a[1] + pt_b[1] + pt_c[1]) / 3) diff --git a/examples/conc_col_pmm/struct_analysis/try_axis.py b/examples/conc_col_pmm/struct_analysis/try_axis.py new file mode 100644 index 0000000..d784af2 --- /dev/null +++ b/examples/conc_col_pmm/struct_analysis/try_axis.py @@ -0,0 +1,157 @@ +import math +import random + +from ..col.axial_limits import AxialLimits +from ..col.column import Column +from .triangles import * + +PHI_FLEXURE = 0.9 # safety factor for flexure-controlled column +COMP_FACTOR = 0.8 # additional reduction factor for axial compression + + +def try_axis(col: Column, theta, c, axial_limits: AxialLimits): + # this function returns the lambda, eccentricity, pn, phi_pn, and phi_mn + # from particular neutral axis angle and neutral axis depth c. The neutral + # axis angle must be between -90 degrees and 0 degrees, inclusive, and the + # neutral axis depth must be greater than or equal to 0 + + if c == 0: + return 0, 0, axial_limits.min_pn, axial_limits.min_phi_pn, 0 + a = col.beta1 * c + epsilon = 1e-11 # acceptable error for considering the neutral axis to + # be vertical or horizontal + red_fc = 0.85 * col.fc / 1000 # reduced f'c for concrete compression limit, ksi + pn = 0 # total axial force, kips (positive is compression) + mn = [0, 0] # Mnx, then Mny, kip-in (positive is compression to the right/top) + + intersects = [False] * 4 # whether line of concrete compression intersects the + # left, top, right, and bottom edges of the section + if theta > -math.pi / 2 + epsilon: + left_y = col.half_h - a / math.cos(theta) - col.w * math.tan(theta) + intersects[0] = -col.half_h < left_y < col.half_h + + right_y = col.half_h - a / math.cos(theta) + intersects[2] = -col.half_h < right_y < col.half_h + + if theta < -epsilon: + top_x = col.half_w + a / math.sin(theta) + intersects[1] = -col.half_w < top_x < col.half_w + + # if theta is -90, take the tan of 0 and get a change + # of zero, and if theta is -45, take the tan of 45 + # and get somewhat of an increase in x + bot_x = col.half_w + a / math.sin(theta) + col.h * math.tan(math.pi / 2 + theta) + intersects[3] = -col.half_w < bot_x < col.half_w + + if not any(intersects): # the whole concrete section is in compression + pn += red_fc * col.area + else: + + def add_axial_moment(pt_a, pt_b, pt_c): + nonlocal pn + tri_area = triangle_area(pt_a, pt_b, pt_c) + pn += tri_area * red_fc + centr = triangle_centroid(pt_a, pt_b, pt_c) + for i in range(2): + mn[i] += tri_area * red_fc * centr[1 - i] + + pt1 = (-col.half_w, left_y) if intersects[0] else (top_x, col.half_h) + pt2 = (bot_x, -col.half_h) if intersects[3] else (col.half_w, right_y) + + add_axial_moment(pt1, pt2, col.top_right) # compression triangle to + # top right corner + if intersects[0]: # there is a compression triangle to top left corner + add_axial_moment(col.top_left, col.top_right, pt1) + if intersects[3]: # there is a compression triangle to bot right corner + add_axial_moment(col.bot_right, col.top_right, pt2) + + strain_per_in = -col.concrete_strain_input.get_value() / c + steel_max_strain = 0 # value to keep record of greatest steel tensile strain + + # calculate a normal vector rotated 90 degrees from the neutral axis angle + normal = (math.cos(theta + math.pi / 2), math.sin(theta + math.pi / 2)) + + def add_bar(coords): + nonlocal steel_max_strain + nonlocal pn + # "offset" is the distance from the center of the bar to the line + # passing through the top right corner of the section and parallel to + # the neutral axis + offset = normal[0] * (col.half_w - coords[0]) + normal[1] * ( + col.half_h - coords[1] + ) + strain = (c - offset) * strain_per_in + steel_max_strain = max(steel_max_strain, strain) + + stress = strain * col.steel_modulus_input.get_value() + stress = max(-col.fy, min(col.fy, stress)) + if a > offset: + # this means this bar is within the compression range, + # so subtract the stress in the concrete, the stress + # will be negative in this case, so add to it + stress += red_fc + # since negative strain and negative stress are defined as + # compression for rebar but compression is positive in the conc. + # the sign of everything needs to be changed + force = col.bar_area * stress + pn -= force + for i in range(2): + mn[i] -= force * coords[1 - i] + + right_bar_x = col.half_w - col.edge_to_bar_center # x coordinate of bars on the + # right edge + y = col.y_start + # iterate over the bars along the left and right lines + # (this includes corner bars) + for i in range(col.bars_y): + for x in (-right_bar_x, right_bar_x): + coords = (x, y) + add_bar(coords) + y += col.y_space + + top_bar_y = col.half_h - col.edge_to_bar_center # y coordinate of bars on the + # top edge + x = col.x_start + for i in range(col.bars_x - 2): + # iterate over the bars along the top and bottom lines, and add the + # force for each one + for y in (-top_bar_y, top_bar_y): + coords = (x, y) + add_bar(coords) + x += col.x_space + + lambda1 = math.atan2(mn[1], mn[0]) # atan2 takes y,x, and we want mny at top + + # if Mx and My are both zero, the angle isn't defined, so return a random + # number to help avoid divide by zero errors + if not any(mn): + lambda1 = random.uniform(0, math.pi / 2) + mn_xy = math.sqrt(mn[0] ** 2 + mn[1] ** 2) / 12 # the moment resultant in kip-ft + + # calculate the factor of safety depending on the maximum steel strain + phi = min( + PHI_FLEXURE, + max( + col.PHI_COMP, + col.PHI_COMP + + (PHI_FLEXURE - col.PHI_COMP) + * (steel_max_strain - col.steel_yield) + / col.concrete_strain_input.get_value(), + ), + ) + # the following values ignore the limit on phi_pn to help convergence near that value + # (so there will still be derivatives above that value) + phi_pn_not_limited = phi * pn + phi_mn_xy = phi * mn_xy + + # the eccentricity does include the limit on phi_pn, and it is reported as + # angle from the Mx-My axis for numerical stability. + phi_pn = min(phi_pn_not_limited, axial_limits.max_phi_pn) + ecc = math.atan2( + phi_pn, phi_mn_xy + ) # the eccentricity as an angle above the M-M plane + + # returning the angle of eccentricity, the eccentricity as an angle from Mx-My plane, the nominal + # axial capacity in kip, the factored axial capacity in kip, and the + # factored moment capacity in kip-ft + return lambda1, ecc, pn, phi_pn_not_limited, phi_mn_xy diff --git a/examples/conc_col_pmm/tests/__init__.py b/examples/conc_col_pmm/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/tests/conftest.py b/examples/conc_col_pmm/tests/conftest.py new file mode 100644 index 0000000..46ea9e3 --- /dev/null +++ b/examples/conc_col_pmm/tests/conftest.py @@ -0,0 +1,93 @@ +import pytest + +from efficalc import Calculation, Input + +from ..col.column import Column +from ..constants.concrete_data import MAX_CONCRETE_STRAIN +from ..constants.rebar_data import STEEL_E, BarSize, rebar_area +from ..pmm_search.load_combo import LoadCombination + + +def getCalculatedColumnProps(bar_size: BarSize): + A_b = Calculation("Ab", rebar_area(bar_size)) + + E_s = Calculation("E_s", STEEL_E) + e_c = Calculation( + "\\epsilon_u", + MAX_CONCRETE_STRAIN, + ) + + return {"A_b": A_b, "E_s": E_s, "e_c": e_c} + + +@pytest.fixture +def example_col(): + bar_size: BarSize = "#5" + calc_props = getCalculatedColumnProps(bar_size) + + return Column( + Input("w", 24), + Input("h", 18), + Input("bar_size", bar_size), + Input("cover", 1.5), + Input("nx", 5), + Input("ny", 4), + Input("f'_c", 8000), + Input("f_y", 80), + False, + False, + calc_props["A_b"], + calc_props["E_s"], + calc_props["e_c"], + ) + + +@pytest.fixture +def loads(): + # for each load case: P, Mx, My, and whether the calc should be shown + loads = [[300, 100, 200, True], [-100, 50, -60, False], [11500, 300, -300, False]] + return [LoadCombination(i + 1, *load) for i, load in enumerate(loads)] + + +@pytest.fixture +def example_col2(): + bar_size: BarSize = "#8" + calc_props = getCalculatedColumnProps(bar_size) + + return Column( + Input("w", 16), + Input("h", 20), + Input("bar_size", bar_size), + Input("cover", 2.5), + Input("nx", 3), + Input("ny", 4), + Input("f'_c", 6000), + Input("f_y", 60), + True, + False, + calc_props["A_b"], + calc_props["E_s"], + calc_props["e_c"], + ) + + +@pytest.fixture +def example_col3(): + bar_size: BarSize = "#8" + calc_props = getCalculatedColumnProps(bar_size) + + return Column( + Input("w", 24), + Input("h", 36), + Input("bar_size", bar_size), + Input("cover", 1.5), + Input("nx", 3), + Input("ny", 4), + Input("f'_c", 4000), + Input("f_y", 40), + False, + False, + calc_props["A_b"], + calc_props["E_s"], + calc_props["e_c"], + ) diff --git a/examples/conc_col_pmm/tests/test_calculation.py b/examples/conc_col_pmm/tests/test_calculation.py new file mode 100644 index 0000000..87885bb --- /dev/null +++ b/examples/conc_col_pmm/tests/test_calculation.py @@ -0,0 +1,88 @@ +import matplotlib +import pytest + +from efficalc import Calculation, clear_saved_objects +from efficalc.calculation_runner import CalculationRunner + +from ..calc_document.calculation import calculation +from ..calc_document.column_inputs import ColumnInputs + +matplotlib.use("Agg") # Use a non-interactive backend + + +@pytest.fixture +def common_setup_teardown(): + # Set up a sample number + yield None # Provide the data to the test + # Teardown: Clean up resources (if any) after the test + clear_saved_objects() + + +def get_calc_by_name(all_items, name): + for item in all_items: + if isinstance(item, Calculation) and item.name == name: + return item + + +def assert_calc_value(calc: Calculation, expected: float): + assert calc.result() == pytest.approx(expected, abs=0.001) + + +def test_calc_with_defaults(common_setup_teardown): + runner = CalculationRunner(calculation) + all_obj = runner.calculate_all_items() + + ppn = get_calc_by_name(all_obj, "{\\phi}P_n") + pmx = get_calc_by_name(all_obj, "{\\phi}M_{nx}") + pmy = get_calc_by_name(all_obj, "{\\phi}M_{ny}") + dcr_mx = get_calc_by_name(all_obj, "DCR_{Mx}") + dcr_my = get_calc_by_name(all_obj, "DCR_{My}") + dcr_p = get_calc_by_name(all_obj, "DCR_{P}") + + assert_calc_value(ppn, 3579.613) + assert_calc_value(pmx, 238.932) + assert_calc_value(pmy, 119.412) + assert_calc_value(dcr_mx, 0.837060) + assert_calc_value(dcr_my, 0.837060) + assert_calc_value(dcr_p, 0.838079) + + +def test_calc_with_custom_load_case(common_setup_teardown): + loads = [[500, 400, 50, "yes"]] + runner = CalculationRunner(lambda: calculation(default_loads=loads)) + all_obj = runner.calculate_all_items() + + ppn = get_calc_by_name(all_obj, "{\\phi}P_n") + pmx = get_calc_by_name(all_obj, "{\\phi}M_{nx}") + pmy = get_calc_by_name(all_obj, "{\\phi}M_{ny}") + dcr_mx = get_calc_by_name(all_obj, "DCR_{Mx}") + dcr_my = get_calc_by_name(all_obj, "DCR_{My}") + dcr_p = get_calc_by_name(all_obj, "DCR_{P}") + + assert_calc_value(ppn, 2140.047) + assert_calc_value(pmx, 1712.806) + assert_calc_value(pmy, 214.404) + assert_calc_value(dcr_mx, 0.23353) + assert_calc_value(dcr_my, 0.23320) + assert_calc_value(dcr_p, 0.23364) + + +def test_calc_with_small_column(common_setup_teardown): + loads = [[18.22, 1.56, 3.03, "yes"]] + col = ColumnInputs(4, 6, "#4", 1, 2, 3, 4000, 40, True, True) + runner = CalculationRunner(lambda: calculation(default_loads=loads, col=col)) + all_obj = runner.calculate_all_items() + + ppn = get_calc_by_name(all_obj, "{\\phi}P_n") + pmx = get_calc_by_name(all_obj, "{\\phi}M_{nx}") + pmy = get_calc_by_name(all_obj, "{\\phi}M_{ny}") + dcr_mx = get_calc_by_name(all_obj, "DCR_{Mx}") + dcr_my = get_calc_by_name(all_obj, "DCR_{My}") + dcr_p = get_calc_by_name(all_obj, "DCR_{P}") + + assert_calc_value(ppn, 26.6350) + assert_calc_value(pmx, 2.28086) + assert_calc_value(pmy, 4.43000) + assert_calc_value(dcr_mx, 0.68395) + assert_calc_value(dcr_my, 0.68397) + assert_calc_value(dcr_p, 0.68406) diff --git a/examples/conc_col_pmm/tests/test_get_capacity.py b/examples/conc_col_pmm/tests/test_get_capacity.py new file mode 100644 index 0000000..45799d0 --- /dev/null +++ b/examples/conc_col_pmm/tests/test_get_capacity.py @@ -0,0 +1,14 @@ +from ..calc_document.plotting import get_capacity, pmm_mesh +from ..col.assign_max_min import calculate_axial_load_limits + + +def test_get_capacity(example_col, loads): + + col = example_col + axial_limits = calculate_axial_load_limits(col) + + # Retrieve the quarter PMM mesh, which has points + # in the format (Mx, My, P). + _, _, _, mesh = pmm_mesh.get_mesh(col, 48, 18, axial_limits) + + _ = get_capacity.get_capacity(mesh, loads[0]) diff --git a/examples/conc_col_pmm/tests/test_get_dcr_ecc.py b/examples/conc_col_pmm/tests/test_get_dcr_ecc.py new file mode 100644 index 0000000..106d39d --- /dev/null +++ b/examples/conc_col_pmm/tests/test_get_dcr_ecc.py @@ -0,0 +1,187 @@ +from ..col.assign_max_min import calculate_axial_load_limits +from ..pmm_search.ecc_search.get_dcr_ecc import get_dcr_ecc +from ..pmm_search.load_combo import LoadCombination + +""" +This test uses a set of load points and a given column as well as +reference values for the DCRs for those load points to check the +accuracy of the DCRs from this program. +""" +loads = [ + [-300, 50, 0], + [-300, 1000, 0], + [-200, 1000, 0], + [-100, 1000, 0], + [0, 1000, 0], + [100, 1000, 0], + [200, 1000, 0], + [300, 1000, 0], + [400, 1000, 0], + [500, 1000, 0], + [600, 1000, 0], + [700, 1000, 0], + [800, 1000, 0], + [900, 1000, 0], + [1000, 1000, 0], + [1100, 1000, 0], + [1200, 1000, 0], + [1300, 1000, 0], + [1400, 1000, 0], + [1500, 1000, 0], + [1600, 1000, 0], + [1700, 1000, 0], + [1800, 1000, 0], + [1900, 1000, 0], + [2000, 1000, 0], + [2100, 1000, 0], + [2200, 1000, 0], + [2200, 50, 0], + [-300, -50, 0], + [-300, -1000, 0], + [-200, -1000, 0], + [-100, -1000, 0], + [0, -1000, 0], + [100, -1000, 0], + [200, -1000, 0], + [300, -1000, 0], + [400, -1000, 0], + [500, -1000, 0], + [600, -1000, 0], + [700, -1000, 0], + [800, -1000, 0], + [900, -1000, 0], + [1000, -1000, 0], + [1100, -1000, 0], + [1200, -1000, 0], + [1300, -1000, 0], + [1400, -1000, 0], + [1500, -1000, 0], + [1600, -1000, 0], + [1700, -1000, 0], + [1800, -1000, 0], + [1900, -1000, 0], + [2000, -1000, 0], + [2100, -1000, 0], + [2200, -1000, 0], + [2200, -50, 0], + [-300, 0, 50], + [-300, 0, 1000], + [-200, 0, 1000], + [-100, 0, 1000], + [0, 0, 1000], + [100, 0, 1000], + [200, 0, 1000], + [300, 0, 1000], + [400, 0, 1000], + [500, 0, 1000], + [600, 0, 1000], + [700, 0, 1000], + [800, 0, 1000], + [900, 0, 1000], + [1000, 0, 1000], + [1100, 0, 1000], + [1200, 0, 1000], + [1300, 0, 1000], + [1400, 0, 1000], + [1500, 0, 1000], + [1600, 0, 1000], + [2000, 0, 0], +] + +dcrs = [ + 1.18111455, + 3.5800972, + 3.24327159, + 2.90662146, + 2.56997156, + 2.23332143, + 1.89667141, + 1.60175514, + 1.34322953, + 1.148969, + 1.01194346, + 0.911275268, + 0.9276003, + 0.9705417, + 1.01678479, + 1.06635, + 1.11907816, + 1.15417635, + 1.184222, + 1.217052, + 1.25151849, + 1.28756726, + 1.324892, + 1.36372554, + 1.40255916, + 1.44369924, + 1.48486662, + 1.31116092, + 1.18111455, + 3.5800972, + 3.24327159, + 2.90662146, + 2.56997156, + 2.23332119, + 1.896671, + 1.601755, + 1.34322941, + 1.148969, + 1.01194358, + 0.9112752, + 0.9276003, + 0.9705417, + 1.01678479, + 1.06635, + 1.11907816, + 1.15417635, + 1.184222, + 1.217052, + 1.25151849, + 1.28756726, + 1.324892, + 1.36372554, + 1.40255916, + 1.44369924, + 1.48486662, + 1.31116092, + 1.23884523, + 4.9428153, + 4.608767, + 4.274719, + 3.94067168, + 3.60662413, + 3.27257633, + 2.93852878, + 2.62903023, + 2.32137275, + 2.022534, + 1.78518534, + 1.59556031, + 1.4507966, + 1.35867274, + 1.35752714, + 1.39450872, + 1.44129765, + 1.49240327, + 1.54428363, + 1.59789062, + 1.19196439, +] + +# the error tolerance for this test +tol = 1e-2 + + +def test_get_dcr(example_col3): + col = example_col3 + axial_limits = calculate_axial_load_limits(col) + + for i in range(78): + load = LoadCombination(i + 1, *(loads[i]), False) + dcr = get_dcr_ecc(col, load, axial_limits) + + if dcr > 0: + assert abs((dcr - dcrs[i]) / dcr) < tol + else: + assert dcrs[0] < tol diff --git a/examples/conc_col_pmm/tests/test_pmm_plotter_plotly.py b/examples/conc_col_pmm/tests/test_pmm_plotter_plotly.py new file mode 100644 index 0000000..814b3f0 --- /dev/null +++ b/examples/conc_col_pmm/tests/test_pmm_plotter_plotly.py @@ -0,0 +1,17 @@ +from ..calc_document.calculation import calculation +from ..calc_document.plotting import get_pmm_data, pmm_plotter_plotly +from ..col import assign_max_min + + +# This test checks for runtime errors +def test_pmm_plotter_plotly(example_col): + # for each load case: P, Mx, My, and whether the calc should be shown + loads = [ + [300, 100, 200, "yes"], + [-100, 50, -60, "no"], + [1500, 300, -300, "no"], + ] + + pmm_data = calculation(default_loads=loads, col=example_col) + + _ = pmm_plotter_plotly.plot(pmm_data) diff --git a/examples/conc_col_pmm/tests/test_point_plotter.py b/examples/conc_col_pmm/tests/test_point_plotter.py new file mode 100644 index 0000000..78f1dc1 --- /dev/null +++ b/examples/conc_col_pmm/tests/test_point_plotter.py @@ -0,0 +1,13 @@ +from ..calc_document.plotting import get_capacity, pmm_mesh, point_plotter +from ..col.assign_max_min import calculate_axial_load_limits + +# This test checks for runtime errors + + +def test_point_plotter(example_col3, loads): + + axial_limits = calculate_axial_load_limits(example_col3) + _, _, _, mesh = pmm_mesh.get_mesh(example_col3, 36, 12, axial_limits) + + capacity = get_capacity.get_capacity(mesh, loads[0]) + _ = point_plotter.plot(capacity, loads[0], False) diff --git a/examples/conc_col_pmm/tests/test_point_search_ecc.py b/examples/conc_col_pmm/tests/test_point_search_ecc.py new file mode 100644 index 0000000..5197a82 --- /dev/null +++ b/examples/conc_col_pmm/tests/test_point_search_ecc.py @@ -0,0 +1,34 @@ +import math + +from ..col.assign_max_min import calculate_axial_load_limits +from ..pmm_search.ecc_search.point_search_ecc import search + +search_tol = 1.5e-3 # the tolerance for error in the points found +ceil_tol = 1e-2 # how close to limits to go + +""" +The function below chooses an arbitrary initial guess and then +for a range of target points (where the target lambda and load +are both allowed to vary) checks whether the eccentricity search +algorithm converges. +""" + + +def test_search(example_col): + axial_limits = calculate_axial_load_limits(example_col) + guess = [-0.7, 30] + lambda_change = math.pi / 12 + ecc_change = math.pi / 12 + lambda_target = 0 + while lambda_target < math.pi / 2 - ceil_tol: + ecc_target = -math.pi / 2 + ecc_change + while ecc_target < math.pi / 2 - ceil_tol: + target = [lambda_target, ecc_target] + Mx, My, P, guess = search(example_col, target, guess, axial_limits) + lambda_found = math.atan2(My, Mx) + Mxy_found = math.sqrt(Mx**2 + My**2) + ecc_found = math.atan2(P, Mxy_found) + assert abs(lambda_found - lambda_target) < search_tol + assert abs(ecc_found - ecc_target) < search_tol + ecc_target += ecc_change + lambda_target += lambda_change diff --git a/examples/conc_col_pmm/tests/test_point_search_load.py b/examples/conc_col_pmm/tests/test_point_search_load.py new file mode 100644 index 0000000..5e4048d --- /dev/null +++ b/examples/conc_col_pmm/tests/test_point_search_load.py @@ -0,0 +1,32 @@ +import math + +from ..col.assign_max_min import calculate_axial_load_limits +from ..pmm_search.load_search.point_search_load import search + +search_tol = 1.5e-3 # the tolerance for error in the points found +ceil_tol = 1e-2 # how close to limits to go + +""" +The function below chooses an arbitrary initial guess and then +for a range of target points (where the target lambda and load +are both allowed to vary) checks whether the load search +algorithm converges. +""" + + +def test_search(example_col): + axial_limits = calculate_axial_load_limits(example_col) + guess = [-0.7, 30] + lambda_change = math.pi / 12 + load_change = axial_limits.load_span / 10 + lambda_target = 0 + while lambda_target < math.pi / 2 - ceil_tol: + load_target = axial_limits.min_phi_pn + load_change + while load_target < axial_limits.max_phi_pn - ceil_tol: + target = [lambda_target, load_target] + Mx, My, P, _, _ = search(example_col, target, guess, axial_limits) + lambda_found = math.atan2(My, Mx) + assert abs(lambda_found - lambda_target) < search_tol + assert (abs(P - load_target) / axial_limits.load_span) < search_tol + load_target += load_change + lambda_target += lambda_change diff --git a/examples/conc_col_pmm/tests/test_triangles.py b/examples/conc_col_pmm/tests/test_triangles.py new file mode 100644 index 0000000..048e89f --- /dev/null +++ b/examples/conc_col_pmm/tests/test_triangles.py @@ -0,0 +1,11 @@ +from ..struct_analysis.triangles import * + +tri1 = ((0, 0), (3, 0), (0, 6)) + + +def test_triangle_area(): + assert triangle_area(tri1[0], tri1[1], tri1[2]) == 9 + + +def test_triangle_centroid(): + assert triangle_centroid(tri1[0], tri1[1], tri1[2]) == (1, 2) diff --git a/examples/conc_col_pmm/tests/test_try_axis.py b/examples/conc_col_pmm/tests/test_try_axis.py new file mode 100644 index 0000000..11d8f84 --- /dev/null +++ b/examples/conc_col_pmm/tests/test_try_axis.py @@ -0,0 +1,33 @@ +import math + +from ..col.assign_max_min import calculate_axial_load_limits +from ..struct_analysis.try_axis import try_axis + +# this test is based on the reference calculation by SP Column which can be found at the following link: +# https://structurepoint.org/publication/pdf/Biaxial-Bending-Interaction-Diagrams-for-Rectangular-Reinforced-Concrete-Column-Design-ACI-318-19.pdf +# page 11 of the page above indicates the values of theta and c being tried as well as the values +# for factored Mx, My, and P using the exact capacity method. + +# The reference P, Mx, My are below, these are factored +reference_forces = (283.72, 214.29, 133.83) + +# the error tolerance for this test +tol = 1e-3 + + +def test_try_axis(example_col2): + axial_limits = calculate_axial_load_limits(example_col2) + + (lambda1, ecc, pn, phi_pn_not_limited, phi_mn_xy) = try_axis( + example_col2, -43.9 * math.pi / 180, 12.5, axial_limits + ) + phi_pn = min(axial_limits.max_phi_pn, phi_pn_not_limited) + + found = ( + phi_pn, + phi_mn_xy * math.cos(lambda1), + phi_mn_xy * math.sin(lambda1), + ) + for i in range(3): + ref = reference_forces[i] + assert abs((found[i] - ref) / ref) < tol diff --git a/examples/conc_col_pmm/tests/test_try_axis_document.py b/examples/conc_col_pmm/tests/test_try_axis_document.py new file mode 100644 index 0000000..82b3866 --- /dev/null +++ b/examples/conc_col_pmm/tests/test_try_axis_document.py @@ -0,0 +1,31 @@ +import math + +from ..calc_document.try_axis_document import try_axis_document +from ..col.assign_max_min import calculate_axial_load_limits + +# this test is based on the reference calculation by SP Column which can be found at the following link: +# https://structurepoint.org/publication/pdf/Biaxial-Bending-Interaction-Diagrams-for-Rectangular-Reinforced-Concrete-Column-Design-ACI-318-19.pdf +# page 11 of the page above indicates the values of theta and c being tried as well as the values +# for factored Mx, My, and P using the exact capacity method. + +# The reference P, Mx, My are below, these are factored +reference_forces = (283.72, 214.29, 133.83) + +# the error tolerance for this test +tol = 1e-3 + + +def test_try_axis(example_col2): + axial_limits = calculate_axial_load_limits(example_col2) + capacities = try_axis_document( + example_col2, axial_limits, -43.9 * math.pi / 180, 12.5 + ) + found = ( + capacities.P.result(), + capacities.Mx.result(), + capacities.My.result(), + ) + print("found", found) + for i in range(3): + ref = reference_forces[i] + assert abs((found[i] - ref) / ref) < tol diff --git a/examples/conc_col_pmm/tests/visual_tests/__init__.py b/examples/conc_col_pmm/tests/visual_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/conc_col_pmm/tests/visual_tests/visual_test_calculation_report.py b/examples/conc_col_pmm/tests/visual_tests/visual_test_calculation_report.py new file mode 100644 index 0000000..57be8f7 --- /dev/null +++ b/examples/conc_col_pmm/tests/visual_tests/visual_test_calculation_report.py @@ -0,0 +1,25 @@ + +# Now you can import from conc_col_pmm +from ...calc_document.document_wrapper import run + +# "w", "h", "bar_size", "bar_cover", "bars_x", "bars_y", "fc", "fy", "cover_type", "transverse_type", +col_data = [18, 24, "#6", 2, 5, 2, 8000, 60, "Edge", "Tied"] + +# for each load case: P, Mx, My, and whether the calc should be shown +# Note that these load cases currently do not override the defaults +loads = [ + [500, 200, 100, "yes"], + [-100, 50, -60, "no"], + [11500, 300, -300, "no"], +] + +# calc_report_example1 +# col_data = [24, 18, "#6", 1.5, 5, 4, 8000, 60, "Edge", "Tied"] +# loads = [[1400, -300, 100, True]] + +# calc_report_example2 +# col_data = [24, 36, "#8", 2, 6, 8, 8000, 60, "Edge", "Tied"] +# loads = [[3000, -200, 100, True]] + +if __name__ == "__main__": + run(True, col_data, loads) diff --git a/examples/conc_col_pmm/tests/visual_tests/visual_test_pmm.py b/examples/conc_col_pmm/tests/visual_tests/visual_test_pmm.py new file mode 100644 index 0000000..edeafff --- /dev/null +++ b/examples/conc_col_pmm/tests/visual_tests/visual_test_pmm.py @@ -0,0 +1,47 @@ +from efficalc import Input + +from ...calc_document.calculation import calculation +from ...calc_document.plotting import pmm_plotter_plotly +from ...col.column import Column +from ...constants.rebar_data import BarSize +from ...tests.conftest import getCalculatedColumnProps + +# TODO: make this use the main calc callsite and get the plotly data from there + + +def example_col(): + bar_size: BarSize = "#6" + calc_props = getCalculatedColumnProps(bar_size) + + return Column( + Input("w", 24), + Input("h", 18), + Input("bar_size", bar_size), + Input("cover", 2), + Input("nx", 5), + Input("ny", 2), + Input("f'_c", 8000), + Input("f_y", 60), + False, + True, + calc_props["A_b"], + calc_props["E_s"], + calc_props["e_c"], + ) + + +if __name__ == "__main__": + col = example_col() + + # for each load case: P, Mx, My, and whether the calc should be shown + load_data = [ + [300, 100, 200, "yes"], + [-100, 50, -60, "no"], + [1500, 300, -300, "no"], + ] + + pmm_data = calculation(default_loads=load_data, col=col) + + fig = pmm_plotter_plotly.plot(pmm_data) + + fig.show() diff --git a/examples/conc_col_pmm/tests/visual_tests/visual_test_point_plotter.py b/examples/conc_col_pmm/tests/visual_tests/visual_test_point_plotter.py new file mode 100644 index 0000000..4704d0a --- /dev/null +++ b/examples/conc_col_pmm/tests/visual_tests/visual_test_point_plotter.py @@ -0,0 +1,53 @@ +import matplotlib.pyplot as plt + +from efficalc import Input + +from ...calc_document.plotting import get_capacity, pmm_mesh, point_plotter +from ...col import assign_max_min +from ...col.column import Column +from ...constants.rebar_data import BarSize +from ...pmm_search.load_combo import LoadCombination +from ...tests.conftest import getCalculatedColumnProps + + +def example_col(): + bar_size: BarSize = "#5" + calc_props = getCalculatedColumnProps(bar_size) + + return Column( + Input("w", 24), + Input("h", 18), + Input("bar_size", bar_size), + Input("cover", 1.5), + Input("nx", 5), + Input("ny", 4), + Input("f'_c", 8000), + Input("f_y", 80), + False, + False, + calc_props["A_b"], + calc_props["E_s"], + calc_props["e_c"], + ) + + +if __name__ == "__main__": + col = example_col() + axial_limits = assign_max_min.calculate_axial_load_limits(col) + + # for each load case: P, Mx, My, and whether the calc should be shown + load_data = [ + [300, 100, 200, True], + [-100, 50, -60, False], + [11500, 300, -300, False], + ] + loads = [LoadCombination(i, *load) for i, load in enumerate(load_data)] + + _, _, _, mesh = pmm_mesh.get_mesh(col, 36, 12, axial_limits) + + capacity = get_capacity.get_capacity(mesh, loads[0]) + fig = point_plotter.plot(capacity, loads[0], False) + + plt.show() + + # plt.savefig("test_plot.png") diff --git a/pyproject.toml b/pyproject.toml index 36d5e99..dc7ef65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "efficalc" -version = "1.2.0" +version = "1.2.6" authors = [ { name="Andrew Young", email="youandvern@gmail.com" }, ] @@ -28,7 +28,7 @@ Documentation = "https://youandvern.github.io/efficalc" Issues = "https://github.com/youandvern/efficalc/issues" [tool.setuptools.packages] -find = { include = ["efficalc", "efficalc.sections", "efficalc.base_definitions"] } +find = { include = ["efficalc", "efficalc.sections", "efficalc.base_definitions", "efficalc.canvas"] } [tool.setuptools] package-data = {"efficalc.sections" = ["section_properties.db"]} diff --git a/requirements.txt b/requirements.txt index ce76d93..1a522ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ latexexpr_efficalc==0.5.3 pylatexenc==2.10 -pytest==8.0.2 +pytest==8.0.2 \ No newline at end of file diff --git a/tests/base_definitions/test_calculation.py b/tests/base_definitions/test_calculation.py index 7fee41f..ada4649 100644 --- a/tests/base_definitions/test_calculation.py +++ b/tests/base_definitions/test_calculation.py @@ -169,6 +169,16 @@ def test_estimate_display_length_number(common_setup_teardown): assert a.estimate_display_length() == CalculationLength.NUMBER +def test_estimate_display_length_negative_number(common_setup_teardown): + a = Calculation("a", -5.254, "mm^2") + assert a.estimate_display_length() == CalculationLength.NUMBER + + +def test_estimate_display_length_large_negative_number(common_setup_teardown): + a = Calculation("a", -107412.254, "mm^2") + assert a.estimate_display_length() == CalculationLength.NUMBER + + def test_estimate_display_length_short(common_setup_teardown): a = Input("a", 2, "mm") b = Input("b", 7, "mm") diff --git a/tests/base_definitions/test_input.py b/tests/base_definitions/test_input.py index 30b4155..1e1ddb3 100644 --- a/tests/base_definitions/test_input.py +++ b/tests/base_definitions/test_input.py @@ -65,6 +65,11 @@ def test_to_str_nan(common_setup_teardown): assert str(a) == "\mathrm{a} = \mathrm{test} \ mm^2" +def test_to_str_plain_text(common_setup_teardown): + a = Input("a", "test", "mm^2", plain_text_value=True) + assert str(a) == r"\mathrm{a} = \mathrm{\text{test}} \ mm^2" + + def test_to_str_number(common_setup_teardown): a = Input("a", -5, "mm^2") assert str(a) == r"a = \left( -5 \right) \ \mathrm{mm^2}" diff --git a/tests/base_definitions/test_table.py b/tests/base_definitions/test_table.py new file mode 100644 index 0000000..8a95d39 --- /dev/null +++ b/tests/base_definitions/test_table.py @@ -0,0 +1,107 @@ +import pytest + +from efficalc import ( + InputTable, + Table, + clear_all_input_default_overrides, + clear_saved_objects, + get_all_calc_objects, + set_input_default_overrides, +) + + +@pytest.fixture +def common_setup_teardown(): + yield 5 # Provide data to the test + # Teardown: Clean up resources (if any) after the test + clear_all_input_default_overrides() + clear_saved_objects() + + +def test_default_values(common_setup_teardown): + a = Table([[1, 2, 3, 4], [5, 6, 7, 8]]) + assert a.data == [[1, 2, 3, 4], [5, 6, 7, 8]] + assert a.headers is None + assert a.title is None + assert a.striped is False + assert a.full_width is False + assert a.result_check is False + assert a.numbered_rows is False + + +def test_set_values(common_setup_teardown): + a = Table( + [[1, 2, 3, 4], [5, 6, 7, 8]], + headers=["a", "b", "c", "d"], + title="my table", + striped=True, + full_width=False, + result_check=True, + numbered_rows=True, + ) + assert a.data == [[1, 2, 3, 4], [5, 6, 7, 8]] + assert a.headers == ["a", "b", "c", "d"] + assert a.title == "my table" + assert a.striped is True + assert a.full_width is False + assert a.result_check is True + assert a.numbered_rows is True + + +def test_save_calc_item(common_setup_teardown): + b = Table([[1, 2, 3, 4], [5, 6, 7, 8]], headers=["a", "b"]) + saved_items = get_all_calc_objects() + assert len(saved_items) == 1 + assert saved_items[0] == b + + +def test_input_table_save_calc_item(common_setup_teardown): + b = InputTable([[1, 2, 3, 4], [5, 6, 7, 8]], headers=["a", "b"]) + saved_items = get_all_calc_objects() + assert len(saved_items) == 1 + assert saved_items[0] == b + + +def test_input_table_set_values(common_setup_teardown): + a = InputTable( + [[1, 2, 3, 4], [5, 6, 7, "test"]], + headers=["a", "b", "c", "d"], + title="my table", + striped=True, + full_width=False, + numbered_rows=True, + ) + assert a.data == [[1, 2, 3, 4], [5, 6, 7, "test"]] + assert a.headers == ["a", "b", "c", "d"] + assert a.title == "my table" + assert a.striped is True + assert a.full_width is False + assert a.result_check is False + assert a.numbered_rows is True + + +def test_input_table_identifier(common_setup_teardown): + a = InputTable([[1, 2, 3, 4], [5, 6, 7, "test"]], headers=["a", "b b"]) + assert a.identifier == "input_table-a-b b" + + +def test_input_table_with_override_value(common_setup_teardown): + set_input_default_overrides({"input_table-a-b": [[9, 9, "test"]]}) + a = InputTable([[1, 2, 3, 4], [5, 6, 7, 8]], headers=["a", "b"]) + assert a.data == [[9, 9, "test"]] + assert a.headers == ["a", "b"] + assert a.title is None + saved_items = get_all_calc_objects() + assert len(saved_items) == 1 + assert saved_items[0] == a + + +def test_input_table_with_empty_override_value(common_setup_teardown): + set_input_default_overrides({"input_table-a-b": []}) + a = InputTable([[1, 2, 3, 4], [5, 6, 7, 8]], headers=["a", "b"]) + assert a.data == [] + assert a.headers == ["a", "b"] + assert a.title is None + saved_items = get_all_calc_objects() + assert len(saved_items) == 1 + assert saved_items[0] == a diff --git a/tests/canvas/test_arrow_marker.py b/tests/canvas/test_arrow_marker.py index a955b95..c89ac06 100644 --- a/tests/canvas/test_arrow_marker.py +++ b/tests/canvas/test_arrow_marker.py @@ -3,19 +3,19 @@ def test_arrow_marker_id_with_defaults(): m = ArrowMarker() - assert m.id == "ArrowMarker-context-stroke-none-None-1-False-auto" + assert m.id == "ArrowMarker-context-stroke-none-None-1-False-auto-center" def test_arrow_marker_id_with_some_customization(): m = ArrowMarker(orientation="auto-start-reverse", fill="blue") - assert m.id == "ArrowMarker-blue-none-None-1-False-auto-start-reverse" + assert m.id == "ArrowMarker-blue-none-None-1-False-auto-start-reverse-center" def test_arrow_marker_id_with_full_customization(): m = ArrowMarker( reverse=True, orientation=30, fill="red", stroke="blue", stroke_width=5, size=3 ) - assert m.id == "ArrowMarker-red-blue-5-3-True-30" + assert m.id == "ArrowMarker-red-blue-5-3-True-30-center" def test_svg_style_props_default(): @@ -122,3 +122,24 @@ def test_svg_reverse(): assert 'viewBox="0 0 4 4"' in svg assert 'd="M 0 2.0 L 4 0 L 4 4 z"' in svg assert 'orient="auto"' in svg + + +def test_svg_base_point_center(): + m = ArrowMarker(base="center", size=3) + svg = m.to_svg() + assert 'viewBox="0 0 12 12"' in svg + assert 'refX="6.0" refY="6.0"' in svg + + +def test_svg_base_point_point(): + m = ArrowMarker(base="point", size=3) + svg = m.to_svg() + assert 'viewBox="0 0 12 12"' in svg + assert 'refX="12" refY="6.0"' in svg + + +def test_svg_base_point_flat(): + m = ArrowMarker(base="flat", size=3) + svg = m.to_svg() + assert 'viewBox="0 0 12 12"' in svg + assert 'refX="0" refY="6.0"' in svg diff --git a/tests/canvas/test_canvas.py b/tests/canvas/test_canvas.py index c0acfc1..4055f10 100644 --- a/tests/canvas/test_canvas.py +++ b/tests/canvas/test_canvas.py @@ -45,6 +45,14 @@ def test_canvas_defaults_no_elements(common_setup_teardown): ) +def test_canvas_with_shifted_viewbox(common_setup_teardown): + c = Canvas(5, 5, min_xy=(-5, 100.2)) + assert ( + '\n \n ' + == c.to_svg() + ) + + def test_canvas_with_one_element(common_setup_teardown): c = Canvas(5, 5) c.add(Line(0, 0, 5, 5)) diff --git a/tests/canvas/test_dimension.py b/tests/canvas/test_dimension.py new file mode 100644 index 0000000..728260e --- /dev/null +++ b/tests/canvas/test_dimension.py @@ -0,0 +1,115 @@ +from efficalc.canvas import Dimension, Line, Text + + +def test_dimension_defaults(): + d = Dimension(0, 0, 100, 0) + svg = d.to_svg() + assert "" in svg + assert 'stroke-width="1"' in svg + assert ( + '' + ) in svg + assert ( + '100.00" in svg + + +def test_dimension_with_text_override(): + d = Dimension(0, 0, 100, 0, text="Custom Text") + svg = d.to_svg() + assert "" in svg + assert 'stroke-width="1"' in svg + assert "" in svg + assert 'stroke-width="1"' in svg + assert "" in svg + assert '" in svg + assert '" in svg + assert '" in svg + assert 'stroke-width="1"' in svg + assert '" in svg + assert 'stroke-width="2"' in svg + assert '" in svg + assert '" in svg + assert_same_paths("M 48,50 43,50 0,0", l._get_leader_line().to_path_commands()) + assert '" in svg + assert 'marker-end="url(#test-marker_leader)"' in svg + assert '" in svg + assert 'stroke-width="2"' in svg + assert 'fill="none"' in svg + assert 'stroke="blue"' in svg + assert '" in svg + assert 'stroke-width="1"' in svg + assert_same_paths("M 48,50 38,50 0,0", l._get_leader_line().to_path_commands()) + assert '" in svg + assert 'stroke-width="1"' in svg + assert_same_paths("M 52,50 57,50 0,0", l._get_leader_line().to_path_commands()) + assert '" in svg + assert 'stroke-width="1"' in svg + assert_same_paths("M 48,50 43,50 0,0", l._get_leader_line().to_path_commands()) + assert '" in svg + assert l.get_markers() == [] def test_line_with_markers(): @@ -39,6 +40,7 @@ def test_line_with_markers(): svg = l.to_svg() assert ' marker-start="url(#test-start)" ' in svg assert ' marker-end="url(#test-end)" ' in svg + assert l.get_markers() == [TestMarker("test-start"), TestMarker("test-end")] def test_line_custom_style(): diff --git a/tests/canvas/test_polyline.py b/tests/canvas/test_polyline.py index 66e3272..a91a89b 100644 --- a/tests/canvas/test_polyline.py +++ b/tests/canvas/test_polyline.py @@ -113,6 +113,11 @@ def test_svg_includes_markers(): assert ' marker-start="url(#test-start)" ' in svg assert ' marker-end="url(#test-end)" ' in svg assert ' marker-mid="url(#test-mid)" ' in svg + assert p.get_markers() == [ + TestMarker("test-start"), + TestMarker("test-end"), + TestMarker("test-mid"), + ] def test_svg_no_markers_when_none(): @@ -128,6 +133,7 @@ def test_svg_no_markers_when_none(): assert "marker-start" not in svg assert "marker-end" not in svg assert "marker-mid" not in svg + assert p.get_markers() == [] def test_common_style_elements_not_requested(): diff --git a/tests/canvas/test_text.py b/tests/canvas/test_text.py new file mode 100644 index 0000000..93eccae --- /dev/null +++ b/tests/canvas/test_text.py @@ -0,0 +1,58 @@ +from efficalc.canvas import Text + + +def test_text_defaults(): + t = Text("Hello, World!", 10, 20) + svg = t.to_svg() + assert "Hello, World!" in svg + + +def test_text_custom_style(): + t = Text( + "Hello, World!", + 10, + 20, + font_size="16px", + rotate=45, + horizontal_base="center", + vertical_base="middle", + fill="red", + stroke="blue", + stroke_width=2, + ) + svg = t.to_svg() + assert "Hello, World!" in svg + + +def test_text_horizontal_base(): + t = Text("Hello, World!", 10, 20, horizontal_base="end") + svg = t.to_svg() + assert "test-description

    ' in result ) + + +def test_table_full_composition(common_setup_teardown): + table = Table( + [["alpha", 1], ["beta", 2]], ["greeks", "numbers"], "greek letters and numbers" + ) + result = generate_html_for_calc_items([table]) + assert "greek letters and numbers" + "greeksnumbers" + "alpha1" + "beta2" + ) in result + + +def test_input_table_full_composition(common_setup_teardown): + table = InputTable( + [["alpha", 1], ["beta", 2]], ["greeks", "numbers"], "greek letters and numbers" + ) + result = generate_html_for_calc_items([table]) + assert "greek letters and numbers" + "greeksnumbers" + "alpha1" + "beta2" + ) in result + + +def test_table_data_only(common_setup_teardown): + table = Table([[1, 2, 3], [1]]) + result = generate_html_for_calc_items([table]) + assert "" not in result + assert ( + "123" + "1" + ) in result + + +def test_table_with_all_styling(common_setup_teardown): + table = Table([[3]], full_width=True, striped=True) + result = generate_html_for_calc_items([table]) + assert '' in result + assert "
    3
    " in result + assert "1" not in result + + +def test_table_with_no_styling(common_setup_teardown): + table = Table([[3]], full_width=False, striped=False) + result = generate_html_for_calc_items([table]) + assert ( + '
    3
    ' + in result + ) + assert "1" not in result + + +def test_table_no_data(common_setup_teardown): + table = Table([], ["greeks", "numbers"]) + + result = generate_html_for_calc_items([table]) + assert "greeksnumbers" + "" + ) in result + assert "1" not in result + + +def test_table_with_row_numbers(common_setup_teardown): + table = Table( + [["f", "g"], ["i", "j"], ["l", "m"]], + ["greeks", "numbers"], + numbered_rows=True, + ) + + result = generate_html_for_calc_items([table]) + assert "greeksnumbers" in result + assert "1fg" in result + assert "2i" in result + assert "3l" in result + + +def test_table_with_row_numbers_without_headers(common_setup_teardown): + table = Table([["f"]], numbered_rows=True) + + result = generate_html_for_calc_items([table]) + assert "1f" in result + assert "2" not in result + + +def test_inline_equation_escapes_hash_character(common_setup_teardown): + a = Input("calc#", 5, "in", "describing text", "refer to code") + result = generate_html_for_calc_items([a]) + assert r"calc\#" in result + assert r"calc#" not in result + assert a.description in result + assert "[" + a.reference + "]" in result + + +def test_equation_escapes_hash_character(common_setup_teardown): + a = Input("a", 2) + b = Input("b", 3) + c = Calculation("c", a + b) + calc = Calculation( + "calc #1", + a - c + brackets(b * a * c) + b - c - a - b, + "in", + "describing # text", + "refer to code", + ) + result = generate_html_for_calc_items([calc]) + assert calc.estimate_display_length() == CalculationLength.LONG + assert calc.name not in result + assert "calc \#1" in result + assert calc.description in result + assert calc.reference in result + assert r"\therefore" in result diff --git a/tests/test_report_builder.py b/tests/test_report_builder.py index f16e7ad..0837814 100644 --- a/tests/test_report_builder.py +++ b/tests/test_report_builder.py @@ -4,7 +4,7 @@ import pytest from efficalc import Calculation, Input, clear_saved_objects -from efficalc.report_builder import ReportBuilder +from efficalc.report_builder import LongCalcDisplayType, ReportBuilder @pytest.fixture @@ -235,3 +235,41 @@ def test_save_report_writes_to_file_in_existing_folder_and_opens( assert "{\\left( {a} \\right)}^{ {2} } + {a} + {2}" in report_content assert "" in report_content assert '' in report_content + + +def test_html_configured_with_scaling_long_equations(calc_function): + mathjax_config = """ + + """ + report_builder = ReportBuilder( + calc_function=calc_function, long_calc_display=LongCalcDisplayType.SCALE + ) + report_content = report_builder.get_html_as_str() + assert mathjax_config in report_content + assert "a = 4 \\ \\mathrm{in}" in report_content + assert '' in report_content + + +def test_html_configured_with_breaking_long_equations(calc_function): + mathjax_config = """ + + """ + report_builder = ReportBuilder( + calc_function=calc_function, long_calc_display=LongCalcDisplayType.LINEBREAK + ) + report_content = report_builder.get_html_as_str() + assert mathjax_config in report_content + assert "a = 4 \\ \\mathrm{in}" in report_content + assert '' in report_content