beamz9000 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- beamz9000-0.1.0/PKG-INFO +230 -0
- beamz9000-0.1.0/README.md +205 -0
- beamz9000-0.1.0/pyproject.toml +58 -0
- beamz9000-0.1.0/pyproject.toml.orig +65 -0
- beamz9000-0.1.0/src/beamz9000/__init__.py +52 -0
- beamz9000-0.1.0/src/beamz9000/analysis.py +432 -0
- beamz9000-0.1.0/src/beamz9000/diagram/__init__.py +4 -0
- beamz9000-0.1.0/src/beamz9000/diagram/compose.py +690 -0
- beamz9000-0.1.0/src/beamz9000/diagram/layout.py +139 -0
- beamz9000-0.1.0/src/beamz9000/graphics/__init__.py +0 -0
- beamz9000-0.1.0/src/beamz9000/graphics/elements.py +319 -0
- beamz9000-0.1.0/src/beamz9000/graphics/glyphs.py +294 -0
- beamz9000-0.1.0/src/beamz9000/graphics/text.py +137 -0
- beamz9000-0.1.0/src/beamz9000/render/__init__.py +5 -0
- beamz9000-0.1.0/src/beamz9000/render/frame.py +26 -0
- beamz9000-0.1.0/src/beamz9000/render/matplotlib.py +173 -0
- beamz9000-0.1.0/src/beamz9000/render/reportlab.py +268 -0
- beamz9000-0.1.0/src/beamz9000/render/svg.py +221 -0
- beamz9000-0.1.0/src/beamz9000/scheme.py +758 -0
- beamz9000-0.1.0/src/beamz9000/spec.py +307 -0
- beamz9000-0.1.0/src/beamz9000/units.py +277 -0
- beamz9000-0.1.0/src/beamz9000/ymprint.py +189 -0
beamz9000-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beamz9000
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry.
|
|
5
|
+
Author: Connor Ferster
|
|
6
|
+
Author-email: Connor Ferster <connor@structuralpython.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Dist: forallpeople>=3.4.0
|
|
9
|
+
Requires-Dist: matplotlib>=3.11
|
|
10
|
+
Requires-Dist: pydantic>=2.10
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Requires-Dist: shapely>=2.1
|
|
13
|
+
Requires-Dist: pycba-tools>=0.1.0 ; extra == 'pycba'
|
|
14
|
+
Requires-Dist: pynite-tools>=0.8.1 ; extra == 'pynite'
|
|
15
|
+
Requires-Dist: reportlab>=4.2 ; extra == 'reportlab'
|
|
16
|
+
Requires-Dist: ymprint>=0.12.0 ; python_full_version >= '3.14' and extra == 'ymprint'
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Project-URL: Home, https://github.com/StructuralPython/beamz9000
|
|
19
|
+
Project-URL: Documentation, https://beamz9000.structuralpython.com
|
|
20
|
+
Provides-Extra: pycba
|
|
21
|
+
Provides-Extra: pynite
|
|
22
|
+
Provides-Extra: reportlab
|
|
23
|
+
Provides-Extra: ymprint
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# beamz9000
|
|
27
|
+
|
|
28
|
+
Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry.
|
|
29
|
+
|
|
30
|
+
**Documentation:** https://beamz9000.structuralpython.com
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import beamz9000 as bz
|
|
34
|
+
|
|
35
|
+
bz.draw("examples/beam_scheme.yaml", "B1.png") # or .pdf, .svg
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
scheme YAML ──► scheme.Beam ──► spec.BeamSpec ──► graphics elements ──► renderer
|
|
42
|
+
(pydantic, (engine-neutral (shapely geometry (matplotlib:
|
|
43
|
+
validated, SI floats, legend + style mixins, png/pdf/svg;
|
|
44
|
+
locations keys, labels) Text, Group) native SVG)
|
|
45
|
+
resolved)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **shapely is the intermediate type.** Supports, arrows, load regions and
|
|
49
|
+
dimension ticks are `Polygon`s and `LineString`s built by small pure
|
|
50
|
+
functions in `graphics/glyphs.py` (the decomposition and proportions follow
|
|
51
|
+
fbd_plotting). Each glyph is drawn at the origin, then oriented and placed.
|
|
52
|
+
- **Style lives next to the geometry**, never inside a backend. `Shape`,
|
|
53
|
+
`Text` and `Group` (`graphics/elements.py`) are frozen pydantic models built
|
|
54
|
+
from `Stroke` (colour, opacity, width, line type), `Fill` (colour, opacity,
|
|
55
|
+
hatch) and `Font` mixins, plus z-order, legend key and role. `translate`,
|
|
56
|
+
`rotate`, `scale`, `mirror_x/y` and `restyle` return new elements. A whole
|
|
57
|
+
diagram serializes to JSON and reads back equal.
|
|
58
|
+
- **Text stays text.** Labels are drawn as native, searchable text (matplotlib
|
|
59
|
+
Text artists with TrueType embedded in PDF, `<text>` in SVG). Layout
|
|
60
|
+
measures each label with libraqm shaping (bundled with matplotlib ≥ 3.11),
|
|
61
|
+
so the space it reserves is the space the label is drawn in.
|
|
62
|
+
- **Strata, top to bottom:** load bands (one per load case, occupancy, or
|
|
63
|
+
"Factored", with overlapping loads packed into rows), the beam, supports,
|
|
64
|
+
dimensions, and a legend to the right. Sizes are fractions of the beam length
|
|
65
|
+
(`diagram.Layout`), and the palette is colour-blind safe (Okabe-Ito).
|
|
66
|
+
|
|
67
|
+
## The scheme format
|
|
68
|
+
|
|
69
|
+
A scheme file maps beam ids to beams. Write plain numbers in any consistent
|
|
70
|
+
unit system, or name a [forallpeople](https://github.com/connorferster/forallpeople)
|
|
71
|
+
environment and write quantities:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
_environment: structural # optional; enables "4500 mm", "2.5 kN/m", ...
|
|
75
|
+
|
|
76
|
+
B1:
|
|
77
|
+
spans: [4500 mm, 2500 mm]
|
|
78
|
+
cantilever: right # left | right | both
|
|
79
|
+
e: 200 GPa # optional; or `ei:`
|
|
80
|
+
i: 300e6 mm**4
|
|
81
|
+
depth: 450 mm # optional; drawn as a rectangle to scale
|
|
82
|
+
node_labels: auto # optional; A, B, C ... or a list like [A, B, null]
|
|
83
|
+
supports: [pin, roller, free] # optional override (see below)
|
|
84
|
+
loads:
|
|
85
|
+
- point: {p: 15 kN, span_loc: [1, 1250 mm], case: L}
|
|
86
|
+
- point: {mag: 10 kN, loc: -500 mm} # no case: already factored
|
|
87
|
+
- dist: {mag: 2.5 kN/m, case: D} # no locations: whole beam
|
|
88
|
+
- dist: {mag1: 1 kN/m, mag2: 4 kN/m, x1: 1000 mm, span_loc2: [0, S], case: L}
|
|
89
|
+
- pressure: {mag: residential, trib_width1: 3 m, trib_width2: 1.5 m, span_loc1: [1, 0 mm], loc2: L}
|
|
90
|
+
- moment: {mag: 5 kN*m, loc: 0 mm}
|
|
91
|
+
occupancies:
|
|
92
|
+
- residential: {D: 1.5 kPa, L: 1.9 kPa}
|
|
93
|
+
load_combos:
|
|
94
|
+
- ULS1: {name: D, factor: 1.4}
|
|
95
|
+
- ULS2: {D: 1.25, L: 1.5}
|
|
96
|
+
results:
|
|
97
|
+
diagrams: [moment, shear, deflection]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Locations
|
|
101
|
+
|
|
102
|
+
| Written | Means |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `loc: 2000` (alias `x`) | 2000 from the left end |
|
|
105
|
+
| `loc: -500` | 500 back from the right end (L − 500) |
|
|
106
|
+
| `loc: L` | the right end |
|
|
107
|
+
| `span_loc: [1, 1250]` | 1250 into span 1 (spans count from 0) |
|
|
108
|
+
| `span_loc: [1, -250]` | 250 back from the end of span 1 |
|
|
109
|
+
| `span_loc: [0, S]` | the end of span 0 |
|
|
110
|
+
| `span_loc: 0` | the end of span 0 (a bare index) |
|
|
111
|
+
|
|
112
|
+
A `dist` or `pressure` load takes `loc1`/`x1`/`span_loc1` and
|
|
113
|
+
`loc2`/`x2`/`span_loc2`. Leaving out the start means the left end, and leaving
|
|
114
|
+
out the end means the right end.
|
|
115
|
+
|
|
116
|
+
### Loads
|
|
117
|
+
|
|
118
|
+
| Type | Magnitude | Notes |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `point` | `mag` (aliases `magnitude`, `p`) | Positive acts downward. `angle` (alias `alpha`), in degrees from vertical between −90 and 90, tilts the arrow: positive turns its tail counter-clockwise about its tip. |
|
|
121
|
+
| `moment` | `mag` (aliases `magnitude`, `m`) | Positive is counter-clockwise. |
|
|
122
|
+
| `dist` | `mag` (aliases `magnitude`, `w`) for a UDL, or `mag1` → `mag2` | Linear between the two. |
|
|
123
|
+
| `pressure` | `mag` (aliases `magnitude`, `q`, `occupancy`) × `trib_width1` → `trib_width2` | `trib_width`/`trib` gives a uniform width. `mag` may name an occupancy: the load then stands for one pressure per case in it, or only `case` if given. |
|
|
124
|
+
|
|
125
|
+
Every load takes an optional `case` (with no case, the load is already factored)
|
|
126
|
+
and an optional `label` that replaces the drawn magnitude. Loads may also be
|
|
127
|
+
written flat, as `{type: point, p: 15, loc: 2000}`.
|
|
128
|
+
|
|
129
|
+
### Supports
|
|
130
|
+
|
|
131
|
+
Without `supports`, the first supported node is pinned and the rest are
|
|
132
|
+
rollers, with cantilevered ends left free. A single span with one cantilevered
|
|
133
|
+
end is fixed at the other end. `supports` overrides this, either with one
|
|
134
|
+
support per node or as `{node index: support}` for just the nodes that differ.
|
|
135
|
+
If `cantilever` is also given, `supports` must leave that end free.
|
|
136
|
+
|
|
137
|
+
| Type | Aliases | Drawn as |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `pin` | `pinned`, `hinge` | A triangle on the ground |
|
|
140
|
+
| `roller` | | A circle on the ground |
|
|
141
|
+
| `fixed` | `fix` | A block. At a beam end it becomes a wall covering the beam's depth. |
|
|
142
|
+
| `spring` | `v_spring`, `vertical_spring` | A zigzag spring to the ground |
|
|
143
|
+
| `rotational_spring` | `m_spring`, `rot_spring` | A pin with a spiral |
|
|
144
|
+
| `free` | `none` | Nothing |
|
|
145
|
+
|
|
146
|
+
A spring can take a stiffness, which is drawn beside it. Write it as
|
|
147
|
+
`{spring: 12 kN/mm}` or `{type: spring, k: 12 kN/mm}`. A `spring` takes a force
|
|
148
|
+
per length, and a `rotational_spring` a moment per radian (written as a moment,
|
|
149
|
+
e.g. `8000 kN*m`).
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
supports:
|
|
153
|
+
- rotational_spring: 8000 kN*m
|
|
154
|
+
- roller
|
|
155
|
+
- spring: 12 kN/mm
|
|
156
|
+
- free
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Drawing
|
|
160
|
+
|
|
161
|
+
- Labels show quantities as they were written: `12 kN/mm` stays "12 kN/mm"
|
|
162
|
+
rather than forallpeople's "12 MN/m". `*` is drawn as a middle dot and `**n`
|
|
163
|
+
as a superscript (`5 kN*m` → "5 kN·m"). Computed values, such as the overall
|
|
164
|
+
length, use forallpeople's units.
|
|
165
|
+
- Load labels show the magnitude without a sign, because the arrow shows the
|
|
166
|
+
direction. The zero end of a triangular load has no label.
|
|
167
|
+
- `node_labels` are drawn in bold on one line under the lowest support.
|
|
168
|
+
- See `examples/drawing_features.yaml` for depth, springs, angled loads and
|
|
169
|
+
node labels together.
|
|
170
|
+
|
|
171
|
+
### Fitting to a size
|
|
172
|
+
|
|
173
|
+
A renderer with a `height` outputs exactly `width × height`, and `draw`
|
|
174
|
+
arranges the diagram to fill it. The load strata stretch within
|
|
175
|
+
`Layout.stretch`, and the legend moves on top of the loads when that draws the
|
|
176
|
+
beam larger:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
bz.draw("beams.yaml", "B1.png", renderer=bz.MatplotlibRenderer(width=6.5, height=2.5))
|
|
180
|
+
diagram = bz.beam_diagram("beams.yaml", aspect=6.5 / 2.5) # without a renderer
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
With `Layout(font_size_pt=8)`, text is 8 pt in the output whatever the beam's
|
|
184
|
+
length, and symbols scale with it.
|
|
185
|
+
|
|
186
|
+
### ymprint block and ReportLab
|
|
187
|
+
|
|
188
|
+
`uv add "beamz9000[ymprint]"` adds a `_beam` block for
|
|
189
|
+
[ymprint](https://ymprint.structuralpython.com) reports
|
|
190
|
+
(`ym convert report.yml` renders it, or call `register(app)` in your own
|
|
191
|
+
application). `beamz9000.render.reportlab.ReportLabRenderer` draws a
|
|
192
|
+
diagram into any ReportLab document as vector graphics with searchable text.
|
|
193
|
+
|
|
194
|
+
### Occupancies, combinations, results
|
|
195
|
+
|
|
196
|
+
- `occupancies` and `load_combos` accept a list of `- name: {...}` items or a
|
|
197
|
+
plain mapping.
|
|
198
|
+
- A combination is `{name: D, factor: 1.4}`, a list of those, or
|
|
199
|
+
`{D: 1.25, L: 1.5}`.
|
|
200
|
+
- `results` analyses the beam with pycba-tools or pynite-tools (the `pycba`
|
|
201
|
+
and `pynite` extras): list any of `forces`, `shear`, `moment`,
|
|
202
|
+
`deflection(s)`, `rotation(s)`, or write `{diagrams: [...], engine: pynite}`.
|
|
203
|
+
The diagram then shows each support's reactions per load case, and the
|
|
204
|
+
result diagrams below, with positive moment on the tension side.
|
|
205
|
+
|
|
206
|
+
### Validation
|
|
207
|
+
|
|
208
|
+
Errors name the load and the problem, for example `load 3 (dist): it must end
|
|
209
|
+
after it starts`. Other checks:
|
|
210
|
+
|
|
211
|
+
- every load lands on the beam
|
|
212
|
+
- span offsets stay inside their span
|
|
213
|
+
- quantities have the right dimensions
|
|
214
|
+
- a beam doesn't mix quantities with bare numbers
|
|
215
|
+
- named occupancies exist
|
|
216
|
+
- supports agree with `cantilever`
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
uv sync
|
|
222
|
+
uv run pytest
|
|
223
|
+
uv run --group docs sphinx-build -b html docs docs/_build/html # docs
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Tests run on every push and pull request (`.github/workflows/tests.yml`). The docs
|
|
227
|
+
are built on pull requests and deployed to bunny.net on every push to `main`
|
|
228
|
+
(`.github/workflows/publish-docs-to-bunny.yml`). Publishing a GitHub release
|
|
229
|
+
uploads the package to PyPI (`.github/workflows/publish.yml`); the release tag
|
|
230
|
+
must match the version in `pyproject.toml`.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# beamz9000
|
|
2
|
+
|
|
3
|
+
Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry.
|
|
4
|
+
|
|
5
|
+
**Documentation:** https://beamz9000.structuralpython.com
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import beamz9000 as bz
|
|
9
|
+
|
|
10
|
+
bz.draw("examples/beam_scheme.yaml", "B1.png") # or .pdf, .svg
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
scheme YAML ──► scheme.Beam ──► spec.BeamSpec ──► graphics elements ──► renderer
|
|
17
|
+
(pydantic, (engine-neutral (shapely geometry (matplotlib:
|
|
18
|
+
validated, SI floats, legend + style mixins, png/pdf/svg;
|
|
19
|
+
locations keys, labels) Text, Group) native SVG)
|
|
20
|
+
resolved)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **shapely is the intermediate type.** Supports, arrows, load regions and
|
|
24
|
+
dimension ticks are `Polygon`s and `LineString`s built by small pure
|
|
25
|
+
functions in `graphics/glyphs.py` (the decomposition and proportions follow
|
|
26
|
+
fbd_plotting). Each glyph is drawn at the origin, then oriented and placed.
|
|
27
|
+
- **Style lives next to the geometry**, never inside a backend. `Shape`,
|
|
28
|
+
`Text` and `Group` (`graphics/elements.py`) are frozen pydantic models built
|
|
29
|
+
from `Stroke` (colour, opacity, width, line type), `Fill` (colour, opacity,
|
|
30
|
+
hatch) and `Font` mixins, plus z-order, legend key and role. `translate`,
|
|
31
|
+
`rotate`, `scale`, `mirror_x/y` and `restyle` return new elements. A whole
|
|
32
|
+
diagram serializes to JSON and reads back equal.
|
|
33
|
+
- **Text stays text.** Labels are drawn as native, searchable text (matplotlib
|
|
34
|
+
Text artists with TrueType embedded in PDF, `<text>` in SVG). Layout
|
|
35
|
+
measures each label with libraqm shaping (bundled with matplotlib ≥ 3.11),
|
|
36
|
+
so the space it reserves is the space the label is drawn in.
|
|
37
|
+
- **Strata, top to bottom:** load bands (one per load case, occupancy, or
|
|
38
|
+
"Factored", with overlapping loads packed into rows), the beam, supports,
|
|
39
|
+
dimensions, and a legend to the right. Sizes are fractions of the beam length
|
|
40
|
+
(`diagram.Layout`), and the palette is colour-blind safe (Okabe-Ito).
|
|
41
|
+
|
|
42
|
+
## The scheme format
|
|
43
|
+
|
|
44
|
+
A scheme file maps beam ids to beams. Write plain numbers in any consistent
|
|
45
|
+
unit system, or name a [forallpeople](https://github.com/connorferster/forallpeople)
|
|
46
|
+
environment and write quantities:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
_environment: structural # optional; enables "4500 mm", "2.5 kN/m", ...
|
|
50
|
+
|
|
51
|
+
B1:
|
|
52
|
+
spans: [4500 mm, 2500 mm]
|
|
53
|
+
cantilever: right # left | right | both
|
|
54
|
+
e: 200 GPa # optional; or `ei:`
|
|
55
|
+
i: 300e6 mm**4
|
|
56
|
+
depth: 450 mm # optional; drawn as a rectangle to scale
|
|
57
|
+
node_labels: auto # optional; A, B, C ... or a list like [A, B, null]
|
|
58
|
+
supports: [pin, roller, free] # optional override (see below)
|
|
59
|
+
loads:
|
|
60
|
+
- point: {p: 15 kN, span_loc: [1, 1250 mm], case: L}
|
|
61
|
+
- point: {mag: 10 kN, loc: -500 mm} # no case: already factored
|
|
62
|
+
- dist: {mag: 2.5 kN/m, case: D} # no locations: whole beam
|
|
63
|
+
- dist: {mag1: 1 kN/m, mag2: 4 kN/m, x1: 1000 mm, span_loc2: [0, S], case: L}
|
|
64
|
+
- pressure: {mag: residential, trib_width1: 3 m, trib_width2: 1.5 m, span_loc1: [1, 0 mm], loc2: L}
|
|
65
|
+
- moment: {mag: 5 kN*m, loc: 0 mm}
|
|
66
|
+
occupancies:
|
|
67
|
+
- residential: {D: 1.5 kPa, L: 1.9 kPa}
|
|
68
|
+
load_combos:
|
|
69
|
+
- ULS1: {name: D, factor: 1.4}
|
|
70
|
+
- ULS2: {D: 1.25, L: 1.5}
|
|
71
|
+
results:
|
|
72
|
+
diagrams: [moment, shear, deflection]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Locations
|
|
76
|
+
|
|
77
|
+
| Written | Means |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `loc: 2000` (alias `x`) | 2000 from the left end |
|
|
80
|
+
| `loc: -500` | 500 back from the right end (L − 500) |
|
|
81
|
+
| `loc: L` | the right end |
|
|
82
|
+
| `span_loc: [1, 1250]` | 1250 into span 1 (spans count from 0) |
|
|
83
|
+
| `span_loc: [1, -250]` | 250 back from the end of span 1 |
|
|
84
|
+
| `span_loc: [0, S]` | the end of span 0 |
|
|
85
|
+
| `span_loc: 0` | the end of span 0 (a bare index) |
|
|
86
|
+
|
|
87
|
+
A `dist` or `pressure` load takes `loc1`/`x1`/`span_loc1` and
|
|
88
|
+
`loc2`/`x2`/`span_loc2`. Leaving out the start means the left end, and leaving
|
|
89
|
+
out the end means the right end.
|
|
90
|
+
|
|
91
|
+
### Loads
|
|
92
|
+
|
|
93
|
+
| Type | Magnitude | Notes |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `point` | `mag` (aliases `magnitude`, `p`) | Positive acts downward. `angle` (alias `alpha`), in degrees from vertical between −90 and 90, tilts the arrow: positive turns its tail counter-clockwise about its tip. |
|
|
96
|
+
| `moment` | `mag` (aliases `magnitude`, `m`) | Positive is counter-clockwise. |
|
|
97
|
+
| `dist` | `mag` (aliases `magnitude`, `w`) for a UDL, or `mag1` → `mag2` | Linear between the two. |
|
|
98
|
+
| `pressure` | `mag` (aliases `magnitude`, `q`, `occupancy`) × `trib_width1` → `trib_width2` | `trib_width`/`trib` gives a uniform width. `mag` may name an occupancy: the load then stands for one pressure per case in it, or only `case` if given. |
|
|
99
|
+
|
|
100
|
+
Every load takes an optional `case` (with no case, the load is already factored)
|
|
101
|
+
and an optional `label` that replaces the drawn magnitude. Loads may also be
|
|
102
|
+
written flat, as `{type: point, p: 15, loc: 2000}`.
|
|
103
|
+
|
|
104
|
+
### Supports
|
|
105
|
+
|
|
106
|
+
Without `supports`, the first supported node is pinned and the rest are
|
|
107
|
+
rollers, with cantilevered ends left free. A single span with one cantilevered
|
|
108
|
+
end is fixed at the other end. `supports` overrides this, either with one
|
|
109
|
+
support per node or as `{node index: support}` for just the nodes that differ.
|
|
110
|
+
If `cantilever` is also given, `supports` must leave that end free.
|
|
111
|
+
|
|
112
|
+
| Type | Aliases | Drawn as |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `pin` | `pinned`, `hinge` | A triangle on the ground |
|
|
115
|
+
| `roller` | | A circle on the ground |
|
|
116
|
+
| `fixed` | `fix` | A block. At a beam end it becomes a wall covering the beam's depth. |
|
|
117
|
+
| `spring` | `v_spring`, `vertical_spring` | A zigzag spring to the ground |
|
|
118
|
+
| `rotational_spring` | `m_spring`, `rot_spring` | A pin with a spiral |
|
|
119
|
+
| `free` | `none` | Nothing |
|
|
120
|
+
|
|
121
|
+
A spring can take a stiffness, which is drawn beside it. Write it as
|
|
122
|
+
`{spring: 12 kN/mm}` or `{type: spring, k: 12 kN/mm}`. A `spring` takes a force
|
|
123
|
+
per length, and a `rotational_spring` a moment per radian (written as a moment,
|
|
124
|
+
e.g. `8000 kN*m`).
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
supports:
|
|
128
|
+
- rotational_spring: 8000 kN*m
|
|
129
|
+
- roller
|
|
130
|
+
- spring: 12 kN/mm
|
|
131
|
+
- free
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Drawing
|
|
135
|
+
|
|
136
|
+
- Labels show quantities as they were written: `12 kN/mm` stays "12 kN/mm"
|
|
137
|
+
rather than forallpeople's "12 MN/m". `*` is drawn as a middle dot and `**n`
|
|
138
|
+
as a superscript (`5 kN*m` → "5 kN·m"). Computed values, such as the overall
|
|
139
|
+
length, use forallpeople's units.
|
|
140
|
+
- Load labels show the magnitude without a sign, because the arrow shows the
|
|
141
|
+
direction. The zero end of a triangular load has no label.
|
|
142
|
+
- `node_labels` are drawn in bold on one line under the lowest support.
|
|
143
|
+
- See `examples/drawing_features.yaml` for depth, springs, angled loads and
|
|
144
|
+
node labels together.
|
|
145
|
+
|
|
146
|
+
### Fitting to a size
|
|
147
|
+
|
|
148
|
+
A renderer with a `height` outputs exactly `width × height`, and `draw`
|
|
149
|
+
arranges the diagram to fill it. The load strata stretch within
|
|
150
|
+
`Layout.stretch`, and the legend moves on top of the loads when that draws the
|
|
151
|
+
beam larger:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
bz.draw("beams.yaml", "B1.png", renderer=bz.MatplotlibRenderer(width=6.5, height=2.5))
|
|
155
|
+
diagram = bz.beam_diagram("beams.yaml", aspect=6.5 / 2.5) # without a renderer
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
With `Layout(font_size_pt=8)`, text is 8 pt in the output whatever the beam's
|
|
159
|
+
length, and symbols scale with it.
|
|
160
|
+
|
|
161
|
+
### ymprint block and ReportLab
|
|
162
|
+
|
|
163
|
+
`uv add "beamz9000[ymprint]"` adds a `_beam` block for
|
|
164
|
+
[ymprint](https://ymprint.structuralpython.com) reports
|
|
165
|
+
(`ym convert report.yml` renders it, or call `register(app)` in your own
|
|
166
|
+
application). `beamz9000.render.reportlab.ReportLabRenderer` draws a
|
|
167
|
+
diagram into any ReportLab document as vector graphics with searchable text.
|
|
168
|
+
|
|
169
|
+
### Occupancies, combinations, results
|
|
170
|
+
|
|
171
|
+
- `occupancies` and `load_combos` accept a list of `- name: {...}` items or a
|
|
172
|
+
plain mapping.
|
|
173
|
+
- A combination is `{name: D, factor: 1.4}`, a list of those, or
|
|
174
|
+
`{D: 1.25, L: 1.5}`.
|
|
175
|
+
- `results` analyses the beam with pycba-tools or pynite-tools (the `pycba`
|
|
176
|
+
and `pynite` extras): list any of `forces`, `shear`, `moment`,
|
|
177
|
+
`deflection(s)`, `rotation(s)`, or write `{diagrams: [...], engine: pynite}`.
|
|
178
|
+
The diagram then shows each support's reactions per load case, and the
|
|
179
|
+
result diagrams below, with positive moment on the tension side.
|
|
180
|
+
|
|
181
|
+
### Validation
|
|
182
|
+
|
|
183
|
+
Errors name the load and the problem, for example `load 3 (dist): it must end
|
|
184
|
+
after it starts`. Other checks:
|
|
185
|
+
|
|
186
|
+
- every load lands on the beam
|
|
187
|
+
- span offsets stay inside their span
|
|
188
|
+
- quantities have the right dimensions
|
|
189
|
+
- a beam doesn't mix quantities with bare numbers
|
|
190
|
+
- named occupancies exist
|
|
191
|
+
- supports agree with `cantilever`
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
uv sync
|
|
197
|
+
uv run pytest
|
|
198
|
+
uv run --group docs sphinx-build -b html docs docs/_build/html # docs
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Tests run on every push and pull request (`.github/workflows/tests.yml`). The docs
|
|
202
|
+
are built on pull requests and deployed to bunny.net on every push to `main`
|
|
203
|
+
(`.github/workflows/publish-docs-to-bunny.yml`). Publishing a GitHub release
|
|
204
|
+
uploads the package to PyPI (`.github/workflows/publish.yml`); the release tag
|
|
205
|
+
must match the version in `pyproject.toml`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "beamz9000"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"forallpeople>=3.4.0",
|
|
10
|
+
"matplotlib>=3.11",
|
|
11
|
+
"pydantic>=2.10",
|
|
12
|
+
"pyyaml>=6.0",
|
|
13
|
+
"shapely>=2.1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[[project.authors]]
|
|
17
|
+
name = "Connor Ferster"
|
|
18
|
+
email = "connor@structuralpython.com"
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Home = "https://github.com/StructuralPython/beamz9000"
|
|
22
|
+
Documentation = "https://beamz9000.structuralpython.com"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
reportlab = ["reportlab>=4.2"]
|
|
26
|
+
ymprint = ["ymprint>=0.12.0 ; python_full_version >= '3.14'"]
|
|
27
|
+
pycba = ["pycba-tools>=0.1.0"]
|
|
28
|
+
pynite = ["pynite-tools>=0.8.1"]
|
|
29
|
+
|
|
30
|
+
[project.entry-points."ymprint.blocks"]
|
|
31
|
+
_beam = "beamz9000.ymprint:beam_block"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.9.14,<0.12"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"ipykernel>=7.1.0",
|
|
40
|
+
"pycba-tools>=0.1.0",
|
|
41
|
+
"pynite-tools>=0.8.1",
|
|
42
|
+
"pytest>=8.3",
|
|
43
|
+
"reportlab>=4.2",
|
|
44
|
+
"ymprint>=0.12.0 ; python_full_version >= '3.14'",
|
|
45
|
+
]
|
|
46
|
+
docs = [
|
|
47
|
+
"autodoc-pydantic>=2.2.0",
|
|
48
|
+
"pycba-tools>=0.1.0",
|
|
49
|
+
"pynite-tools>=0.8.1",
|
|
50
|
+
"myst-parser>=5.2.0",
|
|
51
|
+
"shibuya>=2026.7.12",
|
|
52
|
+
"sphinx>=9.1.1",
|
|
53
|
+
"sphinx-copybutton>=0.5.2",
|
|
54
|
+
"sphinx-design>=0.8.0",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "beamz9000"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
authors = [{ name = "Connor Ferster", email = "connor@structuralpython.com" }]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"forallpeople>=3.4.0",
|
|
11
|
+
"matplotlib>=3.11",
|
|
12
|
+
"pydantic>=2.10",
|
|
13
|
+
"pyyaml>=6.0",
|
|
14
|
+
"shapely>=2.1",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Home = "https://github.com/StructuralPython/beamz9000"
|
|
19
|
+
Documentation = "https://beamz9000.structuralpython.com"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
reportlab = [
|
|
23
|
+
"reportlab>=4.2",
|
|
24
|
+
]
|
|
25
|
+
ymprint = [
|
|
26
|
+
"ymprint>=0.12.0 ; python_full_version >= '3.14'",
|
|
27
|
+
]
|
|
28
|
+
pycba = [
|
|
29
|
+
"pycba-tools>=0.1.0",
|
|
30
|
+
]
|
|
31
|
+
pynite = [
|
|
32
|
+
"pynite-tools>=0.8.1",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# The _beam block for ymprint's `ym` command and every other ymprint application
|
|
36
|
+
# (ymprint 0.12 and later read the `ymprint.blocks` entry-point group).
|
|
37
|
+
[project.entry-points."ymprint.blocks"]
|
|
38
|
+
_beam = "beamz9000.ymprint:beam_block"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.9.14,<0.12"]
|
|
42
|
+
build-backend = "uv_build"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"ipykernel>=7.1.0",
|
|
47
|
+
"pycba-tools>=0.1.0",
|
|
48
|
+
"pynite-tools>=0.8.1",
|
|
49
|
+
"pytest>=8.3",
|
|
50
|
+
"reportlab>=4.2",
|
|
51
|
+
"ymprint>=0.12.0 ; python_full_version >= '3.14'",
|
|
52
|
+
]
|
|
53
|
+
docs = [
|
|
54
|
+
"autodoc-pydantic>=2.2.0",
|
|
55
|
+
"pycba-tools>=0.1.0",
|
|
56
|
+
"pynite-tools>=0.8.1",
|
|
57
|
+
"myst-parser>=5.2.0",
|
|
58
|
+
"shibuya>=2026.7.12",
|
|
59
|
+
"sphinx>=9.1.1",
|
|
60
|
+
"sphinx-copybutton>=0.5.2",
|
|
61
|
+
"sphinx-design>=0.8.0",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
beamz9000: beam free-body diagrams from a simple YAML scheme.
|
|
3
|
+
|
|
4
|
+
The pipeline is: scheme YAML -> `scheme.Beam` (pydantic, validated) ->
|
|
5
|
+
`spec.BeamSpec` (engine-neutral floats) -> shapely-based graphics elements ->
|
|
6
|
+
a renderer (matplotlib, SVG).
|
|
7
|
+
|
|
8
|
+
import beamz9000 as bz
|
|
9
|
+
bz.draw("examples/beam_scheme.yaml", "B1.png")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import pathlib
|
|
15
|
+
from typing import Optional, Union
|
|
16
|
+
|
|
17
|
+
from beamz9000.diagram import Layout, as_spec, beam_diagram
|
|
18
|
+
from beamz9000.graphics.elements import Group, Shape, Text
|
|
19
|
+
from beamz9000.render import MatplotlibRenderer, SVGRenderer
|
|
20
|
+
from beamz9000.scheme import Beam, Scheme, load_scheme, parse_scheme
|
|
21
|
+
from beamz9000.spec import BeamSpec, from_scheme
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def draw(obj, path: Union[str, pathlib.Path], layout: Optional[Layout] = None,
|
|
27
|
+
beam_id: Optional[str] = None, renderer=None) -> pathlib.Path:
|
|
28
|
+
"""
|
|
29
|
+
Draws the free-body diagram of 'obj' (a scheme file path, Scheme, Beam,
|
|
30
|
+
BeamSpec, or an already-built diagram Group) to 'path'. The suffix picks
|
|
31
|
+
the format: .svg uses the native SVGRenderer, anything else (png, pdf,
|
|
32
|
+
...) the MatplotlibRenderer, unless 'renderer' is given.
|
|
33
|
+
|
|
34
|
+
A renderer with a `height` sets the output's exact size, and the diagram
|
|
35
|
+
is arranged to fill it (see `beam_diagram`'s 'aspect'). With
|
|
36
|
+
`layout.font_size_pt`, text is that many points high in the output.
|
|
37
|
+
"""
|
|
38
|
+
path = pathlib.Path(path)
|
|
39
|
+
if renderer is None:
|
|
40
|
+
renderer = SVGRenderer() if path.suffix.lower() == ".svg" else MatplotlibRenderer()
|
|
41
|
+
if isinstance(obj, Group):
|
|
42
|
+
diagram = obj
|
|
43
|
+
else:
|
|
44
|
+
diagram = beam_diagram(obj, layout, beam_id, aspect=renderer.aspect, margin=renderer.margin,
|
|
45
|
+
width=renderer.width_pt)
|
|
46
|
+
return renderer.save(diagram, path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"Beam", "BeamSpec", "Group", "Layout", "MatplotlibRenderer", "SVGRenderer", "Scheme",
|
|
51
|
+
"Shape", "Text", "as_spec", "beam_diagram", "draw", "from_scheme", "load_scheme", "parse_scheme",
|
|
52
|
+
]
|