//! The commands' parameter schemas ([`crate::schema`]), area by area. Each describes the JSON //! the command's `run` reads, as its `params` doc string does in words. use crate::schema::Collection as C; use crate::schema::Dim as D; use crate::schema::*; // ---- Shared pieces ---- /// `{"set": …}` or `{"reference_point": …}`. const TARGET_OPTIONS: &[Field] = &[reference("set", "Set", C::Set), reference("reference_point", "Reference point", C::ReferencePoint)]; const fn target(key: &'static str, label: &'static str) -> Field { either(key, label, TARGET_OPTIONS) } /// The name of an existing definition, for delete commands. macro_rules! existing { ($label:literal, $c:expr) => { &[reference("name", $label, $c).req()] }; } pub const SET_DELETE: &[Field] = existing!("Set", C::Set); pub const MATERIAL_DELETE: &[Field] = existing!("Material", C::Material); pub const SECTION_DELETE: &[Field] = existing!("Section", C::Section); pub const REFERENCE_POINT_DELETE: &[Field] = existing!("Reference point", C::ReferencePoint); pub const COUPLING_DELETE: &[Field] = existing!("Coupling", C::Coupling); pub const MASS_DELETE: &[Field] = existing!("Point mass", C::Mass); pub const SPRING_DELETE: &[Field] = existing!("Spring", C::Spring); pub const BOUNDARY_CONDITION_DELETE: &[Field] = existing!("Boundary condition", C::BoundaryCondition); pub const LOAD_DELETE: &[Field] = existing!("Load", C::Load); pub const FLUID_DOMAIN_DELETE: &[Field] = existing!("Fluid domain", C::FluidDomain); pub const CONTACT_DELETE: &[Field] = existing!("Contact", C::Contact); pub const TIE_DELETE: &[Field] = existing!("Tie", C::Tie); pub const STEP_DELETE: &[Field] = existing!("Step", C::Step); pub const SET_RESOLVE: &[Field] = existing!("Set", C::Set); // ---- Model definitions ---- pub const SET: &[Field] = &[ NAME, rows( "geometry", "Geometry", &[ reference("body", "Body", C::Body).req(), text("face", "Face").hint("persistent face name"), text("edge", "Edge"), text("vertex", "Vertex"), ], ) .hint("whole bodies, or their faces, edges or vertices"), json("nodes", "Nodes").hint("[labels]"), json("elements", "Elements").hint("[labels]"), json("faces", "Element faces").hint("[{\"element\", \"face\"}]"), json("edges", "Element edges").hint("[{\"element\", \"edge\"}]"), ]; const ELASTIC: Field = variants( "elastic", "Elasticity", &[ variant( "isotropic", "Isotropic", &[num("young", "Young's modulus", D::Stress).req(), range("poisson", "Poisson's ratio", D::None, -1.0, 0.5).req()], ), variant( "engineering_constants", "Orthotropic", &[ vector("young", "E1, E2, E3", D::Stress, &["E1", "E2", "E3"]).req(), vector("poisson", "ν12, ν13, ν23", D::None, &["ν12", "ν13", "ν23"]).req(), vector("shear", "G12, G13, G23", D::Stress, &["G12", "G13", "G23"]).req(), ], ), ], ) .def(r#"{"type": "isotropic"}"#); const HARDENING: &[(&str, &str)] = &[("isotropic", "Isotropic"), ("kinematic", "Kinematic")]; const PLASTIC: Field = variants( "plastic", "Plasticity", &[ variant( "bilinear", "Bilinear", &[ positive("yield_stress", "Yield stress", D::Stress).req(), num("hardening_modulus", "Hardening modulus", D::Stress).def("0"), choice("hardening", "Hardening", HARDENING).def("\"isotropic\""), ], ), variant( "table", "Table", &[rows( "points", "Hardening curve", &[positive("strain", "Plastic strain", D::None).req(), positive("stress", "Stress", D::Stress).req()], ) .req()], ), variant( "johnson_cook", "Johnson–Cook", &[ positive("a", "A", D::Stress).req(), num("b", "B", D::Stress).req(), num("n", "n", D::None).req(), num("c", "C", D::None).def("0"), num("m", "m", D::None).def("1"), positive("reference_strain_rate", "Reference strain rate", D::None).def("1").hint("per time unit"), num("room_temperature", "Room temperature", D::Temperature).req(), num("melt_temperature", "Melt temperature", D::Temperature).req(), positive("specific_heat", "Specific heat", D::SpecificHeat).hint("isothermal when empty"), range("taylor_quinney", "Taylor–Quinney", D::None, 0.0, 1.0).def("0.9"), ], ), ], ); const DAMAGE: Field = variants( "damage", "Damage", &[variant( "johnson_cook", "Johnson–Cook", &[ num("d1", "D1", D::None).req(), num("d2", "D2", D::None).req(), num("d3", "D3", D::None).req(), num("d4", "D4", D::None), num("d5", "D5", D::None), ], )], ); const EROSION: Field = group("erosion", "Erosion", &[positive("plastic_strain", "Plastic strain", D::None).req()]); const HYPERELASTIC: Field = variants( "hyperelastic", "Hyperelasticity", &[ variant("neo_hookean", "Neo-Hookean", &[]), variant("mooney_rivlin", "Mooney–Rivlin", &[num("c01", "C01", D::Stress).req()]), variant("yeoh", "Yeoh", &[num("c20", "C20", D::Stress).req(), num("c30", "C30", D::Stress).req()]), ], ) .hint("large strain in nonlinear steps, from the isotropic constants"); const THERMAL: Field = group( "thermal", "Thermal", &[ json("conductivity", "Conductivity").hint("k or [k1, k2, k3]"), positive("specific_heat", "Specific heat", D::SpecificHeat), json("expansion", "Expansion").hint("α or [α1, α2, α3], 1/K"), num("reference_temperature", "Reference temperature", D::Temperature).def("0"), ], ); pub const MATERIAL: &[Field] = &[ NAME, ELASTIC, positive("density", "Density", D::Density), PLASTIC, HYPERELASTIC, DAMAGE, EROSION, THERMAL, variants("fluid", "Fluid", &[variant("newtonian", "Newtonian", &[positive("viscosity", "Viscosity", D::Viscosity).req()])]) .hint("a fluid needs a density"), ]; pub const MATERIAL_PLASTICITY: &[Field] = &[reference("name", "Material", C::Material).req(), PLASTIC.req().def(r#"{"type": "bilinear"}"#)]; pub const MATERIAL_DAMAGE: &[Field] = &[reference("name", "Material", C::Material).req(), DAMAGE.def(r#"{"type": "johnson_cook"}"#).hint("give damage, erosion or both"), EROSION]; const PROFILE: Field = variants( "profile", "Profile", &[ variant("rectangle", "Rectangle", &[positive("width", "Width", D::Length).req(), positive("height", "Height", D::Length).req()]), variant("circle", "Circle", &[positive("radius", "Radius", D::Length).req()]), variant("pipe", "Pipe", &[positive("radius", "Radius", D::Length).req(), positive("thickness", "Wall", D::Length).req()]), variant( "i", "I-beam", &[ positive("height", "Height", D::Length).req(), positive("width", "Width", D::Length).req(), positive("web", "Web", D::Length).req(), positive("flange", "Flange", D::Length).req(), ], ), variant( "box", "Box", &[ positive("width", "Width", D::Length).req(), positive("height", "Height", D::Length).req(), positive("thickness", "Wall", D::Length).req(), ], ), ], ) .req() .def(r#"{"type": "rectangle"}"#); pub const SECTION: &[Field] = &[ NAME, reference("set", "Region", C::Set).req(), reference("material", "Material", C::Material).req(), variants( "kind", "Kind", &[ variant("solid", "Solid", &[]), variant( "shell", "Shell", &[ positive("thickness", "Thickness", D::Length).req(), range("offset", "Offset", D::None, -0.5, 0.5).def("0").hint("fraction of the thickness"), int("points", "Integration points", 1, 15).def("5"), ], ), variant("beam", "Beam", &[PROFILE, xyz("orientation", "Orientation", D::None).req().def("[0, 0, 1]")]), variant("rod", "Rod", &[positive("area", "Area", D::Area).req()]), variant("plane_stress", "Plane stress", &[positive("thickness", "Thickness", D::Length).req().def("1")]), variant("plane_strain", "Plane strain", &[positive("thickness", "Thickness", D::Length).def("1")]), variant("axisymmetric", "Axisymmetric", &[]), ], ) .req() .def(r#"{"type": "solid"}"#), group( "formulation", "Formulation", &[ choice("integration", "Integration", &[("full", "Full"), ("reduced", "Reduced")]).def("\"full\""), flag("incompatible_modes", "Incompatible modes").def("false"), flag("hybrid", "Hybrid").def("false"), ], ), ]; pub const REFERENCE_POINT: &[Field] = &[ NAME, xyz("xyz", "Position", D::Length).req().def("[0, 0, 0]"), int("label", "Node label", 0, u32::MAX as i64).hint("0: the highest free label"), ]; const DOFS: Field = json("dofs", "Degrees of freedom").hint("[1, …, 6], default all"); pub const COUPLING: &[Field] = &[ NAME, reference("reference_point", "Reference point", C::ReferencePoint).req(), reference("set", "Region", C::Set).req(), choice("kind", "Kind", &[("kinematic", "Kinematic"), ("distributing", "Distributing")]).req().def("\"kinematic\""), DOFS, ]; pub const MASS: &[Field] = &[ NAME, TARGET, positive("mass", "Mass", D::Mass).hint("per node; [mx, my, mz] in JSON"), vector("inertia", "Inertia", D::Inertia, &["I11", "I22", "I33", "I12", "I13", "I23"]).hint("about the node; needs a reference point"), ]; const SPRING_FIRST: Field = target("first", "First end").req(); const SPRING_SECOND: Field = target("second", "Second end").hint("ground when empty"); const SPRING_DIRECTION: Field = either("direction", "Direction", &[int("dof", "DOF", 1, 6), xyz("vector", "Vector", D::None), unit("line", "Along the ends")]) .req() .def(r#"{"dof": 1}"#); pub const SPRING: &[Field] = &[ NAME, variants( "", "Type", &[ variant("spring", "Spring", &[SPRING_FIRST, SPRING_SECOND, SPRING_DIRECTION, positive("stiffness", "Stiffness", D::Stiffness).req()]), variant("dashpot", "Dashpot", &[SPRING_FIRST, SPRING_SECOND, SPRING_DIRECTION, positive("coefficient", "Coefficient", D::Damping).req()]), ], ) .req() .def("\"spring\""), ]; /// The target of a boundary condition, force, moment or temperature. const TARGET: Field = target("target", "Target").req(); pub const BOUNDARY_CONDITION: &[Field] = &[ NAME, variants( "", "Type", &[ variant("fixed", "Fixed", &[TARGET]), variant( "displacement", "Displacement", &[ TARGET, free_vector("values", "Values", D::Length, &["u1", "u2", "u3", "ur1", "ur2", "ur3"]) .req() .def("[0, 0, 0, null, null, null]") .hint("empty: free"), ], ), variant("symmetry", "Symmetry", &[TARGET, choice("plane", "Plane normal", &[("x", "X"), ("y", "Y"), ("z", "Z")]).req().def("\"x\"")]), variant("temperature", "Temperature", &[TARGET, num("value", "Temperature", D::Temperature).req()]), ], ) .req() .def("\"fixed\""), ]; const DISTRIBUTION: Field = choice("distribution", "Distribution", &[("total", "Total"), ("per_node", "Per node")]).def("\"total\""); const LOAD_SET: Field = reference("set", "Region", C::Set).req(); pub const LOAD: &[Field] = &[ NAME, variants( "", "Type", &[ variant("force", "Force", &[TARGET, xyz("vector", "Force", D::Force).req(), DISTRIBUTION]), variant("moment", "Moment", &[TARGET, xyz("vector", "Moment", D::Moment).req(), DISTRIBUTION]), variant("pressure", "Pressure", &[LOAD_SET, num("magnitude", "Pressure", D::Stress).req().hint("positive pushes in")]), variant("traction", "Traction", &[LOAD_SET, xyz("vector", "Traction", D::Stress).req()]), variant( "gravity", "Gravity", &[xyz("acceleration", "Acceleration", D::Acceleration).req(), reference("set", "Region", C::Set).hint("the whole model")], ), variant("body_force", "Body force", &[LOAD_SET, xyz("vector", "Force per volume", D::ForcePerVolume).req()]), variant( "centrifugal", "Centrifugal", &[ num("angular_velocity", "Angular velocity", D::AngularVelocity).req(), xyz("point", "Point on axis", D::Length).req().def("[0, 0, 0]"), xyz("axis", "Axis", D::None).req().def("[0, 0, 1]"), reference("set", "Region", C::Set).hint("the whole model"), ], ), variant("surface_flux", "Surface heat flux", &[LOAD_SET, num("flux", "Flux", D::HeatFlux).req()]), variant( "convection", "Convection", &[LOAD_SET, positive("film", "Film coefficient", D::FilmCoefficient).req(), num("sink", "Sink temperature", D::Temperature).req()], ), variant("heat_source", "Heat source", &[LOAD_SET, num("power", "Power per volume", D::None).req()]), variant("temperature", "Temperature", &[TARGET, num("value", "Temperature", D::Temperature).req()]), ], ) .req() .def("\"force\""), ]; const FACE_CONDITION: &[Variant] = &[ variant("wall", "Wall", &[]), variant("moving_wall", "Moving wall", &[xyz("velocity", "Velocity", D::Velocity).req()]), variant("slip", "Slip", &[]), variant( "velocity", "Velocity inlet", &[ xyz("velocity", "Velocity", D::Velocity).req(), choice("profile", "Profile", &[("uniform", "Uniform"), ("parabolic", "Parabolic")]).def("\"uniform\""), ], ), variant("pressure", "Pressure outlet", &[num("pressure", "Pressure", D::Stress).req().def("0")]), variant("periodic", "Periodic", &[]), ]; const fn face(key: &'static str, label: &'static str) -> Field { variants(key, label, FACE_CONDITION).hint("wall") } pub const FLUID_DOMAIN: &[Field] = &[ NAME, reference("material", "Fluid", C::Material).req(), xyz("min", "Box min", D::Length).req(), xyz("max", "Box max", D::Length).req(), positive("spacing", "Cell size", D::Length).req(), group( "faces", "Box faces", &[ face("x_min", "X min"), face("x_max", "X max"), face("y_min", "Y min"), face("y_max", "Y max"), face("z_min", "Z min"), face("z_max", "Z max"), ], ), references("obstacles", "Obstacles", C::Body), json("surfaces", "Obstacle faces").hint("[{\"face\", \"condition\"}]; walls by default"), xyz("initial_velocity", "Initial velocity", D::Velocity), xyz("acceleration", "Acceleration", D::Acceleration), positive("tolerance", "Obstacle tolerance", D::Length).hint("a tenth of the cell size"), ]; pub const FLUID_BOUNDARY: &[Field] = &[ reference("domain", "Domain", C::FluidDomain).req(), json("at", "Boundary").req().def("\"x_min\"").hint("\"x_min\" … \"z_max\", or {\"body\", \"face\"}"), variants("condition", "Condition", FACE_CONDITION).req().def(r#"{"type": "velocity"}"#), ]; const PROCEDURE: Field = variants( "procedure", "Procedure", &[ variant("static", "Static, linear", &[]), variant( "nonlinear_static", "Static, nonlinear", &[ flag("nonlinear_geometry", "Nonlinear geometry").def("true"), range("initial_increment", "Initial increment", D::None, 0.0, 1.0).def("0.1"), range("min_increment", "Min increment", D::None, 0.0, 1.0).def("1e-5"), range("max_increment", "Max increment", D::None, 0.0, 1.0).def("1.0"), int("max_increments", "Max increments", 1, 1_000_000).def("100"), int("max_iterations", "Iterations per increment", 1, 1000).def("16"), group( "tolerances", "Tolerances", &[ positive("force", "Force", D::None).def("1e-6"), positive("displacement", "Displacement", D::None).def("1e-5"), positive("energy", "Energy", D::None).def("1e-10"), ], ), flag("line_search", "Line search").def("false"), json("load_path", "Load path").hint("[[fraction, factor], …]; a ramp when empty"), group( "imperfection", "Imperfection", &[ reference("step", "Buckling step", C::Step).req(), int("mode", "Mode", 1, 100_000).req().def("1"), num("scale", "Largest component", D::Length).req(), ], ), group( "arc_length", "Arc length", &[ choice("constraint", "Constraint", &[("cylindrical", "Cylindrical"), ("spherical", "Spherical")]).def("\"cylindrical\""), positive("max_load_factor", "Max load factor", D::None).def("1"), json("displacement_limit", "Displacement limit").hint("{\"node\": 1, \"direction\": 3, \"displacement\": -10}"), ], ), ], ), variant("buckle", "Buckling", &[int("modes", "Modes", 1, 100_000).req().def("4"), num("shift", "Shift", D::None)]), variant( "frequency", "Frequency", &[ int("modes", "Modes", 1, 10_000).req().def("10"), positive("min_hz", "Min frequency", D::Hertz), positive("max_hz", "Max frequency", D::Hertz), ], ), variant( "explicit_dynamic", "Dynamic, explicit", &[ positive("end_time", "End time", D::Time).req(), range("dt_scale", "Time step scale", D::None, 0.0, 1.0).def("0.9"), int("max_increments", "Max increments", 1, 10_000_000_000).def("10000000"), positive("output_interval", "Field output every", D::Time), positive("history_interval", "History every", D::Time), reference("history_nodes", "History nodes", C::Set), rows( "initial_velocities", "Initial velocities", &[reference("set", "Set", C::Set).req(), xyz("velocity", "Velocity", D::Velocity).req()], ), rows( "rigid_walls", "Rigid walls", &[ reference("set", "Set", C::Set).req(), xyz("point", "Point", D::Length).req(), xyz("normal", "Normal", D::None).req(), FRICTION, ], ), group( "hourglass", "Hourglass control", &[ choice("type", "Type", &[("viscous", "Viscous"), ("stiffness", "Stiffness")]).def("\"viscous\""), positive("coefficient", "Coefficient", D::None), ], ), group("bulk_viscosity", "Bulk viscosity", &[positive("linear", "Linear", D::None), positive("quadratic", "Quadratic", D::None)]), flag("nonlinear_geometry", "Nonlinear geometry").def("true"), choice("stress_rate", "Stress rate", &[("jaumann", "Jaumann"), ("green_naghdi", "Green–Naghdi")]), group( "mass_scaling", "Mass scaling", &[positive("target_increment", "Target increment", D::Time).req(), reference("set", "Set", C::Set)], ), ], ), variant( "heat_transfer", "Heat transfer", &[group( "transient", "Transient", &[ positive("end_time", "End time", D::Time).req(), positive("increment", "Increment", D::Time).req(), range("theta", "θ", D::None, 0.0, 1.0), num("initial_temperature", "Initial temperature", D::Temperature), ], ) .hint("steady state when off")], ), variant("coupled_temperature_displacement", "Coupled temperature–displacement", &[]), variant( "fluid", "Fluid (lattice Boltzmann)", &[ reference("domain", "Domain", C::FluidDomain).req(), positive("end_time", "End time", D::Time).hint("or Steps"), int("steps", "Steps", 1, 1_000_000_000_000), positive("time_step", "Time step", D::Time).hint("chosen for Mach 0.087"), int("frames", "Frames", 1, 100_000).def("10"), positive("magic", "TRT Λ", D::None).hint("0.1875"), ], ), ], ); const OUTPUT: Field = json("output", "Output").hint("[\"displacement\", \"stress\", …]; by procedure when empty"); const STEP_BCS: Field = references("boundary_conditions", "Boundary conditions", C::BoundaryCondition); const STEP_LOADS: Field = references("loads", "Loads", C::Load); pub const STEP_CREATE: &[Field] = &[NAME, PROCEDURE.req().def(r#"{"type": "static"}"#), OUTPUT, STEP_BCS, STEP_LOADS, reference("before", "Insert before", C::Step).hint("last")]; pub const STEP_EDIT: &[Field] = &[reference("name", "Step", C::Step).req(), PROCEDURE, OUTPUT, STEP_BCS, STEP_LOADS]; pub const STEP_ACTIVATE: &[Field] = &[ reference("step", "Step", C::Step).req(), references("boundary_conditions", "Boundary conditions", C::BoundaryCondition), references("loads", "Loads", C::Load), flag("active", "Active").def("true"), ]; pub const UNITS: &[Field] = &[choice( "system", "Unit system", &[ ("mm-t-s-N-MPa", "mm, t, s, N, MPa"), ("m-kg-s-N-Pa", "m, kg, s, N, Pa"), ("mm-kg-ms-kN-GPa", "mm, kg, ms, kN, GPa"), ("mm-g-ms-N-MPa", "mm, g, ms, N, MPa"), ("in-lbf-s-psi", "in, lbf·s²/in, s, lbf, psi"), ("unspecified", "Unspecified"), ], ) .req() .def("\"mm-t-s-N-MPa\"")]; // ---- Contact ---- const FRICTION: Field = group( "friction", "Friction", &[ positive("static", "Static μ", D::None).hint("0"), positive("kinetic", "Kinetic μ", D::None).hint("the static μ"), positive("decay", "Decay", D::None).hint("0; per slip rate"), ], ) .hint("frictionless"); const PENALTY: Field = range("penalty_scale", "Penalty scale", D::None, 0.0, 1.0).def("1"); const PENETRATION: Field = choice("initial_penetration", "Initial penetration", &[("adjust", "Adjust nodes"), ("refuse", "Refuse")]).def("\"adjust\""); pub const CONTACT_PAIR: &[Field] = &[ NAME, reference("main", "Main surface", C::Set).req(), reference("secondary", "Secondary surface", C::Set).req(), FRICTION, PENALTY, PENETRATION, ]; pub const CONTACT_GENERAL: &[Field] = &[NAME, reference("surface", "Surface", C::Set).hint("every exterior face"), FRICTION, PENALTY, PENETRATION]; pub const CONTACT_EDIT: &[Field] = &[ reference("name", "Contact", C::Contact).req(), variants( "surfaces", "Surfaces", &[ variant("pair", "Pair", &[reference("main", "Main", C::Set).req(), reference("secondary", "Secondary", C::Set).req()]), variant("general", "General", &[reference("surface", "Surface", C::Set).hint("every exterior face")]), ], ) .req(), FRICTION, PENALTY, PENETRATION, ]; pub const TIE: &[Field] = &[ NAME, reference("main", "Main surface", C::Set).req(), reference("secondary", "Secondary surface", C::Set).req(), positive("position_tolerance", "Position tolerance", D::Length).hint("half the main element's thickness"), ]; pub const TIE_EDIT: &[Field] = &[ reference("name", "Tie", C::Tie).req(), reference("main", "Main surface", C::Set).req(), reference("secondary", "Secondary surface", C::Set).req(), positive("position_tolerance", "Position tolerance", D::Length).hint("half the main element's thickness"), ]; pub const CONTACT_FIND: &[Field] = &[ positive("tolerance", "Tolerance", D::Length).hint("a tenth of the mean face size"), flag("create", "Create pairs").def("false"), FRICTION, PENALTY, PENETRATION, ]; // ---- Meshing ---- const BODY: Field = reference("body", "Body", C::Body).hint("every body of the design"); const SURFACE: Field = int("surface", "Imported surface", 0, 1_000_000).hint("instead of a body"); const TOLERANCE: Field = positive("tolerance", "Chordal tolerance", D::Millimetre).hint("1e-3 of the diagonal"); const WELD: Field = range("weld", "Weld", D::None, 0.0, 1e-3).hint("1e-9 of the diagonal"); const WELD_DISTANCE: Field = positive("weld_distance", "Weld distance", D::Millimetre).hint("the surface's own"); const FEATURE_ANGLE: Field = range("feature_angle", "Feature angle", D::Degree, 0.0, 180.0).hint("45"); pub const TET: &[Field] = &[ BODY, SURFACE, positive("size", "Element size", D::Millimetre).hint("1/20 of the diagonal"), choice("order", "Order", &[("linear", "Linear (tet4)"), ("quadratic", "Quadratic (tet10)")]).def("\"linear\""), TOLERANCE, flag("quality", "Improve quality").def("true"), range("quality_target", "Quality target", D::Degree, 5.0, 60.0).hint("40"), range("curvature_angle", "Curvature angle", D::Degree, 0.0, 90.0).hint("22.5"), range("grading", "Grading", D::None, 0.05, 2.0).hint("0.3"), int("max_passes", "Improvement passes", 0, 200).hint("20"), int("max_points", "Max points", 1, 20_000_000).hint("1000000"), int("max_steiner", "Max Steiner points", 0, 10_000_000).hint("50000"), int("max_tets", "Max tetrahedra", 1, 50_000_000).hint("5000000"), WELD, WELD_DISTANCE, FEATURE_ANGLE, ]; const HEX_SIZE: Field = positive("size", "Element size", D::Millimetre).hint("1/25 of the diagonal"); const HEX_ORDER: Field = int("order", "Order", 1, 2).def("1").hint("1: hex8, 2: hex20"); pub const HEX_GENERATE: &[Field] = &[ BODY, SURFACE, HEX_SIZE, HEX_ORDER, choice( "method", "Method", &[("auto", "Automatic"), ("swept", "Any sweep"), ("extrude", "Extrude"), ("sweep", "Sweep"), ("revolve", "Revolve"), ("mapped", "Mapped")], ) .def("\"auto\""), TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE, ]; pub const HEX_SWEEP: &[Field] = &[ BODY, SURFACE, HEX_SIZE, HEX_ORDER, choice("method", "Method", &[("swept", "Any sweep"), ("extrude", "Extrude"), ("sweep", "Sweep"), ("revolve", "Revolve")]).def("\"swept\""), TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE, ]; pub const HEX_MAPPED: &[Field] = &[BODY, SURFACE, HEX_SIZE, HEX_ORDER, TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE]; pub const HEX_BLOCKS: &[Field] = &[references("bodies", "Cells", C::Body).hint("every body of the design"), HEX_SIZE, HEX_ORDER, TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE]; pub const CELLS: &[Field] = &[ json("methods", "Methods").hint(r#"{"cell": "auto | hex | swept | extrude | sweep | revolve | mapped | tet"}"#), choice("default", "Other cells", &[("auto", "Auto (hex, else tet)"), ("hex", "Hex"), ("swept", "Swept"), ("mapped", "Mapped"), ("tet", "Tet")]) .def("\"auto\""), references("bodies", "Cells", C::Body).hint("every body of the design"), HEX_SIZE, HEX_ORDER, TOLERANCE, ]; pub const MIRROR: &[Field] = &[ json("plane", "Plane").req().hint(r#""Datum" or {"point": [x, y, z], "normal": [nx, ny, nz]}"#), references("bodies", "Bodies", C::Body).hint("every meshed body"), json("as", "Images").hint(r#"{"body": "image body"}; default: the design's body at the reflected place"#), ]; pub const SYMMETRY: &[Field] = &[ NAME, reference("datum", "Datum plane", C::DatumPlane).req(), reference("set", "Set", C::Set).hint("the faces of the design on the plane"), references("bodies", "Bodies", C::Body).hint("every body"), ]; pub const HEX_OCTREE: &[Field] = &[ BODY, SURFACE, HEX_SIZE, TOLERANCE, int("max_depth", "Refinement levels", 0, 5).hint("3"), int("max_elements", "Max elements", 1, 10_000_000), int("attempts", "Attempts", 1, 8).hint("4"), int("max_sweeps", "Optimiser sweeps", 0, 10_000).hint("300"), WELD, WELD_DISTANCE, FEATURE_ANGLE, ]; pub const BLOCK: &[Field] = &[ text("body", "Body").req().def("\"Block\""), choice( "kind", "Element", &[ ("hex8", "hex8"), ("hex20", "hex20"), ("tet4", "tet4"), ("tet10", "tet10"), ("wedge6", "wedge6"), ("quad4", "quad4 (2D)"), ("quad8", "quad8 (2D)"), ("tri3", "tri3 (2D)"), ("tri6", "tri6 (2D)"), ], ) .req() .def("\"hex8\""), json("corners", "Corners").req().hint("[[x, y, z] × 8] (2D: × 4 at z = 0)"), json("divisions", "Divisions").req().hint("[n12, n14, n15] (2D: [n12, n14])"), json("bias", "Bias").hint("[1, 1, 1]"), json("edges", "Curved and named edges"), json("faces", "Face names").hint("{\"1\": \"bottom\"}"), json("vertices", "Corner names"), text("face", "2D face name").hint("area"), positive("merge", "Merge distance", D::Length).hint("1e-6 of the diagonal"), ]; pub const PLANAR: &[Field] = &[ positive("size", "Element size", D::Length).req(), choice("kind", "Element", &[("quad4", "quad4"), ("quad8", "quad8"), ("tri3", "tri3"), ("tri6", "tri6")]).def("\"quad4\""), BODY, text("face", "Face").hint("a planar face of the body"), TOLERANCE, json("polygon", "Polygon").hint("[[x, y], …] instead of a face"), json("holes", "Holes"), text("name", "Body name").hint("Plane"), int("max_elements", "Max elements", 1, 4_000_000), ]; pub const QUALITY: &[Field] = &[json("elements", "Elements").hint("[labels]; all when empty"), int("bins", "Histogram bins", 1, 1000).def("10")]; pub const SURFACE_CHECK: &[Field] = &[reference("body", "Body", C::Body).req().hint("or an imported surface"), SURFACE, TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE]; pub const NODE_ADD: &[Field] = &[int("label", "Label", 1, u32::MAX as i64).req(), xyz("xyz", "Position", D::Length).req().def("[0, 0, 0]")]; pub const LABEL: &[Field] = &[int("label", "Label", 1, u32::MAX as i64).req()]; pub const ELEMENT_ADD: &[Field] = &[ int("label", "Label", 1, u32::MAX as i64).req(), choice( "kind", "Kind", &[ ("rod2", "rod2"), ("beam2", "beam2"), ("tri3", "tri3"), ("tri6", "tri6"), ("quad4", "quad4"), ("quad8", "quad8"), ("tet4", "tet4"), ("tet10", "tet10"), ("wedge6", "wedge6"), ("hex8", "hex8"), ("hex20", "hex20"), ], ) .req() .def("\"hex8\""), json("nodes", "Nodes").req().hint("[labels]"), ]; pub const SEED_EDGE: &[Field] = &[ json("edges", "Edges").req().hint("[{\"body\", \"edge\"}, …] — Use selection with Select: Edges"), int("count", "Count", 1, 100_000).hint("or an element size"), positive("size", "Element size", D::Length).hint("or a count"), group( "bias", "Bias", &[ range("ratio", "Ratio", D::None, 1.0, 1e4).req().def("1"), choice("toward", "Small toward", &[("start", "Start"), ("end", "End"), ("both", "Both ends")]).req().def("\"end\""), ], ), choice("constraint", "Constraint", &[("exact", "Exact"), ("min", "At least"), ("free", "Free (a goal)")]).def("\"exact\""), ]; pub const SEED_CLEAR: &[Field] = &[json("edges", "Edges").hint("every seed")]; pub const DELAUNAY_POINTS: &[Field] = &[json("points", "Points").req().hint("[[x, y, z], …]")]; pub const TAG: &[Field] = &[ group("geometry", "Geometry", &[reference("body", "Body", C::Body).req(), text("face", "Face"), text("edge", "Edge"), text("vertex", "Vertex")]) .req(), json("elements", "Elements"), json("faces", "Element faces"), json("edges", "Element edges"), json("nodes", "Nodes"), ]; // ---- Geometry and files ---- pub const OPEN_DESIGN: &[Field] = &[ path("path", "Design", false).req(), choice("model", "Model", &[("keep", "Keep the current model"), ("new", "Start a new model")]).def("\"keep\""), ]; const POINT: Dim = D::Millimetre; pub const DATUM_PLANE: &[Field] = &[ NAME, variants( "by", "Definition", &[ variant( "offset", "Principal plane", &[ choice("plane", "Plane", &[("yz", "YZ (normal X)"), ("zx", "ZX (normal Y)"), ("xy", "XY (normal Z)")]).req(), num("offset", "Offset", POINT).def("0"), ], ), variant( "three_points", "Three points", &[xyz("a", "Point A", POINT).req(), xyz("b", "Point B", POINT).req(), xyz("c", "Point C", POINT).req()], ), variant("point_normal", "Point and normal", &[xyz("point", "Point", POINT).req(), xyz("normal", "Normal", D::None).req()]), ], ) .req() .def(r#"{"type": "offset"}"#), ]; pub const DATUM_AXIS: &[Field] = &[ NAME, variants( "by", "Definition", &[ variant( "principal", "Principal axis", &[choice("axis", "Axis", &[("x", "X"), ("y", "Y"), ("z", "Z")]).req(), xyz("through", "Through", POINT).hint("the origin")], ), variant("two_points", "Two points", &[xyz("a", "Point A", POINT).req(), xyz("b", "Point B", POINT).req()]), variant("point_direction", "Point and direction", &[xyz("point", "Point", POINT).req(), xyz("direction", "Direction", D::None).req()]), ], ) .req() .def(r#"{"type": "principal"}"#), ]; pub const DATUM_DELETE: &[Field] = existing!("Datum", C::Datum); pub const PARTITION: &[Field] = &[NAME, reference("datum", "Datum plane", C::DatumPlane).req(), references("bodies", "Bodies", C::Body).hint("every body the plane divides")]; pub const PARTITION_DELETE: &[Field] = existing!("Partition", C::Partition); pub const TESSELLATE: &[Field] = &[reference("body", "Body", C::Body).req(), TOLERANCE]; pub const IMPORT_MESH: &[Field] = &[path("path", "3MF file", false).req()]; pub const FILE_OPEN: &[Field] = &[path("path", "Model", false).req()]; pub const FILE_SAVE: &[Field] = &[path("path", "File", true).hint("the model's file")]; pub const FILE_SAVE_AS: &[Field] = &[path("path", "File", true).req()]; pub const RECOVERY_LIST: &[Field] = &[path("dir", "Folder", false).hint("the recovery folder")]; pub const RECOVER: &[Field] = &[text("id", "Autosave").req(), path("dir", "Folder", false).hint("the recovery folder")]; pub const RECOVERY_DISCARD: &[Field] = &[text("id", "Autosave"), flag("all", "All"), path("dir", "Folder", false).hint("the recovery folder")]; pub const SAMPLE: &[Field] = &[choice( "name", "Sample", &[ ("cantilever", "Cantilever"), ("impact", "Impact"), ("parts", "Parts"), ("drop-test", "Drop test"), ("rubber-seal", "Rubber seal"), ("flow-past", "Flow past a bracket"), ("heated-bracket", "Heated bracket"), ("bracket-hex", "Hex-meshed bracket"), ("crank-arm", "Bicycle crank arm"), ("crank-mirror", "Mirrored crank arm"), ], ) .req() .def("\"cantilever\"")]; pub const COUNT: &[Field] = &[int("count", "Count", 1, 1_000_000).def("1")]; pub const DECK: &[Field] = &[ choice("format", "Format", &[("abaqus", "Abaqus input (.inp)"), ("nastran", "Nastran bulk data (.bdf)"), ("lsdyna", "LS-DYNA keyword (.k)")]) .req() .def("\"abaqus\""), path("path", "File", true).req(), references("steps", "Steps", C::Step).hint("all"), ]; // ---- Solving ---- const STEP: Field = reference("step", "Step", C::Step).hint("the only one"); const SOLVED: Field = reference("step", "Step", C::SolvedStep).hint("the step shown"); const MAX_DOFS: Field = int("max_dofs", "Max DOFs", 1, 6_000_000).hint("6000000"); const MAX_FACTOR: Field = int("max_factor_entries", "Max factor entries", 1, 400_000_000).hint("400000000"); const THREADS: Field = int("threads", "Threads", 1, 256).hint("the session's"); pub const SOLVE_STATIC: &[Field] = &[STEP, MAX_DOFS, MAX_FACTOR, THREADS]; pub const SOLVE_NONLINEAR: &[Field] = &[STEP, MAX_DOFS, MAX_FACTOR, int("max_newton_iterations", "Max Newton iterations", 1, 100_000).hint("100000"), THREADS]; pub const SOLVE_FREQUENCY: &[Field] = &[ STEP, choice("mass", "Mass matrix", &[("consistent", "Consistent"), ("lumped", "Lumped")]).def("\"consistent\""), num("shift", "Shift", D::None).hint("0"), MAX_DOFS, MAX_FACTOR, int("max_iterations", "Max Lanczos vectors", 1, 3000).hint("3000"), THREADS, ]; pub const SOLVE_BUCKLE: &[Field] = &[STEP, MAX_DOFS, MAX_FACTOR, int("max_iterations", "Max Lanczos vectors", 1, 3000).hint("3000"), THREADS]; pub const SOLVE_EXPLICIT: &[Field] = &[ STEP, MAX_DOFS, int("max_increments", "Max increments", 1, 1_000_000_000).hint("1000000000"), int("max_output_values", "Max output values", 1, 50_000_000).hint("50000000"), int("max_contact_faces", "Max contact faces", 1, 10_000_000).hint("10000000"), THREADS, ]; pub const SOLVE_HISTORY: &[Field] = &[SOLVED.req(), int("every", "Keep one in", 1, 1_000_000_000)]; pub const SOLVE_FLUID: &[Field] = &[ STEP, int("max_cells", "Max cells", 1, 16_000_000), int("max_steps", "Max steps", 1, 10_000_000), positive("max_cell_updates", "Max cell updates", D::None), positive("max_frame_bytes", "Max frame bytes", D::None), THREADS, ]; pub const SOLVE_PROBE: &[Field] = &[ SOLVED.req(), json("nodes", "Nodes").hint("[labels]"), reference("set", "Set", C::Set), json("at", "Points").hint("[[x, y, z], …]: nearest nodes"), int("mode", "Mode", 1, 1_000_000), int("frame", "Frame", 0, 1_000_000_000), flag("elements", "Element results"), json("points", "Fluid points").hint("[[x, y, z], …]"), json("forces", "Fluid walls").hint("[\"x_min\", \"F2:top\"]"), ]; // ---- Results ---- pub const DEFORMED: &[Field] = &[SOLVED, flag("show", "Show").def("true"), json("scale", "Scale").def("\"auto\"").hint("\"auto\" or a factor")]; pub const CONTOUR: &[Field] = &[ SOLVED, choice( "field", "Field", &[ ("von_mises", "Von Mises stress"), ("displacement", "Displacement"), ("displacement_x", "Displacement X"), ("displacement_y", "Displacement Y"), ("displacement_z", "Displacement Z"), ("max_principal", "Max principal"), ("mid_principal", "Mid principal"), ("min_principal", "Min principal"), ("stress_xx", "Stress XX"), ("stress_yy", "Stress YY"), ("stress_zz", "Stress ZZ"), ("stress_xy", "Stress XY"), ("stress_xz", "Stress XZ"), ("stress_yz", "Stress YZ"), ("contact_pressure", "Contact pressure"), ("contact_gap", "Contact gap"), ("contact_status", "Contact status"), ("contact_shear", "Contact shear"), ("contact_slip", "Contact slip"), ("none", "None"), ], ) .req() .def("\"von_mises\""), int("bands", "Bands", 2, 24).def("12"), ]; pub const MODES: &[Field] = &[SOLVED, int("mode", "Mode", 1, 1_000_000).def("1")]; pub const ANIMATE: &[Field] = &[flag("on", "Animate").def("true")]; pub const FRAME: &[Field] = &[SOLVED, int("frame", "Frame", 0, 1_000_000_000), positive("time", "Time", D::Time).hint("the last frame")]; pub const REACTIONS: &[Field] = &[SOLVED, references("sets", "Sets", C::Set).hint("the step's BC sets"), xyz("about", "About point", D::Length)]; #[cfg(test)] mod tests { use serde_json::{Value, json}; use super::*; use crate::{EngineError, Session}; /// The form's starting parameters (and those with the required names and references filled /// in from the model) are accepted, or refused only for a required field still empty; never /// for a value the form put there itself. Errors other than parameter errors (no design, no /// results) come after the parameters were read. #[test] fn form_defaults_are_accepted_or_need_only_required_fields() { let (mut checked, mut failures) = (0, Vec::new()); for c in crate::commands() { let Some(form) = c.form else { continue }; if c.id.starts_with("file.") && c.id != "file.save_as" || c.id == "edit.undo" || c.id == "edit.redo" { continue; // they replace or write the session's model; their forms are paths and counts } for fill_refs in [false, true] { let limits = crate::Limits { max_steiner: 10, max_tets: 100, max_hex_elements: 100, threads: 1 }; let mut s = Session { limits, ..Session::default() }; crate::cmd::define::tests::defined_bar(&mut s); s.execute("solve.run", &json!({})).unwrap(); let mut p = prefill(form, &json!({})); if fill_refs { fill_required(&s, form, &mut p); } let missing: Vec = problems(form, &p).into_iter().filter(|p| p.message == "required").map(|p| p.path).collect(); let r = s.execute(c.id, &p).map_err(|e| match e { EngineError::Param { .. } => EngineError::Params(e.to_string()), e => e, }); match r { // State, not parameters: the meshers' default is the open design's bodies, the // bar has no explicit step; activation and probes need one of several fields. Err(EngineError::Params(e)) if STATE.iter().any(|m| e.contains(m)) => {} Err(EngineError::Params(e)) if !missing.iter().any(|k| e.contains(k.rsplit('/').next().unwrap_or(k))) => { failures.push(format!("{}: {p} refused for `{e}`, but only {missing:?} were missing", c.id)); } Err(EngineError::Params(_)) => {} _ => checked += 1, } } } assert!(failures.is_empty(), "{failures:#?}"); assert!(checked > 20, "{checked}"); } /// The commands whose form is the JSON text alone (no schema yet). None: keep it that way. #[test] fn every_command_has_a_form() { let without: Vec<&str> = crate::commands().iter().filter(|c| c.form.is_none()).map(|c| c.id).collect(); assert_eq!(without, Vec::<&str>::new()); // A command that takes nothing has an empty form, and says so. for c in crate::commands() { assert_eq!(c.form.is_some_and(<[Field]>::is_empty), c.params.trim_start().starts_with("{}"), "{}", c.id); } } const STATE: &[&str] = &["open a design first", "`boundary_conditions` or `loads`", "give one of", "is not an explicit", "has a result"]; /// Fill top-level required names (a fresh one) and references (the first existing item). fn fill_required(s: &Session, form: &[Field], p: &mut Value) { let Some(obj) = p.as_object_mut() else { return }; for f in form { if !f.required || obj.contains_key(f.key) { continue; } match f.kind { Kind::Text => { obj.insert(f.key.into(), json!("Fresh")); } Kind::Ref(c) => { if let Some(n) = s.names(c).first() { obj.insert(f.key.into(), json!(n)); } } _ => {} } } } /// Every default is JSON; every variant field's default names a variant. #[test] fn defaults_parse() { fn walk(fields: &[Field]) { for f in fields { if let Some(d) = f.default { assert!(serde_json::from_str::(d).is_ok(), "{}: {d}", f.key); } match f.kind { Kind::Group(g) | Kind::Rows { fields: g, .. } | Kind::Either(g) => walk(g), Kind::Variants { variants, .. } => variants.iter().for_each(|v| walk(v.fields)), _ => {} } } } for c in crate::commands() { if let Some(form) = c.form { walk(form); } } } }