structural-analysis

📁 vamseeachanta/workspace-hub 📅 5 days ago
4
总安装量
4
周安装量
#48495
全站排名
安装命令
npx skills add https://github.com/vamseeachanta/workspace-hub --skill structural-analysis

Agent 安装分布

opencode 4
gemini-cli 4
antigravity 4
github-copilot 4
codex 4
kimi-cli 4

Skill 文档

Structural Analysis Skill

Perform structural analysis for offshore and marine structures including stress calculations, buckling checks, and capacity verification.

Version Metadata

version: 1.0.0
python_min_version: '3.10'
compatibility:
  tested_python:
  - '3.10'
  - '3.11'
  - '3.12'
  - '3.13'
  os:
  - Windows
  - Linux
  - macOS

Changelog

[1.0.0] – 2026-01-07

Added:

  • Initial version metadata and dependency management
  • Semantic versioning support
  • Compatibility information for Python 3.10-3.13

Changed:

  • Enhanced skill documentation structure

When to Use

  • Von Mises stress calculations
  • Plate buckling checks (DNV, API standards)
  • Member capacity verification
  • Combined loading assessment
  • Weld strength verification
  • Safety factor reporting
  • Standards compliance documentation

Supported Standards

Standard Application
DNV-RP-C201 Buckling strength of plated structures
DNV-RP-C202 Buckling strength of shells
DNV-RP-C203 Fatigue design
API RP 2A Fixed offshore platforms
ISO 19902 Fixed steel offshore structures
AISC 360 Steel construction
Eurocode 3 Steel structures

Implementation Pattern

Stress Calculations

from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
import numpy as np
import logging

logger = logging.getLogger(__name__)


@dataclass
class StressState:
    """Complete stress state at a point."""
    sigma_x: float = 0.0    # Normal stress in x direction (MPa)
    sigma_y: float = 0.0    # Normal stress in y direction (MPa)
    sigma_z: float = 0.0    # Normal stress in z direction (MPa)
    tau_xy: float = 0.0     # Shear stress xy (MPa)
    tau_xz: float = 0.0     # Shear stress xz (MPa)
    tau_yz: float = 0.0     # Shear stress yz (MPa)

    def von_mises(self) -> float:
        """Calculate Von Mises equivalent stress."""
        return np.sqrt(
            0.5 * (
                (self.sigma_x - self.sigma_y)**2 +
                (self.sigma_y - self.sigma_z)**2 +
                (self.sigma_z - self.sigma_x)**2 +
                6 * (self.tau_xy**2 + self.tau_xz**2 + self.tau_yz**2)
            )
        )

    def principal_stresses(self) -> Tuple[float, float, float]:
        """Calculate principal stresses."""
        # Build stress tensor
        tensor = np.array([
            [self.sigma_x, self.tau_xy, self.tau_xz],
            [self.tau_xy, self.sigma_y, self.tau_yz],
            [self.tau_xz, self.tau_yz, self.sigma_z]
        ])

        # Eigenvalues are principal stresses
        eigenvalues = np.linalg.eigvalsh(tensor)
        return tuple(sorted(eigenvalues, reverse=True))

    def max_shear(self) -> float:
        """Calculate maximum shear stress."""
        s1, s2, s3 = self.principal_stresses()
        return (s1 - s3) / 2


@dataclass
class MaterialProperties:
    """Material properties for structural analysis."""
    yield_strength: float     # MPa
    ultimate_strength: float  # MPa
    youngs_modulus: float     # MPa
    poissons_ratio: float
    density: float            # kg/m³
    name: str = "Steel"


# Common materials
STEEL_S355 = MaterialProperties(
    yield_strength=355,
    ultimate_strength=510,
    youngs_modulus=210000,
    poissons_ratio=0.3,
    density=7850,
    name="S355"
)

STEEL_S420 = MaterialProperties(
    yield_strength=420,
    ultimate_strength=520,
    youngs_modulus=210000,
    poissons_ratio=0.3,
    density=7850,
    name="S420"
)


class StressCalculator:
    """Calculate stresses in structural members."""

    def __init__(self, material: MaterialProperties):
        self.material = material

    def beam_stress(
        self,
        axial_force: float,
        moment_y: float,
        moment_z: float,
        area: float,
        I_y: float,
        I_z: float,
        y: float,
        z: float
    ) -> float:
        """
        Calculate bending stress in a beam.

        Args:
            axial_force: Axial force (N)
            moment_y: Moment about y-axis (N·m)
            moment_z: Moment about z-axis (N·m)
            area: Cross-sectional area (m²)
            I_y: Moment of inertia about y (m⁴)
            I_z: Moment of inertia about z (m⁴)
            y: Distance from neutral axis in y (m)
            z: Distance from neutral axis in z (m)

        Returns:
            Normal stress (MPa)
        """
        sigma_axial = axial_force / area / 1e6  # Convert to MPa
        sigma_bending_y = moment_y * z / I_y / 1e6
        sigma_bending_z = moment_z * y / I_z / 1e6

        return sigma_axial + sigma_bending_y + sigma_bending_z

    def shear_stress(
        self,
        shear_force: float,
        Q: float,
        I: float,
        t: float
    ) -> float:
        """
        Calculate shear stress using VQ/It formula.

        Args:
            shear_force: Shear force (N)
            Q: First moment of area (m³)
            I: Moment of inertia (m⁴)
            t: Thickness at section (m)

        Returns:
            Shear stress (MPa)
        """
        return shear_force * Q / (I * t) / 1e6

    def torsional_stress(
        self,
        torque: float,
        r: float,
        J: float
    ) -> float:
        """
        Calculate torsional shear stress.

        Args:
            torque: Applied torque (N·m)
            r: Radial distance from center (m)
            J: Polar moment of inertia (m⁴)

        Returns:
            Shear stress (MPa)
        """
        return torque * r / J / 1e6

    def hoop_stress(
        self,
        pressure: float,
        radius: float,
        thickness: float
    ) -> float:
        """
        Calculate hoop stress in thin-walled cylinder.

        Args:
            pressure: Internal pressure (MPa)
            radius: Inner radius (m)
            thickness: Wall thickness (m)

        Returns:
            Hoop stress (MPa)
        """
        return pressure * radius / thickness

    def longitudinal_stress(
        self,
        pressure: float,
        radius: float,
        thickness: float
    ) -> float:
        """
        Calculate longitudinal stress in thin-walled cylinder.

        Args:
            pressure: Internal pressure (MPa)
            radius: Inner radius (m)
            thickness: Wall thickness (m)

        Returns:
            Longitudinal stress (MPa)
        """
        return pressure * radius / (2 * thickness)

Buckling Analysis

@dataclass
class PlateGeometry:
    """Plate geometry for buckling analysis."""
    length: float       # a (mm)
    width: float        # b (mm)
    thickness: float    # t (mm)


@dataclass
class BucklingResult:
    """Results from buckling analysis."""
    critical_stress: float    # MPa
    applied_stress: float     # MPa
    utilization: float
    safety_factor: float
    mode: str
    passes: bool


class PlateBucklingAnalyzer:
    """
    Plate buckling analysis per DNV-RP-C201.
    """

    def __init__(self, material: MaterialProperties):
        self.material = material
        self.E = material.youngs_modulus
        self.nu = material.poissons_ratio
        self.fy = material.yield_strength

    def elastic_buckling_stress(
        self,
        plate: PlateGeometry,
        boundary_conditions: str = "simply_supported"
    ) -> float:
        """
        Calculate elastic buckling stress.

        Args:
            plate: Plate geometry
            boundary_conditions: Boundary condition type

        Returns:
            Elastic buckling stress (MPa)
        """
        a = plate.length
        b = plate.width
        t = plate.thickness

        # Aspect ratio
        alpha = a / b

        # Buckling coefficient (simply supported, uniform compression)
        if alpha < 1:
            k = (alpha + 1/alpha)**2
        else:
            k = 4.0

        # Elastic buckling stress
        sigma_e = k * np.pi**2 * self.E / (12 * (1 - self.nu**2)) * (t / b)**2

        return sigma_e

    def reduced_slenderness(
        self,
        plate: PlateGeometry
    ) -> float:
        """
        Calculate reduced slenderness parameter.

        Args:
            plate: Plate geometry

        Returns:
            Reduced slenderness (lambda_p)
        """
        sigma_e = self.elastic_buckling_stress(plate)
        return np.sqrt(self.fy / sigma_e)

    def johnson_ostenfeld(
        self,
        sigma_e: float
    ) -> float:
        """
        Apply Johnson-Ostenfeld correction for inelastic buckling.

        Args:
            sigma_e: Elastic buckling stress

        Returns:
            Critical buckling stress
        """
        if sigma_e <= 0.5 * self.fy:
            return sigma_e
        else:
            return self.fy * (1 - self.fy / (4 * sigma_e))

    def check_plate_buckling(
        self,
        plate: PlateGeometry,
        sigma_x: float,
        sigma_y: float = 0.0,
        tau: float = 0.0,
        gamma_m: float = 1.15
    ) -> BucklingResult:
        """
        Check plate buckling under combined loading.

        Args:
            plate: Plate geometry
            sigma_x: Compressive stress in x (MPa, positive = compression)
            sigma_y: Compressive stress in y (MPa)
            tau: Shear stress (MPa)
            gamma_m: Material factor

        Returns:
            BucklingResult with utilization
        """
        # Calculate individual buckling stresses
        b = plate.width
        t = plate.thickness

        # Compressive buckling
        sigma_e_x = self.elastic_buckling_stress(plate)
        sigma_cr_x = self.johnson_ostenfeld(sigma_e_x)

        # Shear buckling
        k_tau = 5.34 + 4 * (b / plate.length)**2
        tau_e = k_tau * np.pi**2 * self.E / (12 * (1 - self.nu**2)) * (t / b)**2
        tau_cr = self.johnson_ostenfeld(tau_e)

        # Combined check (interaction formula)
        util_x = sigma_x / (sigma_cr_x / gamma_m) if sigma_cr_x > 0 else 0
        util_tau = (tau / (tau_cr / gamma_m))**2 if tau_cr > 0 else 0

        total_util = util_x + util_tau

        return BucklingResult(
            critical_stress=sigma_cr_x,
            applied_stress=sigma_x,
            utilization=total_util,
            safety_factor=1 / total_util if total_util > 0 else float('inf'),
            mode="plate_buckling",
            passes=total_util <= 1.0
        )


class ColumnBucklingAnalyzer:
    """
    Column buckling analysis per Eurocode 3.
    """

    def __init__(self, material: MaterialProperties):
        self.material = material
        self.E = material.youngs_modulus
        self.fy = material.yield_strength

    def euler_buckling_load(
        self,
        I: float,
        L_eff: float
    ) -> float:
        """
        Calculate Euler critical buckling load.

        Args:
            I: Moment of inertia (mm⁴)
            L_eff: Effective length (mm)

        Returns:
            Critical load (N)
        """
        return np.pi**2 * self.E * I / L_eff**2

    def slenderness_ratio(
        self,
        L_eff: float,
        r: float
    ) -> float:
        """
        Calculate slenderness ratio.

        Args:
            L_eff: Effective length (mm)
            r: Radius of gyration (mm)

        Returns:
            Slenderness ratio
        """
        return L_eff / r

    def reduction_factor(
        self,
        lambda_bar: float,
        buckling_curve: str = "b"
    ) -> float:
        """
        Calculate buckling reduction factor per EC3.

        Args:
            lambda_bar: Non-dimensional slenderness
            buckling_curve: EC3 buckling curve (a0, a, b, c, d)

        Returns:
            Reduction factor chi
        """
        # Imperfection factors
        alpha_dict = {
            "a0": 0.13,
            "a": 0.21,
            "b": 0.34,
            "c": 0.49,
            "d": 0.76
        }
        alpha = alpha_dict.get(buckling_curve, 0.34)

        # Calculate reduction factor
        phi = 0.5 * (1 + alpha * (lambda_bar - 0.2) + lambda_bar**2)
        chi = 1 / (phi + np.sqrt(phi**2 - lambda_bar**2))

        return min(chi, 1.0)

    def check_column_buckling(
        self,
        axial_force: float,
        area: float,
        I_min: float,
        L_eff: float,
        buckling_curve: str = "b",
        gamma_m: float = 1.0
    ) -> BucklingResult:
        """
        Check column buckling capacity.

        Args:
            axial_force: Applied axial force (N)
            area: Cross-sectional area (mm²)
            I_min: Minimum moment of inertia (mm⁴)
            L_eff: Effective length (mm)
            buckling_curve: EC3 curve
            gamma_m: Material factor

        Returns:
            BucklingResult
        """
        # Calculate slenderness
        r = np.sqrt(I_min / area)
        lambda_1 = np.pi * np.sqrt(self.E / self.fy)
        lambda_bar = (L_eff / r) / lambda_1

        # Get reduction factor
        chi = self.reduction_factor(lambda_bar, buckling_curve)

        # Design capacity
        N_cr = chi * area * self.fy / gamma_m

        # Utilization
        util = axial_force / N_cr if N_cr > 0 else float('inf')

        return BucklingResult(
            critical_stress=chi * self.fy / gamma_m,
            applied_stress=axial_force / area,
            utilization=util,
            safety_factor=1 / util if util > 0 else float('inf'),
            mode="column_buckling",
            passes=util <= 1.0
        )

Capacity Verification

@dataclass
class CapacityResult:
    """Results from capacity check."""
    capacity: float
    demand: float
    utilization: float
    governing_mode: str
    passes: bool
    details: Dict


class MemberCapacityChecker:
    """
    Check member capacity for combined loading.
    """

    def __init__(self, material: MaterialProperties):
        self.material = material
        self.stress_calc = StressCalculator(material)
        self.plate_buckling = PlateBucklingAnalyzer(material)
        self.column_buckling = ColumnBucklingAnalyzer(material)

    def check_tension_member(
        self,
        axial_force: float,
        area_gross: float,
        area_net: float,
        gamma_m0: float = 1.0,
        gamma_m2: float = 1.25
    ) -> CapacityResult:
        """
        Check tension capacity per EC3.

        Args:
            axial_force: Applied tension (N)
            area_gross: Gross area (mm²)
            area_net: Net area at connections (mm²)
            gamma_m0: Material factor (yield)
            gamma_m2: Material factor (ultimate)

        Returns:
            CapacityResult
        """
        # Plastic capacity
        N_pl = area_gross * self.material.yield_strength / gamma_m0

        # Ultimate capacity at net section
        N_u = 0.9 * area_net * self.material.ultimate_strength / gamma_m2

        # Governing capacity
        N_Rd = min(N_pl, N_u)
        governing = "plastic" if N_pl <= N_u else "net_section"

        util = axial_force / N_Rd if N_Rd > 0 else float('inf')

        return CapacityResult(
            capacity=N_Rd,
            demand=axial_force,
            utilization=util,
            governing_mode=governing,
            passes=util <= 1.0,
            details={'N_pl': N_pl, 'N_u': N_u}
        )

    def check_combined_loading(
        self,
        N: float,
        M_y: float,
        M_z: float,
        area: float,
        W_pl_y: float,
        W_pl_z: float,
        N_cr_y: float,
        N_cr_z: float,
        gamma_m1: float = 1.0
    ) -> CapacityResult:
        """
        Check member under combined axial and bending.

        Args:
            N: Axial force (N, positive = compression)
            M_y: Moment about y-axis (N·mm)
            M_z: Moment about z-axis (N·mm)
            area: Cross-sectional area (mm²)
            W_pl_y: Plastic section modulus y (mm³)
            W_pl_z: Plastic section modulus z (mm³)
            N_cr_y: Critical buckling load y (N)
            N_cr_z: Critical buckling load z (N)
            gamma_m1: Material factor

        Returns:
            CapacityResult
        """
        fy = self.material.yield_strength

        # Capacities
        N_Rk = area * fy
        M_y_Rk = W_pl_y * fy
        M_z_Rk = W_pl_z * fy

        # Reduction factors
        chi_y = N_cr_y / N_Rk if N_Rk > 0 else 1.0
        chi_z = N_cr_z / N_Rk if N_Rk > 0 else 1.0
        chi = min(chi_y, chi_z, 1.0)

        # Interaction check (simplified)
        util_N = N / (chi * N_Rk / gamma_m1) if N > 0 else 0
        util_My = M_y / (M_y_Rk / gamma_m1)
        util_Mz = M_z / (M_z_Rk / gamma_m1)

        # Combined utilization (simplified linear)
        util_combined = util_N + util_My + util_Mz

        return CapacityResult(
            capacity=chi * N_Rk / gamma_m1,
            demand=N,
            utilization=util_combined,
            governing_mode="combined",
            passes=util_combined <= 1.0,
            details={
                'util_N': util_N,
                'util_My': util_My,
                'util_Mz': util_Mz
            }
        )

YAML Configuration

# config/structural_analysis.yaml

material:
  name: S355
  yield_strength: 355  # MPa
  ultimate_strength: 510
  youngs_modulus: 210000
  poissons_ratio: 0.3

plates:
  - id: bottom_plate
    length: 2000
    width: 1000
    thickness: 20
    loading:
      sigma_x: 150
      sigma_y: 0
      tau: 30

  - id: side_plate
    length: 3000
    width: 1500
    thickness: 16
    loading:
      sigma_x: 200
      sigma_y: 50
      tau: 40

columns:
  - id: leg_1
    area: 15000  # mm²
    I_min: 5.0e7  # mm⁴
    L_eff: 8000  # mm
    buckling_curve: b
    axial_force: 2500000  # N

safety_factors:
  gamma_m0: 1.0
  gamma_m1: 1.0
  gamma_m2: 1.25

output:
  report_path: reports/structural_analysis.html
  include_plots: true

Usage Examples

Stress Analysis

from structural_analysis import StressState, StressCalculator, STEEL_S355

# Create stress state
stress = StressState(
    sigma_x=150.0,
    sigma_y=50.0,
    tau_xy=30.0
)

# Calculate Von Mises
vm = stress.von_mises()
print(f"Von Mises stress: {vm:.1f} MPa")

# Check against yield
sf = STEEL_S355.yield_strength / vm
print(f"Safety factor: {sf:.2f}")

Buckling Check

from structural_analysis import (
    PlateBucklingAnalyzer, PlateGeometry, STEEL_S355
)

analyzer = PlateBucklingAnalyzer(STEEL_S355)

plate = PlateGeometry(
    length=2000,
    width=1000,
    thickness=20
)

result = analyzer.check_plate_buckling(
    plate=plate,
    sigma_x=150,
    tau=30,
    gamma_m=1.15
)

print(f"Utilization: {result.utilization:.2%}")
print(f"Status: {'PASS' if result.passes else 'FAIL'}")

Combined Capacity Check

from structural_analysis import MemberCapacityChecker, STEEL_S355

checker = MemberCapacityChecker(STEEL_S355)

result = checker.check_combined_loading(
    N=2500000,  # N
    M_y=500e6,  # N·mm
    M_z=200e6,  # N·mm
    area=15000,
    W_pl_y=2.5e6,
    W_pl_z=1.5e6,
    N_cr_y=8e6,
    N_cr_z=6e6
)

print(f"Combined utilization: {result.utilization:.2%}")

Best Practices

Analysis Approach

  • Start with simple hand calculations
  • Verify FEA results with analytical methods
  • Check all load combinations
  • Include manufacturing tolerances

Safety Factors

  • Use code-specified factors
  • Document any deviations
  • Consider consequence of failure
  • Account for inspection limitations

Documentation

  • Clearly state assumptions
  • Reference applicable standards
  • Show detailed calculations
  • Include sensitivity checks

Pipeline Wall Thickness & M-T Interaction Assessment

Supported Pipeline/Riser Codes

Standard Application M-T Method
API RP 1111 Offshore hydrocarbon pipelines Von Mises equivalent stress
API STD 2RD Dynamic risers for FPS 4 methods (limit load, angle-based, DNV-equivalent, API-equivalent)
DNV-ST-F101 Submarine pipeline systems Local buckling (load-controlled & displacement-controlled)
DNV-OS-F201 Dynamic risers Combined loading criteria
PD 8010-2 Subsea pipelines Von Mises + buckling

Report Structure for Pipeline Assessment

Engineers expect results first, inputs last:

  1. Executive Summary — PASS/FAIL, governing check, margin, recommendations
  2. Pressure-Only Checks — burst, collapse, propagation (prerequisites)
  3. Combined Loading Contour — Von Mises utilisation heatmap across T-M plane
  4. Allowable Bending Envelope — capacity curves per operating condition
  5. Capacity Limits Table — T+, T-, M_allow per condition
  6. Pressure Sensitivity — parametric study at varying internal pressure
  7. Key Points Summary — utilisation at selected reference points
  8. Input Data — geometry, material, design basis (appendix / collapsible)

What a Structural Engineer Looks For

  1. Clear PASS/FAIL verdict at the top with governing check identified
  2. Pressure-only checks first — if any fail, M-T analysis is moot
  3. Combined loading interaction diagram showing the design space
  4. Allowable envelopes overlaid with design operating points
  5. Margin quantification — not just pass/fail, but how much margin
  6. Code clause references for every check performed
  7. Input data traceability — can the reviewer reproduce the results?
  8. Consistency checks — do T_y and M_p match hand calculations?
  9. Units clearly stated in every table header and axis label

Review Checklist

Input Validation

  • D/t ratio in valid range (15-45 typical for offshore pipe; <15 is thick-walled, >50 very thin)
  • SMYS matches grade (X42:290, X52:359, X56:386, X60:414, X65:448, X70:483, X80:552 MPa)
  • SMTS > SMYS (ratio typically 1.08-1.20 for carbon steel)
  • Corrosion allowance reasonable (0-6 mm for 20+ year design life)
  • Young’s modulus ~207 GPa, Poisson’s ratio ~0.3 for steel

Section Property Sanity

  • A = pi/4 * (OD^2 – ID^2)
  • I = pi/64 * (OD^4 – ID^4)
  • Z = I / (OD/2) for elastic section modulus
  • T_y = A * SMYS
  • M_p per code (API RP 1111 uses SMYS * (D^3 – d_i^3) / 6)

Pressure Check Sanity

  • Hoop utilisation sigma_h / SMYS < 0.50 typical; >0.60 is concerning
  • If hoop utilisation > f_d * SMYS, pipe cannot satisfy combined check at any T,M
  • Burst: P_b = 0.45 * (SMYS + SMTS) * ln(OD/ID)
  • Propagation: P_pr = 24 * SMYS * (t/OD)^2.4

Combined Loading Sanity

  • At origin (T=0, M=0), utilisation = sigma_h / sigma_allow (pressure-only baseline)
  • Envelopes properly nested: Survival > Extreme > Installation > Normal Operating
  • T_pos > |T_neg| when net internal pressure positive (hoop stress asymmetry)
  • M_allow decreases monotonically past peak as |T| increases

Red Flags

  • D/t < 15: Very thick-walled; thin-wall hoop formula may be inaccurate, consider Lame’s equation
  • D/t > 50: Very thin-walled; susceptible to local buckling and ovality
  • Hoop utilisation > 80%: Very little capacity for tension and bending
  • Propagation util > 0.8: May need buckle arrestors
  • Envelope curves crossing: Calculation error (nested property violated)
  • Utilisation at origin > 1.0: Pressure alone exceeds capacity — fundamental design issue

API RP 1111 Design Factors

Condition f_d Application
Normal Operating 0.72 Production, steady-state
Installation 0.80 Lay operations, pre-commissioning
Extreme 0.96 100-year environmental
Survival 1.00 Abnormal / accidental

API STD 2RD vs API RP 1111

Aspect API RP 1111 (Pipelines) API STD 2RD (Risers)
Design philosophy Limit state (WSD) LRFD
Combined loading Von Mises equivalent stress 4 methods available
Stress components Hoop + longitudinal (2D) Full triaxial (3D)
Yield exceedance Not permitted Permitted for displacement-controlled
Additional checks — Fatigue, VIV, dynamic amplification

Typical Follow-Up Analyses After M-T Interaction

  1. On-bottom stability (lateral/vertical under hydrodynamic loading)
  2. Free-span analysis (VIV and static bending at unsupported spans)
  3. Installation analysis (lay tension vs bending during S-lay/J-lay/reel-lay)
  4. Lateral buckling and walking under thermal/pressure cycling
  5. Fatigue at girth welds (especially for dynamic risers)
  6. Fracture mechanics / ECA per BS 7910
  7. Upheaval buckling for buried pipelines
  8. Tie-in spool M-T interaction at connections
  9. Riser touch-down zone combined loading (risers only)

Key Source Files

  • src/digitalmodel/structural/analysis/wall_thickness.py — Core dataclasses and analyzer
  • src/digitalmodel/structural/analysis/wall_thickness_mt_report.py — M-T interaction report
  • src/digitalmodel/structural/analysis/wall_thickness_codes.py — Code strategy registry
  • tests/test_wall_thickness_mt_report.py — 13 tests covering envelope, bisection, asymmetry

Related Skills