news 2026/11/5 23:33:54

python的机器人基础原理工业场景模拟第十一篇:码垛仿真,同一末端目标位姿,统计逆运动学可行解数量,区分肘上,肘下构型。

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python的机器人基础原理工业场景模拟第十一篇:码垛仿真,同一末端目标位姿,统计逆运动学可行解数量,区分肘上,肘下构型。

《Python机器人基础原理》第十一篇:码垛仿真——同一末端目标位姿下的逆解可行解统计与肘上/肘下构型区分

1. 实际工业应用场景

周二上午,某电商物流分拣中心的码垛工位。三条传送带同时送来不同尺寸的纸箱,一台4-DOF关节型码垛机器人需要把纸箱从传送带上抓起,按照"底层交错、逐层递减"的垛型码放到托盘上。

机器人构型:4-DOF关节型(3旋转+1垂直平移),大臂长450 mm,小臂长350 mm,基座安装高度d₁=320 mm,z轴行程±120 mm。关节硬件限位:θ₁ ±155°、θ₂ ±115°、θ₃ ±180°、d₄ ±120 mm。末端真空吸盘质量0.8 kg,额定抓取负载8 kg。

上午十点,工艺工程师小陈站在HMI屏幕前,盯着仿真软件里机器人手臂的两种姿态发呆。

"这两种姿态都能把箱子放到同一个码垛位置,但一种小臂相对大臂逆时针弯折,一种顺时针弯折。现场调试的时候,选哪种?"他对身后的电气组长老刘说。

"顺时针那种吧,手臂收得紧,旁边箱子不容易蹭到。"

"但那个位置旁边已经码了三箱了,顺时针姿态的手臂会蹭到旁边箱子的顶角。我试了逆时针姿态,刚好能绕过去。"

"那你就用逆时针啊。"

"问题是——我怎么知道每个码垛目标点到底有几种可行构型?有些点两种都能到,有些点只有一种能到,还有些点两种都撞限位。我总不能一个一个点手动试吧?"

"这就是你下一篇要写的东西。"

本篇解决的问题:给定同一个末端目标位姿,自动统计逆运动学可行解的数量,并明确区分"肘上"(elbow-up,俯视平面内小臂相对大臂逆时针弯折,θ₂>0)和"肘下"(elbow-down,顺时针弯折,θ₂<0)两种构型。在第九篇逆解求解器(输出2组数学解)和第十篇限位筛选器(剔除超限解)的基础上,本篇增加构型分类逻辑,让工艺工程师一眼看出每个目标点有多少种物理可执行的到达方式,以及每种方式属于哪种弯折方向。

⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于码垛构型分析与解数量统计。不可直接用于真实机器人控制器、PLC或码垛产线。真实工业现场需额外考虑碰撞检测、奇异规避、动力学约束与实时性要求。

2. 痛点分析

课程模块 本篇对应的工程痛点

第3章 连杆参数及运动学求解 → 3.4 逆运动学 同一目标位姿存在多组数学解,需统计可行解数量并区分构型类别

第3章 连杆参数及运动学求解 → 3.2 D-H参数法 肘上/肘下构型对应不同的θ₂符号(正/负),嵌入D-H参数体系

第3章 连杆参数及运动学求解 → 3.3 正运动学 构型分类后二次正运动学校验,确认每类构型仍满足位姿精度

第6章 轨迹规划与控制 码垛路径规划需预知每个目标点的可行构型集合,避免运行时才发现有解不可用

本篇代码解决 L3 离线构型分析层。输入第十篇筛选后的有效解集合,输出每个目标点的可行解数量统计 + 肘上/肘下构型分类标签。代码不是设备实时控制器,不做轨迹插补,不做碰撞检测。它做的事情是:告诉工艺工程师"这个目标点有几种走法、每种走法的肘部姿态是什么",为后续轨迹规划提供决策依据。节拍约束与负载参数属于后续筛选维度,本篇不覆盖。

不覆盖:碰撞检测(需三维几何模型)、动力学约束、轨迹规划、实时控制、5轴以上冗余构型、多目标点连续路径优化。

3. 核心逻辑讲解

物理直觉

SCARA型4-DOF机器人的所有旋转关节轴线竖直,大臂和小臂在水平面内运动。"肘上/肘下"是SCARA文献中的习惯用语,实际指从正上方俯视时小臂相对大臂的弯折方向——逆时针弯折(θ₂>0)称为elbow-up,顺时针弯折(θ₂<0)称为elbow-down。这不是空间意义上的上下,而是俯视平面内的两种构型选择。

给定水平面内一个目标点 (x, y),余弦定理能解出肘关节角θ₂的两个值:一个正、一个负。两种弯折方式对应不同的手臂平面姿态,但末端都能到达同一个 (x, y) 坐标。码垛场景的特殊性在于:箱子堆叠后周围空间被占用,某些姿态会蹭到已码放的箱子。工艺工程师需要知道每个目标点有几种姿态可选,才能根据周围环境选择不干涉的那一种。

业务逻辑 → 代码映射

输入:末端目标位姿 (x, y, z, φ) + 机器人参数 + 关节限位

↓

步骤1:调用第九篇IK求解器,输出2组数学解

↓

步骤2:调用第十篇限位筛选器,剔除超限解,得到有效解集合

↓

步骤3:对有效解逐组分类——

θ₂ > 0 → 肘上构型 (elbow-up, 俯视逆时针)

θ₂ < 0 → 肘下构型 (elbow-down, 俯视顺时针)

|cos(θ₂)| ≈ 1 → 奇异构型(肘部完全伸直,arccos域|cos_t2| > 1-1e-9)

↓

步骤4:统计可行解总数 + 各构型数量

↓

步骤5:二次正运动学校验(精度兜底,tol=1e-9)

↓

步骤6:输出结构化结果:解数量、构型分布、每组的关节角度

关键公式文字翻译

余弦定理求肘角:在由大臂L₁、小臂L₂和目标水平距离r构成的三角形中,cos(θ₂) = (r² - L₁² - L₂²) / (2·L₁·L₂)。分母2·L₁·L₂是三角形两边乘积的两倍。分子r² - L₁² - L₂²是目标距离平方减去两边平方和。当r接近L₁+L₂时cos(θ₂)接近1,θ₂接近0°(手臂几乎伸直);当r接近|L₁-L₂|时cos(θ₂)接近-1,θ₂接近±180°(手臂完全折叠)。θ₂取正号对应肘上(俯视逆时针),取负号对应肘下(俯视顺时针)。

构型分类判据:θ₂的符号直接决定弯折方向。θ₂ = ±arccos(cos_t2),其中arccos返回值∈[0, π],取负后∈[-π, π]。归一化到[-π, π]后符号不变(恒等变换),分类判据在归一化下稳定。分类阈值为0 rad,使用1e-12容差避免浮点精度导致的误分类。

奇异检测(cos域判定):当r = L₁+L₂时cos(θ₂)=1,θ₂=0°,两组解退化为同一组。由于arccos(1-ε) ≈ √(2ε),θ₂域的1e-12容差等价于cos域1e-24精度——浮点上不可达。因此奇异检测改在cos域:|cos_t2| > 1 - 1e-9,对应θ₂ ≈ √(2×1e-9) ≈ 4.5e-5 rad ≈ 0.0026°,物理意义清晰且浮点可达。

4. OOP代码实现

技术栈

严格限定:numpy, scipy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方机器人库。sympy仅用于第九篇符号推导,本篇不涉及。

项目结构树

robot_palletizing_sim/

├── run_palletizing_sim.py

├── requirements.txt

├── palletizing/

│ ├── __init__.py

│ ├── models.py

│ ├── inverse_kinematics.py

│ ├── joint_limits.py

│ ├── config_classifier.py

│ ├── forward_kinematics.py

│ └── visualize.py

├── tests/

│ ├── __init__.py

│ └── test_palletizing_sim.py

└── results/

核心源码

"palletizing/models.py" — dataclass数据模型

"""

4-DOF码垛机器人数据模型

连杆参数、D-H表、关节限位、末端目标位姿、构型分类结果结构体

"""

from dataclasses import dataclass, field

from typing import List, Tuple, Optional

import numpy as np

@dataclass

class JointLimit:

"""

单关节硬件限位

min_val: 最小允许值 (rad 或 m)

max_val: 最大允许值 (rad 或 m)

"""

min_val: float

max_val: float

joint_name: str = ""

def __post_init__(self):

if self.min_val >= self.max_val:

raise ValueError(

f"JointLimit: min_val({self.min_val})必须严格小于"

f"max_val({self.max_val})")

if not np.isfinite(self.min_val) or not np.isfinite(self.max_val):

raise ValueError("JointLimit: 限位值必须为有限数值")

def contains(self, value: float) -> bool:

"""

判断关节值是否在限位区间内

浮点比较使用1e-10容差,避免边界舍入误判

"""

return (self.min_val - 1e-10) <= value <= (self.max_val + 1e-10)

@dataclass

class JointLimitSet:

"""

完整关节限位集合(4-DOF码垛机器人)

"""

limits: List[JointLimit]

def __post_init__(self):

if len(self.limits) != 4:

raise ValueError(f"需要4个关节限位, 实际{len(self.limits)}个")

@classmethod

def default_4dof_palletizing(cls):

"""

默认4-DOF码垛机器人关节限位

θ1: ±155° θ2: ±115° θ3: ±180° d4: ±120 mm

"""

pi = 3.141592653589793

return cls(limits=[

JointLimit(-155 * pi / 180, 155 * pi / 180, 'joint1_base_yaw'),

JointLimit(-115 * pi / 180, 115 * pi / 180, 'joint2_elbow_yaw'),

JointLimit(-180 * pi / 180, 180 * pi / 180, 'joint3_wrist_roll'),

JointLimit(-0.120, 0.120, 'joint4_z_lift'),

])

def check_all_with_indices(self, joint_values: List[float]) -> Tuple[bool, List[int]]:

"""

检查全部关节是否在限位内

返回:(全部通过?, 超限关节索引列表)

使用索引而非字符串匹配,避免子串匹配脆弱性

"""

if len(joint_values) != 4:

raise ValueError(f"需要4个关节值, 实际{len(joint_values)}个")

all_pass = True

violation_indices = []

for i, (val, limit) in enumerate(zip(joint_values, self.limits)):

if not limit.contains(val):

all_pass = False

violation_indices.append(i)

return all_pass, violation_indices

@dataclass

class DHParam:

"""标准D-H参数"""

theta: float

d: float

a: float

alpha: float

def __post_init__(self):

for name, val in [('theta', self.theta), ('d', self.d),

('a', self.a), ('alpha', self.alpha)]:

if not np.isfinite(val):

raise ValueError(f"DHParam.{name}必须为有限数值")

@dataclass

class SCARAParams:

"""

4-DOF码垛机器人完整参数

大臂450mm, 小臂350mm, d1=320mm

"""

links: List[DHParam]

joint_names: List[str] = field(default_factory=lambda: [

'joint1_base_yaw', 'joint2_elbow_yaw',

'joint3_wrist_roll', 'joint4_z_lift'

])

@classmethod

def default_palletizing(cls):

pi = 3.141592653589793

links = [

DHParam(0.0, 0.320, 0.450, 0.0),

DHParam(0.0, 0.0, 0.350, 0.0),

DHParam(0.0, 0.0, 0.0, 0.0),

DHParam(0.0, 0.0, 0.0, -pi/2),

]

return cls(links=links)

@dataclass

class TargetPose:

"""

末端目标位姿

x, y, z: 基座标系下位置 (m)

phi: 绕基座z轴目标姿态角 (rad)

label: 可选标签(如"垛位A-第3层")

"""

x: float

y: float

z: float

phi: float

label: str = ""

def __post_init__(self):

for name, val in [('x', self.x), ('y', self.y), ('z', self.z)]:

if not np.isfinite(val):

raise ValueError(f"TargetPose.{name}必须为有限数值")

if not np.isfinite(self.phi):

raise ValueError("TargetPose.phi必须为有限数值")

@dataclass

class ConfigClassification:

"""

单组解的构型分类结果

"""

joint_values: List[float]

config_type: str # 'elbow_up', 'elbow_down', 'singular', 'rejected', 'verify_failed'

is_valid: bool

position_error_m: float = 0.0

yaw_error_rad: float = 0.0

violation_indices: List[int] = field(default_factory=list)

"palletizing/inverse_kinematics.py" — 逆运动学求解器

"""

逆运动学求解器:给定末端目标位姿,求解全部数学解(2组)

肘下对应θ₂<0(取-arccos),肘上对应θ₂>0(取+arccos)

"""

import numpy as np

from palletizing.models import SCARAParams, TargetPose

class InverseKinematicsSCARA:

"""

4-DOF SCARA逆运动学解析求解器

输出2组数学解:索引0=肘下(θ₂<0),索引1=肘上(θ₂>0)

"""

def __init__(self, params: SCARAParams):

self.params = params

self.L1 = params.links[0].a

self.L2 = params.links[1].a

self.d1 = params.links[0].d

def solve_all(self, target: TargetPose) -> list:

"""

求解全部数学解(2组)

返回:[[θ1_elbow_down, θ2_elbow_down, θ3_down, d4],

[θ1_elbow_up, θ2_elbow_up, θ3_up, d4]]

"""

x, y, z = target.x, target.y, target.z

phi = target.phi

r = np.sqrt(x**2 + y**2)

r_max = self.L1 + self.L2

r_min = abs(self.L1 - self.L2)

if r > r_max + 1e-6:

raise ValueError(

f"目标点水平距离{r*1000:.1f}mm超出最大可达半径"

f"{(r_max)*1000:.1f}mm")

if r < r_min - 1e-6:

raise ValueError(

f"目标点水平距离{r*1000:.1f}mm小于最小可达半径"

f"{(r_min)*1000:.1f}mm")

cos_t2 = (r**2 - self.L1**2 - self.L2**2) / (2 * self.L1 * self.L2)

cos_t2 = np.clip(cos_t2, -1.0, 1.0)

# 肘下分支:θ₂取负(俯视顺时针弯折)

theta2_down = -np.arccos(cos_t2)

phi_angle = np.arctan2(y, x)

psi_down = np.arctan2(self.L2 * np.sin(theta2_down),

self.L1 + self.L2 * np.cos(theta2_down))

theta1_down = phi_angle - psi_down

theta3_down = phi - theta1_down - theta2_down

# 肘上分支:θ₂取正(俯视逆时针弯折)

theta2_up = np.arccos(cos_t2)

psi_up = np.arctan2(self.L2 * np.sin(theta2_up),

self.L1 + self.L2 * np.cos(theta2_up))

theta1_up = phi_angle - psi_up

theta3_up = phi - theta1_up - theta2_up

d4 = z - self.d1

solutions = []

solutions.append([theta1_down, theta2_down, theta3_down, d4])

solutions.append([theta1_up, theta2_up, theta3_up, d4])

return solutions, cos_t2 # 返回cos_t2供奇异检测使用

"palletizing/joint_limits.py" — 关节限位管理器

"""

关节限位管理器:封装限位检查、角度归一化

超限统计使用关节索引计数,避免字符串子串匹配脆弱性

"""

import numpy as np

from palletizing.models import JointLimitSet

class JointLimitChecker:

"""

关节限位检查器

归一化隐含前提:关节是±摆动关节,不是多圈连续转轴

对SCARA/码垛构型成立;连续转轴机器人不适用此约定

"""

def __init__(self, limit_set: JointLimitSet):

self.limit_set = limit_set

@staticmethod

def normalize_angle(angle_rad: float) -> float:

"""

将角度归一化到 [-π, π] 区间

使用arctan2(sin, cos)方法

θ₂ = ±arccos(·) ∈ [-π, π],归一化在此区间内为恒等变换

分类判据θ₂>0/θ₂<0的符号在归一化后不变

"""

return np.arctan2(np.sin(angle_rad), np.cos(angle_rad))

def normalize_joint_values(self, joint_values: list) -> list:

"""对旋转关节(前3个)做角度归一化,平移关节不归一化"""

normalized = list(joint_values)

for i in range(min(3, len(normalized))):

normalized[i] = self.normalize_angle(normalized[i])

return normalized

def check_single(self, joint_values: list) -> tuple:

"""

检查单组解的限位合规性

返回:(是否通过, 超限关节索引列表, 归一化后的关节值)

"""

norm_values = self.normalize_joint_values(joint_values)

passed, violation_indices = \

self.limit_set.check_all_with_indices(norm_values)

return passed, violation_indices, norm_values

"palletizing/config_classifier.py" — 构型分类器(核心业务类)

"""

构型分类器:区分肘上/肘下构型,统计可行解数量

奇异检测在cos域判定(|cos_t2| > 1-1e-9),避免arccos√放大效应

"""

import numpy as np

from palletizing.models import SCARAParams, TargetPose, JointLimitSet

from palletizing.models import ConfigClassification

from palletizing.inverse_kinematics import InverseKinematicsSCARA

from palletizing.joint_limits import JointLimitChecker

from palletizing.forward_kinematics import ForwardKinematicsSCARA

class ConfigClassifier:

"""

构型分类器:IK求解 → 限位筛选 → 构型分类 → 二次校验

设计原则:IK与FK共享同一个params对象,确保状态一致性

"""

def __init__(self, params: SCARAParams,

limit_set: JointLimitSet,

fk: ForwardKinematicsSCARA):

self.params = params

self.ik = InverseKinematicsSCARA(params)

self.limit_checker = JointLimitChecker(limit_set)

self.fk = fk

self.tolerance = 1e-9

self.singular_cos_tol = 1e-9 # cos域奇异检测阈值

def classify_single(self, target: TargetPose) -> dict:

"""

对单个目标点执行完整分析流程

返回结构化结果

"""

result = {

'target_label': target.label,

'target_pose': [target.x, target.y, target.z, target.phi],

'total_solutions': 0,

'valid_solutions': 0,

'elbow_up_count': 0,

'elbow_down_count': 0,

'singular_count': 0,

'rejected_count': 0,

'classifications': [],

'singular_detected': False,

'cos_t2_value': 0.0

}

# 步骤1:IK求解(同时获取cos_t2用于奇异检测)

try:

solutions, cos_t2 = self.ik.solve_all(target)

result['cos_t2_value'] = float(cos_t2)

except ValueError as e:

result['error'] = str(e)

return result

result['total_solutions'] = len(solutions)

# cos域奇异检测:|cos_t2| > 1 - 1e-9 对应 θ₂ < √(2e-9) ≈ 4.5e-5 rad

is_singular_by_cos = abs(cos_t2) > (1.0 - self.singular_cos_tol)

# 步骤2+3:逐解筛选 + 分类

for idx, sol in enumerate(solutions):

passed, violation_indices, norm_sol = \

self.limit_checker.check_single(sol)

classification = ConfigClassification(

joint_values=norm_sol,

config_type='unknown',

is_valid=passed,

violation_indices=violation_indices

)

if not passed:

result['rejected_count'] += 1

classification.config_type = 'rejected'

else:

# 二次正运动学校验

x_calc, y_calc, z_calc, phi_calc = \

self.fk.get_position_and_yaw(norm_sol)

pos_error = np.sqrt((x_calc - target.x)**2 +

(y_calc - target.y)**2 +

(z_calc - target.z)**2)

yaw_error = abs(phi_calc - target.phi)

yaw_error = min(yaw_error, 2*np.pi - yaw_error)

if pos_error >= self.tolerance or yaw_error >= self.tolerance:

result['rejected_count'] += 1

classification.is_valid = False

classification.config_type = 'verify_failed'

classification.position_error_m = float(pos_error)

classification.yaw_error_rad = float(yaw_error)

else:

classification.position_error_m = float(pos_error)

classification.yaw_error_rad = float(yaw_error)

# 构型分类:基于θ₂符号 + cos域奇异检测

theta2 = norm_sol[1]

if is_singular_by_cos:

# cos域判定为奇异,两组解退化为同一组

classification.config_type = 'singular'

result['singular_count'] += 1

result['singular_detected'] = True

elif theta2 > 0:

classification.config_type = 'elbow_up'

result['elbow_up_count'] += 1

else:

classification.config_type = 'elbow_down'

result['elbow_down_count'] += 1

result['valid_solutions'] += 1

result['classifications'].append(classification)

return result

def batch_classify(self, targets: list) -> list:

"""

批量分析多个目标点

"""

results = []

for target in targets:

results.append(self.classify_single(target))

return results

"palletizing/forward_kinematics.py" — 正运动学(二次校验用)

"""

正运动学求解器:用于筛选后有效解的二次精度校验

IK与FK共享同一个params对象,确保状态一致性

"""

import numpy as np

from palletizing.models import SCARAParams

class ForwardKinematicsSCARA:

"""4-DOF SCARA正运动学求解器"""

def __init__(self, params: SCARAParams):

self.params = params

def compute_pose(self, joint_values):

if len(joint_values) != 4:

raise ValueError(f"需要4个关节值, 实际{len(joint_values)}个")

T = np.eye(4)

for i in range(3):

self.params.links[i].theta = joint_values[i]

T = T @ self._dh_matrix(self.params.links[i])

self.params.links[3].d = joint_values[3]

T = T @ self._dh_matrix(self.params.links[3])

return T

@staticmethod

def _dh_matrix(param):

ct, st = np.cos(param.theta), np.sin(param.theta)

ca, sa = np.cos(param.alpha), np.sin(param.alpha)

return np.array([

[ct, -st*ca, st*sa, param.a*ct],

[st, ct*ca, -ct*sa, param.a*st],

[0, sa, ca, param.d],

[0, 0, 0, 1]

], dtype=float)

def get_position_and_yaw(self, joint_values):

T = self.compute_pose(joint_values)

x, y, z = T[0, 3], T[1, 3], T[2, 3]

phi = joint_values[0] + joint_values[1] + joint_values[2]

phi = np.arctan2(np.sin(phi), np.cos(phi))

return x, y, z, phi

"palletizing/visualize.py" — 可视化模块

"""

码垛仿真结果可视化:4张核心图表

"""

import numpy as np

import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']

plt.rcParams['axes.unicode_minus'] = False

class Visualizer:

def __init__(self, results_dir="results"):

self.results_dir = results_dir

import os

os.makedirs(results_dir, exist_ok=True)

def plot_config_distribution_pie(self, results_list):

"""

① 有效解构型分布饼图(仅统计valid solutions的构型比例)

工程结论:展示所有目标点中肘上/肘下/奇异的比例

"""

total_up = sum(r['elbow_up_count'] for r in results_list)

total_down = sum(r['elbow_down_count'] for r in results_list)

total_singular = sum(r['singular_count'] for r in results_list)

labels = []

sizes = []

colors = []

if total_up > 0:

labels.append(f'Elbow Up ({total_up})')

sizes.append(total_up)

colors.append('steelblue')

if total_down > 0:

labels.append(f'Elbow Down ({total_down})')

sizes.append(total_down)

colors.append('orange')

if total_singular > 0:

labels.append(f'Singular ({total_singular})')

sizes.append(total_singular)

colors.append('purple')

fig, ax = plt.subplots(figsize=(8, 8))

if sizes:

wedges, texts, autotexts = ax.pie(

sizes, labels=labels, colors=colors,

autopct='%1.1f%%', startangle=90,

wedgeprops=dict(edgecolor='black', linewidth=1)

)

for autotext in autotexts:

autotext.set_fontsize(9)

ax.set_title('Valid Configuration Distribution (Excluding Rejected)')

plt.tight_layout()

path = f"{self.results_dir}/01_config_distribution_pie.png"

plt.savefig(path, dpi=150)

plt.close()

return path

def plot_solution_count_bar(self, results_list):

"""

② 各目标点可行解数量柱状图

工程结论:一眼看出哪些目标点只有1种构型(限位淘汰了另一种)

"""

fig, ax = plt.subplots(figsize=(12, 5))

n = len(results_list)

indices = range(n)

valid_counts = [r['valid_solutions'] for r in results_list]

labels = [r.get('target_label', f'Pt{i}') for i, r in enumerate(results_list)]

bars = ax.bar(indices, valid_counts, color='steelblue',

alpha=0.8, edgecolor='black')

for bar, count in zip(bars, valid_counts):

if count > 0:

ax.text(bar.get_x() + bar.get_width()/2,

bar.get_height() + 0.05,

str(count), ha='center', va='bottom',

fontweight='bold')

# 奇异检测标注(仅在柱高>0时标注)

for i, r in enumerate(results_list):

if r.get('singular_detected', False) and valid_counts[i] > 0:

ax.text(i, valid_counts[i] + 0.15, 'SINGULAR',

ha='center', va='bottom',

fontsize=7, color='purple', rotation=0)

ax.set_xticks(indices)

ax.set_xticklabels(labels, rotation=45, ha='right', fontsize=8)

ax.set_ylabel('Valid Solution Count')

ax.set_title('Feasible Solution Count per Target Point')

ax.set_ylim(0, max(valid_counts + [1]) + 0.5)

ax.grid(True, alpha=0.3, axis='y')

plt.tight_layout()

path = f"{self.results_dir}/02_solution_count_bar

利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 14:08:29

HSV颜色识别原理与工业级调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 17:24:17

高效错题整理方法论与十年实践心得

1. 错题整理的价值与方法论错题整理这件事&#xff0c;我从2013年开始坚持到现在已经整整十年。最初只是为了应付考试&#xff0c;后来发现这套方法对知识体系的构建有着惊人的效果。特别是2013年到2016年这段时间积累的错题本&#xff0c;至今仍是我最常翻阅的学习资料之一。错…

作者头像 李华
网站建设 2026/11/5 23:33:40

Spring源码面试:Bean生命周期、三级缓存与@Autowired解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 23:02:29

Python+IMAP远程关机:用邮件指令轻松控制电脑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 16:07:24

2025年韩系车强力反击:本土化定义与产品策略如何重塑合资格局

1. 先别急着下结论&#xff1a;2025年韩系车翻身的真实信号这几年只要聊到汽车市场&#xff0c;大家都习惯把韩系车当成“被遗忘的边缘角色”。尤其是2022年到2024年那阵子&#xff0c;韩系车在国内的存在感肉眼可见地下降&#xff0c;经销商退网、车型更新慢、终端只能靠降价续…

作者头像 李华
网站建设 2026/10/23 5:37:20

服务器磁盘满了?从告警到清理的完整排查

背景 凌晨收到微信告警&#xff1a;磁盘使用率超过 90%。 SSH 上去一看&#xff1a; $ df -h Filesystem Size Used Avail Use% Mounted on /dev/vda1 40G 36G 2.1G 95% /40G 的盘&#xff0c;只剩 2G。再涨一点&#xff0c;MariaDB 写不进去&#xff0c;Ngin…

作者头像 李华