visualdynamics.demo.drone¶
drone
¶
The demonstration airframe: a quadcopter, built with visualdynamics.
This is the model every drone fixture comes from — the truth set, the reduced
test geometry, and the plant the Rattlesnake runs fly against. It is built
entirely with visualdynamics.fem — like every fixture model since the
sdynpy demo articles left the repository: a demonstration of the
toolset should be made by the toolset.
It builds a model and returns it; it writes no files. testdata/'s
generate_drone.py, in the visualdynamics-generators repository, is
what turns it into fixtures on disk.
from visualdynamics.demo import drone
model = drone.build()
shapes = model.eigensolution(maximum_frequency=400, damping=0.01)
Run it directly to print what it weighs and where its modes are:
python3 -m visualdynamics.demo.drone
The geometry is the model. Every surface is swept from a profile along a path, every edge of every face becomes a beam, and the mass is shared equally over the nodes. There is nothing derived and nothing tied: what you see is what is solved, node for node.
That is the second design. The first was a beam skeleton with surfaces hung off it on rigid ties — nodes that were drawn but not solved, their motion extrapolated from the beam they rode. It worked, and it was cheap, and it was a picture of a fidelity the model did not have: the arm walls looked meshed and were carrying nothing. Letting the drawing be the structure is simpler to write, simpler to explain, and honest about what it is.
Beams on the edges, not springs. A spring on each edge leaves a quad free to shear and a flat panel free to fold, since neither changes any edge length, and the model comes back a mechanism with a zero-frequency mode per panel. A beam carries moment, so the same wireframe stands up. Proved on a cube before anything else was built: six rigid-body modes, then 121.7 Hz.
The members are massless and the nodes carry it all. Equal shares of the total, so the mass follows the model rather than a section table — which also means it follows the mesh, and the mesh is kept roughly even for that reason. Each node also gets a rotary inertia, without which the rotational degrees of freedom carry nothing and the mass matrix will not factorize.
The numbers are plausible, not surveyed. One section size sets the whole frame's stiffness, chosen to put the elastic modes in a band a shaker test would use. This is a demonstration article, not a drone anyone has weighed.
Classes:
| Name | Description |
|---|---|
Shape |
A drawing: nodes and faces, and nothing else. |
Functions:
| Name | Description |
|---|---|
ring |
Points round a circle, in a sweep's own cross-section plane. |
sweep |
Sweep cross-sections along a path, as rings of nodes joined by quads. |
bar |
A flat rectangular member between two points, in a stated plane. |
blade_profile |
A blade section: a thin cambered shape, rotated to its pitch. |
bridge |
Quads between two rings that already exist — a shared joint. |
girder |
A hollow box swept along a path, with windows cut through it. |
build |
The quadcopter, at whatever mesh density is asked for. |
draw |
The airframe as a drawing — nodes and faces, no structure at all. |
part_of |
Which part of the airframe a node belongs to. |
instrumented |
The nodes a modal survey of this airframe would put sensors on. |
describe |
What it weighs and where its modes are, by what moves in each. |
Classes¶
Shape
¶
A drawing: nodes and faces, and nothing else.
No members, no mass, no elements of any kind — the whole airframe is
built into one of these, and fem.Model.from_geometry is what turns it
into a structure afterwards. Keeping the two apart is the point: the
geometry is authored once and the physics is derived from it, so there
is no second description to disagree with the first.
Anything landing on an existing node is welded to it. Parts are drawn independently — a strut between two ring nodes, a nacelle standing on an arm — and where two of them meet they put a node in the same place twice. Drawn, that looks joined; solved, it is not, and each loose piece brings six zero-frequency modes. The airframe came back with 210 against the six it should have before this was here: 24 truss struts, 4 nacelles, 4 legs and 2 camera mounts, every one a free body.
Methods:
| Name | Description |
|---|---|
drop_face |
Remove a face by the nodes it spans, if it is there. |
pieces |
The drawing's disconnected parts, largest first. |
orient |
Wind every face the same way round, and turn the lot outward. |
prune |
Drop nodes no face uses, and say how many there were. |
geometry |
What was drawn, as a Geometry: quads and triangles, no lines. |
Source code in src/visualdynamics/demo/drone.py
Methods:¶
drop_face
¶
Remove a face by the nodes it spans, if it is there.
Source code in src/visualdynamics/demo/drone.py
pieces
¶
The drawing's disconnected parts, largest first.
Asked of the drawing rather than of a solved model, because that is when it can still be fixed cheaply. Two surfaces that touch are not joined unless they share nodes or something spans them, and the cost of finding out later is six zero-frequency modes per loose piece with nothing on screen to say which.
Source code in src/visualdynamics/demo/drone.py
orient
¶
Wind every face the same way round, and turn the lot outward.
Windings were being corrected girder by girder — reverse if the frame was flipped, reverse again if the rows ran the other way — and it never came right, because a part's winding depends on how it was built and the answer wanted is a property of the finished surface. So this asks the surface instead: walk face to face over shared edges, and where two neighbours traverse their shared edge the same way round, one of them is inside out. Then check the signed volume and turn everything over if the whole shell ended up pointing in.
Faces whose normals point into the solid show as dark patches, and would colour by displacement from the wrong side.
Source code in src/visualdynamics/demo/drone.py
prune
¶
Drop nodes no face uses, and say how many there were.
A window more than one cell tall leaves the nodes strictly inside it belonging to nothing: the edges between two hole cells get no wall, and there is no face on either side. Rather than forbid tall windows — which is what keeps a truss looking like a slotted plate — the drawing simply forgets the nodes nothing referred to.
Source code in src/visualdynamics/demo/drone.py
geometry
¶
geometry(length_unit: str = 'm') -> Geometry
What was drawn, as a Geometry: quads and triangles, no lines.
Each face goes into the block of the part that drew it, so the geometry says by itself which region is which — and a saved file can rebuild the structure without anything passed alongside it.
Blocks are named for the whole part, sides and all ('arm front left', not 'arm'). Collapsing them to the part alone reads more tidily and loses the one thing the sensor set is picked by: which of the four arms a node is on.
Source code in src/visualdynamics/demo/drone.py
Functions:¶
ring
¶
Points round a circle, in a sweep's own cross-section plane.
Source code in src/visualdynamics/demo/drone.py
sweep
¶
sweep(
shape: Shape,
path: Sequence[ArrayLike],
profiles: Profile | Sequence[Profile],
group: str,
color: int,
cap_start: bool = False,
cap_end: bool = False,
) -> list[list[int]]
Sweep cross-sections along a path, as rings of nodes joined by quads.
profiles is one profile per station, so a tube can taper, swell or
change shape along its length; pass a single profile to keep it
uniform. Returns the rings.
Source code in src/visualdynamics/demo/drone.py
bar
¶
bar(
shape: Shape,
start: ArrayLike,
end: ArrayLike,
thick: float,
depth: float,
normal: ArrayLike,
group: str,
color: int,
) -> list[list[int]] | None
A flat rectangular member between two points, in a stated plane.
The frame is given rather than worked out, because a truss has members
at every angle and letting each pick its own reference direction lets
the sections twist relative to one another. normal is the plane the
truss lies in, so every bar in one arm is the same slab of material
seen from a different angle — which is what a plate truss is.
Source code in src/visualdynamics/demo/drone.py
blade_profile
¶
A blade section: a thin cambered shape, rotated to its pitch.
Twist is applied to the profile rather than to the sweep, because the path is a straight radial line and its frame does not turn — a blade that is flat at the tip and coarse at the root is the whole reason a propeller looks like one rather than like a paddle.
Source code in src/visualdynamics/demo/drone.py
bridge
¶
bridge(
shape: Shape,
lower: Sequence[int],
upper: Sequence[int],
color: int,
) -> None
Quads between two rings that already exist — a shared joint.
Source code in src/visualdynamics/demo/drone.py
girder
¶
girder(
shape: Shape,
left: Sequence[int],
right: Sequence[int],
path: Sequence[ArrayLike],
half_height: float,
half_thick: float,
windows: Callable[[int, int], bool] | None,
group: str,
color: int,
cap_end: bool = True,
shear: float = 0.0,
end_left: Sequence[int] | None = None,
end_right: Sequence[int] | None = None,
) -> tuple[list[list[int]], list[list[int]]]
A hollow box swept along a path, with windows cut through it.
One closed surface: two flat sides, a strip along the top and bottom, a cap at the far end, and a wall round every window joining one side to the other. Nothing overlaps anything and there is no face inside the solid — which is the whole point. Built as separate bars welded at their centres, as this was first, a truss is a heap of interpenetrating boxes with their end caps buried in the joints, and colouring it by displacement shows the insides through the skin.
left and right are the rows of nodes the root starts from, so a
girder can grow out of a surface that already exists rather than being
parked against it; end_left and end_right do the same at the far
end, so a member can grow into something as well as out of it.
shear leans the interior node rows alternately along the run, which
turns the openings from upright rectangles into a zigzag — the
difference between a slotted plate and a truss.
Source code in src/visualdynamics/demo/drone.py
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 | |
build
¶
build(
sides: int = 12,
arm_stations: int = 9,
leg_stations: int = 5,
body_rings: int = 3,
total_mass: float = TOTAL_MASS,
) -> Model
The quadcopter, at whatever mesh density is asked for.
sides is how many facets go round every swept tube and round the body,
and is what most of the node count comes from. The defaults are about a
thousand nodes, which the dense eigensolver clears in half a minute;
tests build it coarser, since what they check is the airframe and not
the mesh.
Source code in src/visualdynamics/demo/drone.py
draw
¶
draw(
sides: int = 12,
arm_stations: int = 9,
leg_stations: int = 5,
body_rings: int = 3,
) -> Shape
The airframe as a drawing — nodes and faces, no structure at all.
Source code in src/visualdynamics/demo/drone.py
part_of
¶
Which part of the airframe a node belongs to.
instrumented
¶
The nodes a modal survey of this airframe would put sensors on.
Found from the model rather than written down, because writing them down has been wrong twice: the numbering moves whenever the mesh does, and a generator holding stale ids produces a test of nowhere.
Source code in src/visualdynamics/demo/drone.py
describe
¶
What it weighs and where its modes are, by what moves in each.