refactor: 重组项目目录结构
以讲义内容为骨架迁移到标准目录格式: - officefile/ 主内容(12章 + 附录 + CC4SI补充) - dofile/ 代码示例(11个Python脚本) - data/ 图片资源 - output/ 生成输出(忽略) - Archive/ 归档旧目录(忽略) - .claude/skills/ 保留markdown-to-docx工具链 - .pandoc/ 保留CSL和本地化配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,961 @@
|
||||
"""
|
||||
空间优化示例 (Spatial Optimization Example)
|
||||
==========================================
|
||||
|
||||
本示例展示空间智能系统中的空间优化方法。
|
||||
空间优化是在空间约束下寻找最优解的过程。
|
||||
|
||||
核心概念:
|
||||
1. 目标函数 - 需要最大化或最小化的目标
|
||||
2. 约束条件 - 空间和非空间限制
|
||||
3. 决策变量 - 可控制的空间变量
|
||||
4. 优化算法 - 求解最优解的方法
|
||||
5. 帕累托前沿 - 多目标优化中的解集
|
||||
|
||||
应用场景:
|
||||
- 选址优化
|
||||
- 路径优化
|
||||
- 空间配置优化
|
||||
- 资源分配优化
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
from typing import List, Dict, Tuple, Optional, Any, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
import heapq
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 基础数据结构
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""二维点"""
|
||||
x: float
|
||||
y: float
|
||||
|
||||
def distance_to(self, other: 'Point') -> float:
|
||||
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self.x:.2f}, {self.y:.2f})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DemandPoint:
|
||||
"""需求点"""
|
||||
id: str
|
||||
location: Point
|
||||
demand: float # 需求量
|
||||
population: int = 0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Demand({self.id}, demand={self.demand})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Facility:
|
||||
"""设施"""
|
||||
id: str
|
||||
location: Point
|
||||
capacity: float # 服务能力
|
||||
fixed_cost: float = 0 # 固定成本
|
||||
variable_cost: float = 0 # 单位可变成本
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Facility({self.id}, loc={self.location})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OptimizationResult:
|
||||
"""优化结果"""
|
||||
success: bool
|
||||
objective_value: float
|
||||
solution: Any
|
||||
iterations: int = 0
|
||||
convergence_history: List[float] = field(default_factory=list)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 约束条件
|
||||
# ============================================================================
|
||||
|
||||
class ConstraintType(Enum):
|
||||
"""约束类型"""
|
||||
EQUALITY = "equality" # 等式约束
|
||||
INEQUALITY = "inequality" # 不等式约束
|
||||
BOUNDS = "bounds" # 边界约束
|
||||
|
||||
|
||||
@dataclass
|
||||
class Constraint:
|
||||
"""约束条件"""
|
||||
name: str
|
||||
constraint_type: ConstraintType
|
||||
rhs: float # 右端值
|
||||
lhs_function: Optional[Callable[[Any], float]] = None # 左端函数
|
||||
|
||||
def is_satisfied(self, variables: Any, tolerance: float = 1e-6) -> bool:
|
||||
"""检查约束是否满足"""
|
||||
if self.lhs_function is None:
|
||||
return True
|
||||
|
||||
lhs = self.lhs_function(variables)
|
||||
|
||||
if self.constraint_type == ConstraintType.EQUALITY:
|
||||
return abs(lhs - self.rhs) < tolerance
|
||||
elif self.constraint_type == ConstraintType.INEQUALITY:
|
||||
return lhs <= self.rhs + tolerance
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 优化问题定义
|
||||
# ============================================================================
|
||||
|
||||
class OptimizationProblem(ABC):
|
||||
"""优化问题抽象基类"""
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self.name = name
|
||||
self.constraints: List[Constraint] = []
|
||||
self.objective_calls = 0
|
||||
|
||||
@abstractmethod
|
||||
def objective(self, variables: Any) -> float:
|
||||
"""目标函数"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_initial_solution(self) -> Any:
|
||||
"""获取初始解"""
|
||||
pass
|
||||
|
||||
def add_constraint(self, constraint: Constraint) -> None:
|
||||
"""添加约束"""
|
||||
self.constraints.append(constraint)
|
||||
|
||||
def is_feasible(self, variables: Any) -> bool:
|
||||
"""检查解是否可行"""
|
||||
return all(c.is_satisfied(variables) for c in self.constraints)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 贪心算法
|
||||
# ============================================================================
|
||||
|
||||
class GreedyOptimizer:
|
||||
"""
|
||||
贪心优化器
|
||||
|
||||
每一步选择当前最优的选项。
|
||||
"""
|
||||
|
||||
def __init__(self, problem: OptimizationProblem):
|
||||
self.problem = problem
|
||||
|
||||
def optimize(self, max_iterations: int = 1000) -> OptimizationResult:
|
||||
"""
|
||||
执行贪心优化
|
||||
|
||||
Args:
|
||||
max_iterations: 最大迭代次数
|
||||
|
||||
Returns:
|
||||
优化结果
|
||||
"""
|
||||
current_solution = self.problem.get_initial_solution()
|
||||
current_value = self.problem.objective(current_solution)
|
||||
|
||||
history = [current_value]
|
||||
|
||||
for iteration in range(max_iterations):
|
||||
# 生成邻居解
|
||||
neighbors = self._generate_neighbors(current_solution)
|
||||
|
||||
# 找最优邻居
|
||||
best_neighbor = None
|
||||
best_neighbor_value = float('inf')
|
||||
|
||||
for neighbor in neighbors:
|
||||
if self.problem.is_feasible(neighbor):
|
||||
value = self.problem.objective(neighbor)
|
||||
if value < best_neighbor_value:
|
||||
best_neighbor = neighbor
|
||||
best_neighbor_value = value
|
||||
|
||||
# 如果没有改进,停止
|
||||
if best_neighbor is None or best_neighbor_value >= current_value:
|
||||
break
|
||||
|
||||
current_solution = best_neighbor
|
||||
current_value = best_neighbor_value
|
||||
history.append(current_value)
|
||||
|
||||
return OptimizationResult(
|
||||
success=True,
|
||||
objective_value=current_value,
|
||||
solution=current_solution,
|
||||
iterations=len(history),
|
||||
convergence_history=history
|
||||
)
|
||||
|
||||
def _generate_neighbors(self, solution: Any) -> List[Any]:
|
||||
"""生成邻居解 (需要根据具体问题实现)"""
|
||||
return []
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 模拟退火算法
|
||||
# ============================================================================
|
||||
|
||||
class SimulatedAnnealing:
|
||||
"""
|
||||
模拟退火算法
|
||||
|
||||
一种概率性全局优化算法,能够跳出局部最优。
|
||||
"""
|
||||
|
||||
def __init__(self, problem: OptimizationProblem):
|
||||
self.problem = problem
|
||||
|
||||
def optimize(self,
|
||||
initial_temp: float = 1000.0,
|
||||
cooling_rate: float = 0.95,
|
||||
min_temp: float = 0.01,
|
||||
max_iterations: int = 10000) -> OptimizationResult:
|
||||
"""
|
||||
执行模拟退火优化
|
||||
|
||||
Args:
|
||||
initial_temp: 初始温度
|
||||
cooling_rate: 冷却速率
|
||||
min_temp: 最小温度
|
||||
max_iterations: 最大迭代次数
|
||||
|
||||
Returns:
|
||||
优化结果
|
||||
"""
|
||||
current_solution = self.problem.get_initial_solution()
|
||||
current_value = self.problem.objective(current_solution)
|
||||
|
||||
best_solution = current_solution
|
||||
best_value = current_value
|
||||
|
||||
temperature = initial_temp
|
||||
history = [current_value]
|
||||
|
||||
iteration = 0
|
||||
while temperature > min_temp and iteration < max_iterations:
|
||||
# 生成邻居解
|
||||
neighbor = self._generate_neighbor(current_solution)
|
||||
|
||||
if self.problem.is_feasible(neighbor):
|
||||
neighbor_value = self.problem.objective(neighbor)
|
||||
|
||||
# 决定是否接受新解
|
||||
delta = neighbor_value - current_value
|
||||
|
||||
if delta < 0 or random.random() < math.exp(-delta / temperature):
|
||||
current_solution = neighbor
|
||||
current_value = neighbor_value
|
||||
|
||||
# 更新最优解
|
||||
if current_value < best_value:
|
||||
best_solution = current_solution
|
||||
best_value = current_value
|
||||
|
||||
history.append(best_value)
|
||||
temperature *= cooling_rate
|
||||
iteration += 1
|
||||
|
||||
return OptimizationResult(
|
||||
success=True,
|
||||
objective_value=best_value,
|
||||
solution=best_solution,
|
||||
iterations=iteration,
|
||||
convergence_history=history
|
||||
)
|
||||
|
||||
def _generate_neighbor(self, solution: Any) -> Any:
|
||||
"""生成邻居解 (需要根据具体问题实现)"""
|
||||
return solution
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 遗传算法
|
||||
# =============================================================================
|
||||
|
||||
class GeneticAlgorithm:
|
||||
"""
|
||||
遗传算法
|
||||
|
||||
模拟自然进化的全局优化算法。
|
||||
"""
|
||||
|
||||
def __init__(self, problem: OptimizationProblem):
|
||||
self.problem = problem
|
||||
|
||||
def optimize(self,
|
||||
population_size: int = 50,
|
||||
generations: int = 100,
|
||||
mutation_rate: float = 0.1,
|
||||
crossover_rate: float = 0.8,
|
||||
elitism_count: int = 2) -> OptimizationResult:
|
||||
"""
|
||||
执行遗传算法优化
|
||||
|
||||
Args:
|
||||
population_size: 种群大小
|
||||
generations: 迭代代数
|
||||
mutation_rate: 变异率
|
||||
crossover_rate: 交叉率
|
||||
elitism_count: 精英保留数量
|
||||
|
||||
Returns:
|
||||
优化结果
|
||||
"""
|
||||
# 初始化种群
|
||||
population = self._initialize_population(population_size)
|
||||
history = []
|
||||
|
||||
for generation in range(generations):
|
||||
# 评估适应度
|
||||
fitness = []
|
||||
for individual in population:
|
||||
value = self.problem.objective(individual)
|
||||
fitness.append(1.0 / (1.0 + value)) # 转换为适应度 (越小越好 -> 越大越好)
|
||||
|
||||
# 记录最优值
|
||||
best_idx = max(range(len(fitness)), key=lambda i: fitness[i])
|
||||
best_value = self.problem.objective(population[best_idx])
|
||||
history.append(best_value)
|
||||
|
||||
# 选择
|
||||
selected = self._selection(population, fitness)
|
||||
|
||||
# 交叉
|
||||
offspring = self._crossover(selected, crossover_rate)
|
||||
|
||||
# 变异
|
||||
offspring = self._mutation(offspring, mutation_rate)
|
||||
|
||||
# 精英保留
|
||||
if elitism_count > 0:
|
||||
elite_indices = sorted(range(len(fitness)),
|
||||
key=lambda i: fitness[i], reverse=True)[:elitism_count]
|
||||
for i, idx in enumerate(elite_indices):
|
||||
offspring[i] = population[idx]
|
||||
|
||||
population = offspring
|
||||
|
||||
# 返回最优解
|
||||
final_fitness = [self.problem.objective(ind) for ind in population]
|
||||
best_idx = min(range(len(final_fitness)), key=lambda i: final_fitness[i])
|
||||
|
||||
return OptimizationResult(
|
||||
success=True,
|
||||
objective_value=final_fitness[best_idx],
|
||||
solution=population[best_idx],
|
||||
iterations=generations,
|
||||
convergence_history=history
|
||||
)
|
||||
|
||||
def _initialize_population(self, size: int) -> List[Any]:
|
||||
"""初始化种群"""
|
||||
return [self.problem.get_initial_solution() for _ in range(size)]
|
||||
|
||||
def _selection(self, population: List[Any], fitness: List[float]) -> List[Any]:
|
||||
"""锦标赛选择"""
|
||||
selected = []
|
||||
tournament_size = max(3, len(population) // 10)
|
||||
|
||||
for _ in range(len(population)):
|
||||
# 随机选择tournament_size个个体
|
||||
contestants = random.sample(list(zip(population, fitness)), tournament_size)
|
||||
# 选择适应度最高的
|
||||
winner = max(contestants, key=lambda x: x[1])[0]
|
||||
selected.append(winner)
|
||||
|
||||
return selected
|
||||
|
||||
def _crossover(self, population: List[Any], rate: float) -> List[Any]:
|
||||
"""交叉操作"""
|
||||
offspring = []
|
||||
|
||||
for i in range(0, len(population), 2):
|
||||
parent1 = population[i]
|
||||
parent2 = population[i + 1] if i + 1 < len(population) else population[0]
|
||||
|
||||
if random.random() < rate:
|
||||
child1, child2 = self._crossover_operators(parent1, parent2)
|
||||
else:
|
||||
child1, child2 = parent1, parent2
|
||||
|
||||
offspring.extend([child1, child2])
|
||||
|
||||
return offspring[:len(population)]
|
||||
|
||||
def _crossover_operators(self, parent1: Any, parent2: Any) -> Tuple[Any, Any]:
|
||||
"""交叉算子 (需要根据具体问题实现)"""
|
||||
return parent1, parent2
|
||||
|
||||
def _mutation(self, population: List[Any], rate: float) -> List[Any]:
|
||||
"""变异操作"""
|
||||
mutated = []
|
||||
|
||||
for individual in population:
|
||||
if random.random() < rate:
|
||||
mutated.append(self._mutate(individual))
|
||||
else:
|
||||
mutated.append(individual)
|
||||
|
||||
return mutated
|
||||
|
||||
def _mutate(self, individual: Any) -> Any:
|
||||
"""变异算子 (需要根据具体问题实现)"""
|
||||
return individual
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 选址优化问题
|
||||
# ============================================================================
|
||||
|
||||
class LocationProblem(OptimizationProblem):
|
||||
"""
|
||||
设施选址问题
|
||||
|
||||
在给定候选位置中选择最优的设施位置组合。
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
demand_points: List[DemandPoint],
|
||||
candidate_locations: List[Point],
|
||||
num_facilities: int,
|
||||
fixed_costs: List[float] = None,
|
||||
transportation_cost: float = 1.0):
|
||||
"""
|
||||
初始化选址问题
|
||||
|
||||
Args:
|
||||
demand_points: 需求点列表
|
||||
candidate_locations: 候选位置列表
|
||||
num_facilities: 设施数量
|
||||
fixed_costs: 各候选位置的固定成本
|
||||
transportation_cost: 单位运输成本
|
||||
"""
|
||||
super().__init__("设施选址问题")
|
||||
self.demand_points = demand_points
|
||||
self.candidate_locations = candidate_locations
|
||||
self.num_facilities = num_facilities
|
||||
self.fixed_costs = fixed_costs or [0] * len(candidate_locations)
|
||||
self.transportation_cost = transportation_cost
|
||||
|
||||
def objective(self, solution: List[int]) -> float:
|
||||
"""
|
||||
计算目标函数值
|
||||
|
||||
Args:
|
||||
solution: 选中的候选位置索引列表
|
||||
|
||||
Returns:
|
||||
总成本
|
||||
"""
|
||||
self.objective_calls += 1
|
||||
|
||||
if not solution or len(solution) != self.num_facilities:
|
||||
return float('inf')
|
||||
|
||||
total_cost = 0.0
|
||||
|
||||
# 固定成本
|
||||
for idx in solution:
|
||||
if 0 <= idx < len(self.fixed_costs):
|
||||
total_cost += self.fixed_costs[idx]
|
||||
|
||||
# 运输成本 (每个需求点分配到最近的设施)
|
||||
for demand in self.demand_points:
|
||||
min_dist = float('inf')
|
||||
for facility_idx in solution:
|
||||
if 0 <= facility_idx < len(self.candidate_locations):
|
||||
facility_loc = self.candidate_locations[facility_idx]
|
||||
dist = demand.location.distance_to(facility_loc)
|
||||
min_dist = min(min_dist, dist)
|
||||
|
||||
total_cost += min_dist * demand.demand * self.transportation_cost
|
||||
|
||||
return total_cost
|
||||
|
||||
def get_initial_solution(self) -> List[int]:
|
||||
"""获取初始解 (随机选择)"""
|
||||
n = len(self.candidate_locations)
|
||||
if n <= self.num_facilities:
|
||||
return list(range(n))
|
||||
return random.sample(range(n), self.num_facilities)
|
||||
|
||||
|
||||
class LocationSAOptimizer(SimulatedAnnealing):
|
||||
"""针对选址问题的模拟退火优化器"""
|
||||
|
||||
def _generate_neighbor(self, solution: List[int]) -> List[int]:
|
||||
"""生成邻居解"""
|
||||
if not solution:
|
||||
return solution
|
||||
|
||||
neighbor = solution.copy()
|
||||
n = len(self.problem.candidate_locations)
|
||||
|
||||
# 随机选择一个操作
|
||||
operation = random.choice(['replace', 'swap'])
|
||||
|
||||
if operation == 'replace':
|
||||
# 替换一个设施
|
||||
idx = random.randint(0, len(neighbor) - 1)
|
||||
available = [i for i in range(n) if i not in neighbor]
|
||||
if available:
|
||||
neighbor[idx] = random.choice(available)
|
||||
|
||||
elif operation == 'swap' and len(neighbor) >= 1:
|
||||
# 交换一个设施
|
||||
idx = random.randint(0, len(neighbor) - 1)
|
||||
available = [i for i in range(n) if i not in neighbor]
|
||||
if available:
|
||||
neighbor[idx] = random.choice(available)
|
||||
|
||||
return neighbor
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# P-中值问题
|
||||
# ============================================================================
|
||||
|
||||
class PMedianProblem(LocationProblem):
|
||||
"""
|
||||
P-中值问题
|
||||
|
||||
选择P个设施位置,使需求点到最近设施的总加权距离最小。
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
demand_points: List[DemandPoint],
|
||||
candidate_locations: List[Point],
|
||||
p: int):
|
||||
super().__init__(demand_points, candidate_locations, p, [0] * len(candidate_locations))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 覆盖问题
|
||||
# ============================================================================
|
||||
|
||||
class MaxCoverageProblem(OptimizationProblem):
|
||||
"""
|
||||
最大覆盖问题
|
||||
|
||||
在给定设施数量限制下,最大化覆盖的需求量。
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
demand_points: List[DemandPoint],
|
||||
candidate_locations: List[Point],
|
||||
num_facilities: int,
|
||||
coverage_radius: float):
|
||||
"""
|
||||
初始化最大覆盖问题
|
||||
|
||||
Args:
|
||||
demand_points: 需求点列表
|
||||
candidate_locations: 候选位置列表
|
||||
num_facilities: 设施数量
|
||||
coverage_radius: 覆盖半径
|
||||
"""
|
||||
super().__init__("最大覆盖问题")
|
||||
self.demand_points = demand_points
|
||||
self.candidate_locations = candidate_locations
|
||||
self.num_facilities = num_facilities
|
||||
self.coverage_radius = coverage_radius
|
||||
|
||||
# 预计算覆盖关系
|
||||
self.coverage_matrix = self._compute_coverage()
|
||||
|
||||
def _compute_coverage(self) -> List[List[bool]]:
|
||||
"""计算覆盖矩阵"""
|
||||
matrix = []
|
||||
for loc in self.candidate_locations:
|
||||
coverage = []
|
||||
for demand in self.demand_points:
|
||||
covered = loc.distance_to(demand.location) <= self.coverage_radius
|
||||
coverage.append(covered)
|
||||
matrix.append(coverage)
|
||||
return matrix
|
||||
|
||||
def objective(self, solution: List[int]) -> float:
|
||||
"""
|
||||
计算覆盖的需求量 (负值,因为算法最小化)
|
||||
|
||||
Args:
|
||||
solution: 选中的候选位置索引列表
|
||||
|
||||
Returns:
|
||||
负的覆盖需求量
|
||||
"""
|
||||
covered = [False] * len(self.demand_points)
|
||||
|
||||
for facility_idx in solution:
|
||||
if 0 <= facility_idx < len(self.coverage_matrix):
|
||||
for j, is_covered in enumerate(self.coverage_matrix[facility_idx]):
|
||||
if is_covered:
|
||||
covered[j] = True
|
||||
|
||||
total_demand = sum(
|
||||
self.demand_points[j].demand
|
||||
for j, c in enumerate(covered) if c
|
||||
)
|
||||
|
||||
return -total_demand # 负值用于最小化
|
||||
|
||||
def get_initial_solution(self) -> List[int]:
|
||||
"""获取初始解"""
|
||||
n = len(self.candidate_locations)
|
||||
if n <= self.num_facilities:
|
||||
return list(range(n))
|
||||
return random.sample(range(n), self.num_facilities)
|
||||
|
||||
|
||||
class MaxCoverageGAOptimizer(GeneticAlgorithm):
|
||||
"""针对最大覆盖问题的遗传算法优化器"""
|
||||
|
||||
def _crossover_operators(self, parent1: List[int], parent2: List[int]) -> Tuple[List[int], List[int]]:
|
||||
"""单点交叉"""
|
||||
if not parent1 or not parent2:
|
||||
return parent1, parent2
|
||||
|
||||
size = min(len(parent1), len(parent2))
|
||||
if size < 2:
|
||||
return parent1, parent2
|
||||
|
||||
point = random.randint(1, size - 1)
|
||||
|
||||
child1 = parent1[:point] + [x for x in parent2[point:] if x not in parent1[:point]]
|
||||
child2 = parent2[:point] + [x for x in parent1[point:] if x not in parent2[:point]]
|
||||
|
||||
# 补足长度
|
||||
all_indices = set(range(len(self.problem.candidate_locations)))
|
||||
while len(child1) < len(parent1):
|
||||
available = list(all_indices - set(child1))
|
||||
if available:
|
||||
child1.append(random.choice(available))
|
||||
else:
|
||||
break
|
||||
|
||||
while len(child2) < len(parent2):
|
||||
available = list(all_indices - set(child2))
|
||||
if available:
|
||||
child2.append(random.choice(available))
|
||||
else:
|
||||
break
|
||||
|
||||
return child1[:len(parent1)], child2[:len(parent2)]
|
||||
|
||||
def _mutate(self, individual: List[int]) -> List[int]:
|
||||
"""变异操作"""
|
||||
if not individual:
|
||||
return individual
|
||||
|
||||
mutated = individual.copy()
|
||||
n = len(self.problem.candidate_locations)
|
||||
|
||||
# 随机替换一个基因
|
||||
idx = random.randint(0, len(mutated) - 1)
|
||||
available = [i for i in range(n) if i not in mutated]
|
||||
if available:
|
||||
mutated[idx] = random.choice(available)
|
||||
|
||||
return mutated
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 路径优化 (TSP)
|
||||
# ============================================================================
|
||||
|
||||
class TSPProblem(OptimizationProblem):
|
||||
"""
|
||||
旅行商问题 (TSP)
|
||||
|
||||
寻找访问所有城市的最短路径。
|
||||
"""
|
||||
|
||||
def __init__(self, cities: List[Point]):
|
||||
"""
|
||||
初始化TSP问题
|
||||
|
||||
Args:
|
||||
cities: 城市位置列表
|
||||
"""
|
||||
super().__init__("旅行商问题")
|
||||
self.cities = cities
|
||||
self.n = len(cities)
|
||||
|
||||
def objective(self, solution: List[int]) -> float:
|
||||
"""
|
||||
计算路径总长度
|
||||
|
||||
Args:
|
||||
solution: 城市访问顺序列表
|
||||
|
||||
Returns:
|
||||
路径总长度
|
||||
"""
|
||||
if not solution or len(solution) != self.n:
|
||||
return float('inf')
|
||||
|
||||
total = 0.0
|
||||
for i in range(len(solution)):
|
||||
from_idx = solution[i]
|
||||
to_idx = solution[(i + 1) % len(solution)]
|
||||
total += self.cities[from_idx].distance_to(self.cities[to_idx])
|
||||
|
||||
return total
|
||||
|
||||
def get_initial_solution(self) -> List[int]:
|
||||
"""获取初始解 (随机顺序)"""
|
||||
solution = list(range(self.n))
|
||||
random.shuffle(solution)
|
||||
return solution
|
||||
|
||||
|
||||
class TSPSAOptimizer(SimulatedAnnealing):
|
||||
"""针对TSP的模拟退火优化器"""
|
||||
|
||||
def _generate_neighbor(self, solution: List[int]) -> List[int]:
|
||||
"""生成邻居解 (2-opt交换)"""
|
||||
if len(solution) < 2:
|
||||
return solution
|
||||
|
||||
neighbor = solution.copy()
|
||||
|
||||
# 随机选择两个位置并交换
|
||||
i, j = random.sample(range(len(neighbor)), 2)
|
||||
neighbor[i], neighbor[j] = neighbor[j], neighbor[i]
|
||||
|
||||
return neighbor
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ========================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示空间优化的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("空间优化示例演示")
|
||||
print("="*70)
|
||||
|
||||
random.seed(42)
|
||||
|
||||
# ========================================================================
|
||||
# 1. P-中值问题 (选址优化)
|
||||
# ========================================================================
|
||||
print("\n[部分 1] P-中值问题 - 商场选址优化")
|
||||
print("-" * 50)
|
||||
|
||||
# 生成需求点
|
||||
demand_points = []
|
||||
for i in range(20):
|
||||
demand_points.append(DemandPoint(
|
||||
id=f"D{i}",
|
||||
location=Point(random.uniform(0, 100), random.uniform(0, 100)),
|
||||
demand=random.uniform(50, 200)
|
||||
))
|
||||
|
||||
print(f"\n需求点数量: {len(demand_points)}")
|
||||
print(f"总需求量: {sum(d.demand for d in demand_points):.1f}")
|
||||
|
||||
# 候选位置
|
||||
candidate_locations = [
|
||||
Point(20, 20), Point(50, 20), Point(80, 20),
|
||||
Point(20, 50), Point(50, 50), Point(80, 50),
|
||||
Point(20, 80), Point(50, 80), Point(80, 80)
|
||||
]
|
||||
|
||||
print(f"\n候选位置数量: {len(candidate_locations)}")
|
||||
print("候选位置:", [str(loc) for loc in candidate_locations])
|
||||
|
||||
# 创建P-中值问题 (选择3个位置)
|
||||
p_median = PMedianProblem(demand_points, candidate_locations, p=3)
|
||||
|
||||
# 使用模拟退火求解
|
||||
sa_optimizer = LocationSAOptimizer(p_median)
|
||||
sa_result = sa_optimizer.optimize(
|
||||
initial_temp=100,
|
||||
cooling_rate=0.95,
|
||||
min_temp=0.1,
|
||||
max_iterations=1000
|
||||
)
|
||||
|
||||
print(f"\n模拟退火结果:")
|
||||
print(f" 目标函数值: {sa_result.objective_value:.2f}")
|
||||
print(f" 迭代次数: {sa_result.iterations}")
|
||||
print(f" 选中的位置: {[str(candidate_locations[i]) for i in sa_result.solution]}")
|
||||
|
||||
# 贪心算法对比
|
||||
greedy_optimizer = GreedyOptimizer(p_median)
|
||||
|
||||
# 简单的贪心: 逐步添加最优位置
|
||||
best_solution = None
|
||||
best_value = float('inf')
|
||||
|
||||
for _ in range(100):
|
||||
solution = p_median.get_initial_solution()
|
||||
value = p_median.objective(solution)
|
||||
if value < best_value:
|
||||
best_value = value
|
||||
best_solution = solution
|
||||
|
||||
print(f"\n随机搜索对比:")
|
||||
print(f" 目标函数值: {best_value:.2f}")
|
||||
print(f" 选中的位置: {[str(candidate_locations[i]) for i in best_solution]}")
|
||||
|
||||
# ========================================================================
|
||||
# 2. 最大覆盖问题
|
||||
# ========================================================================
|
||||
print("\n\n[部分 2] 最大覆盖问题 - 5G基站选址")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建更大规模的需求点
|
||||
coverage_demands = []
|
||||
for i in range(50):
|
||||
coverage_demands.append(DemandPoint(
|
||||
id=f"C{i}",
|
||||
location=Point(random.uniform(0, 100), random.uniform(0, 100)),
|
||||
demand=random.uniform(10, 100)
|
||||
))
|
||||
|
||||
coverage_radius = 25
|
||||
num_bases = 5
|
||||
|
||||
max_coverage = MaxCoverageProblem(
|
||||
coverage_demands,
|
||||
candidate_locations,
|
||||
num_bases,
|
||||
coverage_radius
|
||||
)
|
||||
|
||||
# 使用遗传算法求解
|
||||
ga_optimizer = MaxCoverageGAOptimizer(max_coverage)
|
||||
ga_result = ga_optimizer.optimize(
|
||||
population_size=50,
|
||||
generations=100,
|
||||
mutation_rate=0.1
|
||||
)
|
||||
|
||||
covered_demand = -ga_result.objective_value
|
||||
total_demand = sum(d.demand for d in coverage_demands)
|
||||
coverage_ratio = covered_demand / total_demand * 100
|
||||
|
||||
print(f"\n遗传算法结果:")
|
||||
print(f" 覆盖需求量: {covered_demand:.1f} / {total_demand:.1f}")
|
||||
print(f" 覆盖率: {coverage_ratio:.1f}%")
|
||||
print(f" 迭代次数: {ga_result.iterations}")
|
||||
print(f" 选中的位置: {[str(candidate_locations[i]) for i in ga_result.solution]}")
|
||||
|
||||
# ========================================================================
|
||||
# 3. 旅行商问题 (TSP)
|
||||
# ========================================================================
|
||||
print("\n\n[部分 3] 旅行商问题 - 配送路线优化")
|
||||
print("-" * 50)
|
||||
|
||||
# 生成城市
|
||||
cities = []
|
||||
for i in range(15):
|
||||
cities.append(Point(random.uniform(0, 100), random.uniform(0, 100)))
|
||||
|
||||
print(f"\n城市数量: {len(cities)}")
|
||||
print(f"城市位置: {[str(city) for city in cities[:5]]}...")
|
||||
|
||||
tsp = TSPProblem(cities)
|
||||
tsp_optimizer = TSPSAOptimizer(tsp)
|
||||
tsp_result = tsp_optimizer.optimize(
|
||||
initial_temp=1000,
|
||||
cooling_rate=0.99,
|
||||
min_temp=0.01,
|
||||
max_iterations=5000
|
||||
)
|
||||
|
||||
print(f"\n模拟退火结果:")
|
||||
print(f" 最短路径长度: {tsp_result.objective_value:.2f}")
|
||||
print(f" 访问顺序: {[cities[i].__repr__() for i in tsp_result.solution[:5]]}...")
|
||||
|
||||
# 对比随机解
|
||||
random_solution = list(range(len(cities)))
|
||||
random.shuffle(random_solution)
|
||||
random_length = tsp.objective(random_solution)
|
||||
print(f"\n随机解对比:")
|
||||
print(f" 路径长度: {random_length:.2f}")
|
||||
print(f" 改进: {(1 - tsp_result.objective_value / random_length) * 100:.1f}%")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 多目标优化讨论
|
||||
# ========================================================================
|
||||
print("\n\n[部分 4] 多目标优化说明")
|
||||
print("-" * 50)
|
||||
|
||||
print("""
|
||||
在实际应用中,空间优化往往涉及多个目标:
|
||||
|
||||
1. 成本最小化
|
||||
- 设施建设成本
|
||||
- 运营成本
|
||||
- 运输成本
|
||||
|
||||
2. 服务最大化
|
||||
- 覆盖范围
|
||||
- 服务质量
|
||||
- 响应时间
|
||||
|
||||
3. 公平性
|
||||
- 服务均等化
|
||||
- 负载均衡
|
||||
|
||||
4. 环境影响
|
||||
- 最小化污染
|
||||
- 保护生态
|
||||
|
||||
处理方法:
|
||||
- 加权求和法 (将多目标转为单目标)
|
||||
- 帕累托优化 (寻找非劣解集)
|
||||
- 约束法 (将部分目标转为约束)
|
||||
- 目标规划 (设定目标满意水平)
|
||||
""")
|
||||
|
||||
# ========================================================================
|
||||
# 5. 收敛过程可视化 (文本)
|
||||
# ========================================================================
|
||||
print("\n[部分 5] 优化过程")
|
||||
print("-" * 50)
|
||||
|
||||
if len(ga_result.convergence_history) > 0:
|
||||
print("\n遗传算法收敛过程:")
|
||||
steps = min(10, len(ga_result.convergence_history))
|
||||
step_size = len(ga_result.convergence_history) // steps
|
||||
|
||||
for i in range(0, len(ga_result.convergence_history), step_size):
|
||||
iteration = i
|
||||
value = -ga_result.convergence_history[i] # 转回正值
|
||||
print(f" 迭代 {iteration:4d}: 覆盖需求量 = {value:.1f}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,957 @@
|
||||
"""
|
||||
空间推理示例 (Spatial Reasoning Example)
|
||||
=======================================
|
||||
|
||||
本示例展示空间智能系统中的空间推理方法。
|
||||
空间推理是从已知空间事实推导新知识的过程。
|
||||
|
||||
核心概念:
|
||||
1. 定性推理 - 使用定性术语描述空间关系
|
||||
2. 定量推理 - 使用精确数值计算
|
||||
3. 空间逻辑 - 形式化的空间推理规则
|
||||
4. 路径规划 - 寻找最优路径
|
||||
5. 可见性分析 - 判断视线可见性
|
||||
|
||||
应用场景:
|
||||
- 导航与路径规划
|
||||
- 空间查询与分析
|
||||
- 地理推理系统
|
||||
- 机器人导航
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import heapq
|
||||
from typing import List, Dict, Tuple, Optional, Set, Any
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
# 导入空间表征示例中的基础类
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
try:
|
||||
from spatial_representation import Point, LineString, Polygon, Envelope
|
||||
except ImportError:
|
||||
# 如果导入失败,定义简化版本
|
||||
@dataclass
|
||||
class Point:
|
||||
x: float
|
||||
y: float
|
||||
def distance_to(self, other):
|
||||
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
||||
def __repr__(self):
|
||||
return f"({self.x:.2f}, {self.y:.2f})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 定性空间推理
|
||||
# ============================================================================
|
||||
|
||||
class CardinalDirection(Enum):
|
||||
"""基本方向"""
|
||||
NORTH = "N"
|
||||
SOUTH = "S"
|
||||
EAST = "E"
|
||||
WEST = "W"
|
||||
NORTHEAST = "NE"
|
||||
NORTHWEST = "NW"
|
||||
SOUTHEAST = "SE"
|
||||
SOUTHWEST = "SW"
|
||||
|
||||
@classmethod
|
||||
def from_angle(cls, angle: float) -> 'CardinalDirection':
|
||||
"""
|
||||
从角度获取方向
|
||||
|
||||
Args:
|
||||
angle: 角度 (度, 0=东, 90=北)
|
||||
|
||||
Returns:
|
||||
方向枚举
|
||||
"""
|
||||
# 归一化到0-360
|
||||
angle = angle % 360
|
||||
|
||||
if angle >= 337.5 or angle < 22.5:
|
||||
return cls.EAST
|
||||
elif 22.5 <= angle < 67.5:
|
||||
return cls.NORTHEAST
|
||||
elif 67.5 <= angle < 112.5:
|
||||
return cls.NORTH
|
||||
elif 112.5 <= angle < 157.5:
|
||||
return cls.NORTHWEST
|
||||
elif 157.5 <= angle < 202.5:
|
||||
return cls.WEST
|
||||
elif 202.5 <= angle < 247.5:
|
||||
return cls.SOUTHWEST
|
||||
elif 247.5 <= angle < 292.5:
|
||||
return cls.SOUTH
|
||||
else: # 292.5 <= angle < 337.5
|
||||
return cls.SOUTHEAST
|
||||
|
||||
|
||||
@dataclass
|
||||
class QualitativeRelation:
|
||||
"""定性空间关系"""
|
||||
relation_type: str # "direction", "distance", "topology"
|
||||
value: str # 如 "north", "near", "inside"
|
||||
confidence: float = 1.0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.relation_type}={self.value} (conf={self.confidence:.2f})"
|
||||
|
||||
|
||||
class QualitativeReasoner:
|
||||
"""
|
||||
定性空间推理器
|
||||
|
||||
使用定性术语进行空间推理。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.facts: List[Tuple[str, str, QualitativeRelation]] = []
|
||||
|
||||
def add_fact(self, entity1: str, entity2: str,
|
||||
relation: QualitativeRelation) -> None:
|
||||
"""添加空间事实"""
|
||||
self.facts.append((entity1, entity2, relation))
|
||||
|
||||
def infer_direction(self, from_point: Point, to_point: Point) -> QualitativeRelation:
|
||||
"""
|
||||
推断两点之间的方向关系
|
||||
|
||||
Args:
|
||||
from_point: 起始点
|
||||
to_point: 目标点
|
||||
|
||||
Returns:
|
||||
方向关系
|
||||
"""
|
||||
dx = to_point.x - from_point.x
|
||||
dy = to_point.y - from_point.y
|
||||
|
||||
# 计算角度 (从东开始逆时针)
|
||||
angle = math.degrees(math.atan2(dy, dx))
|
||||
|
||||
direction = CardinalDirection.from_angle(angle)
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="direction",
|
||||
value=direction.value,
|
||||
confidence=1.0
|
||||
)
|
||||
|
||||
def infer_distance_category(self, p1: Point, p2: Point,
|
||||
thresholds: Dict[str, float] = None) -> QualitativeRelation:
|
||||
"""
|
||||
推断距离类别
|
||||
|
||||
Args:
|
||||
p1: 第一个点
|
||||
p2: 第二个点
|
||||
thresholds: 距离阈值字典
|
||||
|
||||
Returns:
|
||||
距离类别关系
|
||||
"""
|
||||
if thresholds is None:
|
||||
thresholds = {
|
||||
"very_close": 100,
|
||||
"close": 500,
|
||||
"moderate": 1000,
|
||||
"far": 5000
|
||||
}
|
||||
|
||||
dist = p1.distance_to(p2)
|
||||
|
||||
if dist < thresholds.get("very_close", 100):
|
||||
category = "very_close"
|
||||
elif dist < thresholds.get("close", 500):
|
||||
category = "close"
|
||||
elif dist < thresholds.get("moderate", 1000):
|
||||
category = "moderate"
|
||||
else:
|
||||
category = "far"
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="distance",
|
||||
value=category,
|
||||
confidence=1.0
|
||||
)
|
||||
|
||||
def compose_relations(self, rel1: QualitativeRelation,
|
||||
rel2: QualitativeRelation) -> QualitativeRelation:
|
||||
"""
|
||||
组合两个关系
|
||||
|
||||
例如: A在B的北边,B在C的东边 -> A在C的东北边
|
||||
"""
|
||||
if rel1.relation_type == "direction" and rel2.relation_type == "direction":
|
||||
# 方向组合
|
||||
return self._compose_directions(rel1.value, rel2.value)
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="unknown",
|
||||
value="unknown",
|
||||
confidence=0.5
|
||||
)
|
||||
|
||||
def _compose_directions(self, dir1: str, dir2: str) -> QualitativeRelation:
|
||||
"""组合两个方向"""
|
||||
direction_map = {
|
||||
"N": (0, 1), "S": (0, -1), "E": (1, 0), "W": (-1, 0),
|
||||
"NE": (1, 1), "NW": (-1, 1), "SE": (1, -1), "SW": (-1, -1)
|
||||
}
|
||||
|
||||
if dir1 in direction_map and dir2 in direction_map:
|
||||
v1 = direction_map[dir1]
|
||||
v2 = direction_map[dir2]
|
||||
|
||||
# 向量相加
|
||||
result = (v1[0] + v2[0], v1[1] + v2[1])
|
||||
|
||||
# 找到最接近的方向
|
||||
best_dir = "unknown"
|
||||
best_dot = -1
|
||||
|
||||
for name, vec in direction_map.items():
|
||||
dot = result[0] * vec[0] + result[1] * vec[1]
|
||||
if dot > best_dot:
|
||||
best_dot = dot
|
||||
best_dir = name
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="direction",
|
||||
value=best_dir,
|
||||
confidence=0.8
|
||||
)
|
||||
|
||||
return QualitativeRelation("direction", "unknown", 0.3)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 路径规划
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
"""图节点"""
|
||||
id: str
|
||||
point: Point
|
||||
neighbors: List[str] = field(default_factory=list)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Edge:
|
||||
"""图边"""
|
||||
from_node: str
|
||||
to_node: str
|
||||
weight: float # 权重 (如距离)
|
||||
bidirectional: bool = True
|
||||
|
||||
|
||||
class SpatialGraph:
|
||||
"""
|
||||
空间图
|
||||
|
||||
用于路径规划的空间网络结构。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.nodes: Dict[str, Node] = {}
|
||||
self.edges: List[Edge] = []
|
||||
|
||||
def add_node(self, id: str, point: Point) -> Node:
|
||||
"""添加节点"""
|
||||
node = Node(id=id, point=point)
|
||||
self.nodes[id] = node
|
||||
return node
|
||||
|
||||
def add_edge(self, from_id: str, to_id: str, weight: float = None,
|
||||
bidirectional: bool = True) -> None:
|
||||
"""添加边"""
|
||||
if from_id not in self.nodes or to_id not in self.nodes:
|
||||
raise ValueError("节点不存在")
|
||||
|
||||
# 如果未指定权重,使用欧氏距离
|
||||
if weight is None:
|
||||
weight = self.nodes[from_id].point.distance_to(self.nodes[to_id].point)
|
||||
|
||||
edge = Edge(from_id, to_id, weight, bidirectional)
|
||||
self.edges.append(edge)
|
||||
|
||||
# 更新邻接关系
|
||||
self.nodes[from_id].neighbors.append(to_id)
|
||||
if bidirectional:
|
||||
self.nodes[to_id].neighbors.append(from_id)
|
||||
|
||||
def get_edge_weight(self, from_id: str, to_id: str) -> float:
|
||||
"""获取边的权重"""
|
||||
for edge in self.edges:
|
||||
if edge.from_node == from_id and edge.to_node == to_id:
|
||||
return edge.weight
|
||||
if edge.bidirectional and edge.from_node == to_id and edge.to_node == from_id:
|
||||
return edge.weight
|
||||
return float('inf')
|
||||
|
||||
def shortest_path(self, start_id: str, end_id: str) -> Optional[List[str]]:
|
||||
"""
|
||||
使用Dijkstra算法计算最短路径
|
||||
|
||||
Args:
|
||||
start_id: 起始节点ID
|
||||
end_id: 目标节点ID
|
||||
|
||||
Returns:
|
||||
节点ID列表,表示路径
|
||||
"""
|
||||
if start_id not in self.nodes or end_id not in self.nodes:
|
||||
return None
|
||||
|
||||
# 优先队列: (距离, 节点ID)
|
||||
pq = [(0, start_id)]
|
||||
# 距离字典
|
||||
distances = {node_id: float('inf') for node_id in self.nodes}
|
||||
distances[start_id] = 0
|
||||
# 前驱节点
|
||||
previous = {start_id: None}
|
||||
# 已访问
|
||||
visited = set()
|
||||
|
||||
while pq:
|
||||
current_dist, current_id = heapq.heappop(pq)
|
||||
|
||||
if current_id in visited:
|
||||
continue
|
||||
visited.add(current_id)
|
||||
|
||||
if current_id == end_id:
|
||||
break
|
||||
|
||||
# 检查所有邻居
|
||||
for neighbor_id in self.nodes[current_id].neighbors:
|
||||
if neighbor_id in visited:
|
||||
continue
|
||||
|
||||
edge_weight = self.get_edge_weight(current_id, neighbor_id)
|
||||
new_dist = current_dist + edge_weight
|
||||
|
||||
if new_dist < distances[neighbor_id]:
|
||||
distances[neighbor_id] = new_dist
|
||||
previous[neighbor_id] = current_id
|
||||
heapq.heappush(pq, (new_dist, neighbor_id))
|
||||
|
||||
# 重建路径
|
||||
if distances[end_id] == float('inf'):
|
||||
return None
|
||||
|
||||
path = []
|
||||
current = end_id
|
||||
while current is not None:
|
||||
path.append(current)
|
||||
current = previous.get(current)
|
||||
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
def shortest_path_distance(self, start_id: str, end_id: str) -> float:
|
||||
"""获取最短路径距离"""
|
||||
path = self.shortest_path(start_id, end_id)
|
||||
if not path:
|
||||
return float('inf')
|
||||
|
||||
total = 0.0
|
||||
for i in range(len(path) - 1):
|
||||
total += self.get_edge_weight(path[i], path[i + 1])
|
||||
|
||||
return total
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# A*路径规划
|
||||
# ============================================================================
|
||||
|
||||
class AStarPlanner:
|
||||
"""
|
||||
A*路径规划器
|
||||
|
||||
使用启发式搜索的高效路径规划算法。
|
||||
"""
|
||||
|
||||
def __init__(self, graph: SpatialGraph):
|
||||
self.graph = graph
|
||||
|
||||
def heuristic(self, node_id: str, goal_id: str) -> float:
|
||||
"""
|
||||
启发式函数 (使用欧氏距离)
|
||||
|
||||
Args:
|
||||
node_id: 当前节点
|
||||
goal_id: 目标节点
|
||||
|
||||
Returns:
|
||||
启发式估计值
|
||||
"""
|
||||
if node_id not in self.graph.nodes or goal_id not in self.graph.nodes:
|
||||
return 0.0
|
||||
|
||||
return self.graph.nodes[node_id].point.distance_to(
|
||||
self.graph.nodes[goal_id].point
|
||||
)
|
||||
|
||||
def plan(self, start_id: str, goal_id: str) -> Optional[Tuple[List[str], float]]:
|
||||
"""
|
||||
规划路径
|
||||
|
||||
Args:
|
||||
start_id: 起始节点ID
|
||||
goal_id: 目标节点ID
|
||||
|
||||
Returns:
|
||||
(路径节点列表, 总距离) 或 None
|
||||
"""
|
||||
if start_id not in self.graph.nodes or goal_id not in self.graph.nodes:
|
||||
return None
|
||||
|
||||
# 开集和闭集
|
||||
open_set = {start_id}
|
||||
closed_set = set()
|
||||
|
||||
# g值: 从起点到当前节点的实际距离
|
||||
g_score = {node_id: float('inf') for node_id in self.graph.nodes}
|
||||
g_score[start_id] = 0
|
||||
|
||||
# f值: g值 + 启发式值
|
||||
f_score = {node_id: float('inf') for node_id in self.graph.nodes}
|
||||
f_score[start_id] = self.heuristic(start_id, goal_id)
|
||||
|
||||
# 前驱节点
|
||||
came_from = {}
|
||||
|
||||
while open_set:
|
||||
# 获取f值最小的节点
|
||||
current = min(open_set, key=lambda x: f_score[x])
|
||||
|
||||
if current == goal_id:
|
||||
# 重建路径
|
||||
path = [current]
|
||||
total_distance = g_score[current]
|
||||
|
||||
while current in came_from:
|
||||
current = came_from[current]
|
||||
path.append(current)
|
||||
|
||||
path.reverse()
|
||||
return (path, total_distance)
|
||||
|
||||
open_set.remove(current)
|
||||
closed_set.add(current)
|
||||
|
||||
# 检查邻居
|
||||
for neighbor in self.graph.nodes[current].neighbors:
|
||||
if neighbor in closed_set:
|
||||
continue
|
||||
|
||||
# 计算 tentative_g_score
|
||||
edge_weight = self.graph.get_edge_weight(current, neighbor)
|
||||
tentative_g = g_score[current] + edge_weight
|
||||
|
||||
if neighbor not in open_set:
|
||||
open_set.add(neighbor)
|
||||
elif tentative_g >= g_score[neighbor]:
|
||||
continue
|
||||
|
||||
# 更新
|
||||
came_from[neighbor] = current
|
||||
g_score[neighbor] = tentative_g
|
||||
f_score[neighbor] = tentative_g + self.heuristic(neighbor, goal_id)
|
||||
|
||||
return None # 没有找到路径
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 可见性分析
|
||||
# ============================================================================
|
||||
|
||||
class VisibilityAnalyzer:
|
||||
"""
|
||||
可见性分析器
|
||||
|
||||
判断点之间的可见性,考虑障碍物。
|
||||
"""
|
||||
|
||||
def __init__(self, obstacles: List[Polygon] = None):
|
||||
"""
|
||||
初始化可见性分析器
|
||||
|
||||
Args:
|
||||
obstacles: 障碍物多边形列表
|
||||
"""
|
||||
self.obstacles = obstacles or []
|
||||
|
||||
def add_obstacle(self, obstacle: Polygon) -> None:
|
||||
"""添加障碍物"""
|
||||
self.obstacles.append(obstacle)
|
||||
|
||||
def is_visible(self, p1: Point, p2: Point,
|
||||
tolerance: float = 1e-6) -> bool:
|
||||
"""
|
||||
判断两点之间是否可见
|
||||
|
||||
Args:
|
||||
p1: 第一个点
|
||||
p2: 第二个点
|
||||
tolerance: 容差
|
||||
|
||||
Returns:
|
||||
是否可见
|
||||
"""
|
||||
# 检视线是否与任何障碍物相交
|
||||
for obstacle in self.obstacles:
|
||||
if self._line_intersects_polygon(p1, p2, obstacle):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _line_intersects_polygon(self, p1: Point, p2: Point,
|
||||
polygon: Polygon) -> bool:
|
||||
"""判断线段是否与多边形相交"""
|
||||
# 首先检查包围盒
|
||||
line_min_x = min(p1.x, p2.x)
|
||||
line_max_x = max(p1.x, p2.x)
|
||||
line_min_y = min(p1.y, p2.y)
|
||||
line_max_y = max(p1.y, p2.y)
|
||||
|
||||
poly_min_x = min(p.x for p in polygon.exterior)
|
||||
poly_max_x = max(p.x for p in polygon.exterior)
|
||||
poly_min_y = min(p.y for p in polygon.exterior)
|
||||
poly_max_y = max(p.y for p in polygon.exterior)
|
||||
|
||||
# 包围盒不相交
|
||||
if line_max_x < poly_min_x or line_min_x > poly_max_x or \
|
||||
line_max_y < poly_min_y or line_min_y > poly_max_y:
|
||||
return False
|
||||
|
||||
# 检查线段是否与多边形的任何边相交
|
||||
n = len(polygon.exterior)
|
||||
for i in range(n):
|
||||
v1 = polygon.exterior[i]
|
||||
v2 = polygon.exterior[(i + 1) % n]
|
||||
|
||||
if self._segments_intersect(p1, p2, v1, v2):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _segments_intersect(self, p1: Point, p2: Point,
|
||||
p3: Point, p4: Point) -> bool:
|
||||
"""判断两条线段是否相交"""
|
||||
def orientation(a, b, c):
|
||||
val = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y)
|
||||
if abs(val) < 1e-10:
|
||||
return 0 # 共线
|
||||
return 1 if val > 0 else 2 # 顺时针/逆时针
|
||||
|
||||
def on_segment(a, b, c):
|
||||
return min(a.x, c.x) <= b.x <= max(a.x, c.x) and \
|
||||
min(a.y, c.y) <= b.y <= max(a.y, c.y)
|
||||
|
||||
o1 = orientation(p1, p2, p3)
|
||||
o2 = orientation(p1, p2, p4)
|
||||
o3 = orientation(p3, p4, p1)
|
||||
o4 = orientation(p3, p4, p2)
|
||||
|
||||
# 一般情况
|
||||
if o1 != o2 and o3 != o4:
|
||||
return True
|
||||
|
||||
# 特殊情况
|
||||
if o1 == 0 and on_segment(p1, p3, p2):
|
||||
return True
|
||||
if o2 == 0 and on_segment(p1, p4, p2):
|
||||
return True
|
||||
if o3 == 0 and on_segment(p3, p1, p4):
|
||||
return True
|
||||
if o4 == 0 and on_segment(p3, p2, p4):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def viewshed(self, observer: Point, radius: float,
|
||||
num_rays: int = 360) -> List[Tuple[Point, bool]]:
|
||||
"""
|
||||
计算视域 (可视范围)
|
||||
|
||||
Args:
|
||||
observer: 观察点
|
||||
radius: 视距
|
||||
num_rays: 射线数量
|
||||
|
||||
Returns:
|
||||
(点, 可见性) 列表
|
||||
"""
|
||||
results = []
|
||||
|
||||
for i in range(num_rays):
|
||||
angle = 2 * math.pi * i / num_rays
|
||||
target = Point(
|
||||
observer.x + radius * math.cos(angle),
|
||||
observer.y + radius * math.sin(angle)
|
||||
)
|
||||
|
||||
visible = self.is_visible(observer, target)
|
||||
results.append((target, visible))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间推理引擎
|
||||
# ============================================================================
|
||||
|
||||
class SpatialReasoningEngine:
|
||||
"""
|
||||
空间推理引擎
|
||||
|
||||
集成多种空间推理功能的综合引擎。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.qualitative_reasoner = QualitativeReasoner()
|
||||
self.graph = SpatialGraph()
|
||||
self.visibility_analyzer = VisibilityAnalyzer()
|
||||
self.astar_planner = None
|
||||
|
||||
def build_road_network(self, points: List[Tuple[str, Point]],
|
||||
connections: List[Tuple[str, str]]) -> None:
|
||||
"""
|
||||
构建道路网络
|
||||
|
||||
Args:
|
||||
points: (节点ID, 点) 列表
|
||||
connections: (节点1, 节点2) 连接列表
|
||||
"""
|
||||
for id, point in points:
|
||||
self.graph.add_node(id, point)
|
||||
|
||||
for id1, id2 in connections:
|
||||
self.graph.add_edge(id1, id2)
|
||||
|
||||
self.astar_planner = AStarPlanner(self.graph)
|
||||
|
||||
def navigate(self, start: str, goal: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
导航规划
|
||||
|
||||
Args:
|
||||
start: 起始点ID
|
||||
goal: 目标点ID
|
||||
|
||||
Returns:
|
||||
导航结果字典
|
||||
"""
|
||||
if not self.astar_planner:
|
||||
return None
|
||||
|
||||
result = self.astar_planner.plan(start, goal)
|
||||
|
||||
if result:
|
||||
path, distance = result
|
||||
|
||||
# 计算方向指示
|
||||
directions = []
|
||||
for i in range(len(path) - 1):
|
||||
from_node = self.graph.nodes[path[i]]
|
||||
to_node = self.graph.nodes[path[i + 1]]
|
||||
|
||||
relation = self.qualitative_reasoner.infer_direction(
|
||||
from_node.point, to_node.point
|
||||
)
|
||||
directions.append({
|
||||
"from": path[i],
|
||||
"to": path[i + 1],
|
||||
"direction": relation.value,
|
||||
"distance": from_node.point.distance_to(to_node.point)
|
||||
})
|
||||
|
||||
return {
|
||||
"path": path,
|
||||
"total_distance": distance,
|
||||
"num_steps": len(path) - 1,
|
||||
"directions": directions
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def query_relation(self, entity1: str, entity2: str,
|
||||
point1: Point, point2: Point) -> Dict[str, Any]:
|
||||
"""
|
||||
查询两个实体之间的空间关系
|
||||
|
||||
Args:
|
||||
entity1: 实体1名称
|
||||
entity2: 实体2名称
|
||||
point1: 实体1位置
|
||||
point2: 实体2位置
|
||||
|
||||
Returns:
|
||||
关系字典
|
||||
"""
|
||||
direction = self.qualitative_reasoner.infer_direction(point1, point2)
|
||||
distance_cat = self.qualitative_reasoner.infer_distance_category(point1, point2)
|
||||
actual_distance = point1.distance_to(point2)
|
||||
|
||||
return {
|
||||
"entity1": entity1,
|
||||
"entity2": entity2,
|
||||
"direction": direction.value,
|
||||
"distance_category": distance_cat.value,
|
||||
"actual_distance": actual_distance,
|
||||
"bearing": math.degrees(math.atan2(
|
||||
point2.y - point1.y,
|
||||
point2.x - point1.x
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ========================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示空间推理的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("空间推理示例演示")
|
||||
print("="*70)
|
||||
|
||||
# ========================================================================
|
||||
# 1. 定性空间推理
|
||||
# ========================================================================
|
||||
print("\n[部分 1] 定性空间推理")
|
||||
print("-" * 50)
|
||||
|
||||
reasoner = QualitativeReasoner()
|
||||
|
||||
# 创建几个地标点
|
||||
landmarks = {
|
||||
"西湖": Point(120.148, 30.259),
|
||||
"钱塘江": Point(120.250, 30.200),
|
||||
"滨江": Point(120.350, 30.220),
|
||||
"萧山": Point(120.400, 30.150)
|
||||
}
|
||||
|
||||
print("\n地标位置:")
|
||||
for name, point in landmarks.items():
|
||||
print(f" {name}: {point}")
|
||||
|
||||
# 推断方向关系
|
||||
print("\n方向关系:")
|
||||
for i, (name1, point1) in enumerate(list(landmarks.items())[:-1]):
|
||||
name2 = list(landmarks.keys())[i + 1]
|
||||
point2 = landmarks[name2]
|
||||
|
||||
relation = reasoner.infer_direction(point1, point2)
|
||||
dist_relation = reasoner.infer_distance_category(point1, point2)
|
||||
|
||||
print(f" {name1} -> {name2}:")
|
||||
print(f" 方向: {relation.value}")
|
||||
print(f" 距离类别: {dist_relation.value}")
|
||||
|
||||
# ========================================================================
|
||||
# 2. 路径规划
|
||||
# ========================================================================
|
||||
print("\n\n[部分 2] 路径规划")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建道路网络
|
||||
network_points = [
|
||||
("A", Point(0, 0)),
|
||||
("B", Point(50, 30)),
|
||||
("C", Point(100, 50)),
|
||||
("D", Point(30, 80)),
|
||||
("E", Point(80, 100)),
|
||||
("F", Point(120, 120)),
|
||||
("G", Point(150, 60))
|
||||
]
|
||||
|
||||
connections = [
|
||||
("A", "B"), ("B", "C"), ("A", "D"),
|
||||
("B", "D"), ("D", "E"), ("C", "E"),
|
||||
("E", "F"), ("C", "G"), ("G", "F")
|
||||
]
|
||||
|
||||
graph = SpatialGraph()
|
||||
for id, point in network_points:
|
||||
graph.add_node(id, point)
|
||||
for id1, id2 in connections:
|
||||
graph.add_edge(id1, id2)
|
||||
|
||||
print("\n道路网络:")
|
||||
print(f" 节点数: {len(graph.nodes)}")
|
||||
print(f" 边数: {len(graph.edges)}")
|
||||
|
||||
# Dijkstra最短路径
|
||||
print("\nDijkstra最短路径 (A -> F):")
|
||||
dijkstra_path = graph.shortest_path("A", "F")
|
||||
if dijkstra_path:
|
||||
print(f" 路径: {' -> '.join(dijkstra_path)}")
|
||||
distance = graph.shortest_path_distance("A", "F")
|
||||
print(f" 总距离: {distance:.2f}")
|
||||
|
||||
# A*路径规划
|
||||
print("\nA*路径规划 (A -> F):")
|
||||
astar = AStarPlanner(graph)
|
||||
astar_result = astar.plan("A", "F")
|
||||
if astar_result:
|
||||
path, dist = astar_result
|
||||
print(f" 路径: {' -> '.join(path)}")
|
||||
print(f" 总距离: {dist:.2f}")
|
||||
|
||||
# ========================================================================
|
||||
# 3. 可见性分析
|
||||
# ========================================================================
|
||||
print("\n\n[部分 3] 可见性分析")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建障碍物
|
||||
obstacle1 = Polygon(exterior=[
|
||||
Point(60, 40),
|
||||
Point(80, 40),
|
||||
Point(80, 70),
|
||||
Point(60, 70),
|
||||
Point(60, 40)
|
||||
])
|
||||
|
||||
obstacle2 = Polygon(exterior=[
|
||||
Point(100, 80),
|
||||
Point(130, 80),
|
||||
Point(130, 110),
|
||||
Point(100, 110),
|
||||
Point(100, 80)
|
||||
])
|
||||
|
||||
visibility = VisibilityAnalyzer([obstacle1, obstacle2])
|
||||
|
||||
print("\n障碍物:")
|
||||
print(f" 障碍物1: {obstacle1}")
|
||||
print(f" 障碍物2: {obstacle2}")
|
||||
|
||||
# 测试可见性
|
||||
observer = Point(30, 50)
|
||||
test_points = [
|
||||
("目标A", Point(90, 50)), # 被障碍物1遮挡
|
||||
("目标B", Point(120, 120)), # 被障碍物2遮挡
|
||||
("目标C", Point(150, 30)), # 可见
|
||||
("目标D", Point(50, 90)) # 可见
|
||||
]
|
||||
|
||||
print(f"\n从观察点 {observer} 观察:")
|
||||
for name, target in test_points:
|
||||
visible = visibility.is_visible(observer, target)
|
||||
status = "可见" if visible else "不可见"
|
||||
print(f" {name} {target}: {status}")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 综合推理引擎
|
||||
# ========================================================================
|
||||
print("\n\n[部分 4] 综合空间推理引擎")
|
||||
print("-" * 50)
|
||||
|
||||
engine = SpatialReasoningEngine()
|
||||
|
||||
# 构建城市路网
|
||||
city_points = [
|
||||
("火车站", Point(100, 100)),
|
||||
("市政府", Point(150, 120)),
|
||||
("西湖", Point(200, 100)),
|
||||
("钱江新城", Point(180, 180)),
|
||||
("滨江", Point(120, 200)),
|
||||
("萧山机场", Point(250, 250))
|
||||
]
|
||||
|
||||
city_connections = [
|
||||
("火车站", "市政府"),
|
||||
("火车站", "滨江"),
|
||||
("市政府", "西湖"),
|
||||
("市政府", "钱江新城"),
|
||||
("滨江", "钱江新城"),
|
||||
("钱江新城", "萧山机场"),
|
||||
("西湖", "萧山机场")
|
||||
]
|
||||
|
||||
engine.build_road_network(city_points, city_connections)
|
||||
|
||||
print("\n城市路网:")
|
||||
for name, point in city_points:
|
||||
print(f" {name}: {point}")
|
||||
|
||||
# 导航示例
|
||||
print("\n导航示例: 从 火车站 到 萧山机场")
|
||||
nav_result = engine.navigate("火车站", "萧山机场")
|
||||
|
||||
if nav_result:
|
||||
print(f"\n路径规划结果:")
|
||||
print(f" 路径: {' -> '.join(nav_result['path'])}")
|
||||
print(f" 总距离: {nav_result['total_distance']:.2f}")
|
||||
print(f" 步数: {nav_result['num_steps']}")
|
||||
|
||||
print(f"\n详细指引:")
|
||||
for i, step in enumerate(nav_result['directions'], 1):
|
||||
dir_map = {
|
||||
"N": "向北", "S": "向南", "E": "向东", "W": "向西",
|
||||
"NE": "向东北", "NW": "向西北", "SE": "向东南", "SW": "向西南"
|
||||
}
|
||||
direction_cn = dir_map.get(step['direction'], step['direction'])
|
||||
print(f" {i}. 从 {step['from']} {direction_cn} 前往 {step['to']} "
|
||||
f"(距离: {step['distance']:.1f})")
|
||||
|
||||
# 空间关系查询
|
||||
print("\n空间关系查询:")
|
||||
relation = engine.query_relation(
|
||||
"火车站", "西湖",
|
||||
city_points[0][1], city_points[2][1]
|
||||
)
|
||||
|
||||
print(f" {relation['entity1']} 相对于 {relation['entity2']}:")
|
||||
print(f" 方向: {relation['direction']}")
|
||||
print(f" 距离: {relation['actual_distance']:.2f}")
|
||||
print(f" 方位角: {relation['bearing']:.1f}°")
|
||||
|
||||
# ========================================================================
|
||||
# 5. 多路径比较
|
||||
# ========================================================================
|
||||
print("\n\n[部分 5] 多路径比较")
|
||||
print("-" * 50)
|
||||
|
||||
destinations = ["市政府", "西湖", "钱江新城", "滨江", "萧山机场"]
|
||||
start = "火车站"
|
||||
|
||||
print(f"\n从 {start} 到各目的地的距离:")
|
||||
results = []
|
||||
for dest in destinations:
|
||||
if dest == start:
|
||||
continue
|
||||
path_info = engine.navigate(start, dest)
|
||||
if path_info:
|
||||
results.append((dest, path_info['total_distance'], path_info['path']))
|
||||
|
||||
results.sort(key=lambda x: x[1])
|
||||
|
||||
for i, (dest, dist, path) in enumerate(results, 1):
|
||||
print(f" {i}. {dest:8s}: {dist:6.1f} (路径: {' -> '.join(path)})")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,982 @@
|
||||
"""
|
||||
空间表征示例 (Spatial Representation Example)
|
||||
============================================
|
||||
|
||||
本示例展示空间智能系统中的空间表征方法。
|
||||
空间表征是对地理空间现象的抽象和建模,是空间推理的基础。
|
||||
|
||||
核心概念:
|
||||
1. 空间对象模型 - 点、线、面等几何对象
|
||||
2. 空间关系模型 - 拓扑、距离、方向关系
|
||||
3. 空间场模型 - 连续表面的表示
|
||||
4. 空间索引结构 - 加速空间查询
|
||||
5. 多尺度表征 - 不同详细程度的表示
|
||||
|
||||
应用场景:
|
||||
- 地理信息系统 (GIS)
|
||||
- 空间数据库
|
||||
- 空间推理引擎
|
||||
- 地理可视化
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import json
|
||||
from typing import List, Dict, Tuple, Optional, Any, Set
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 几何类型与空间对象
|
||||
# ============================================================================
|
||||
|
||||
class GeometryType(Enum):
|
||||
"""几何类型枚举"""
|
||||
POINT = "Point"
|
||||
LINESTRING = "LineString"
|
||||
POLYGON = "Polygon"
|
||||
MULTIPOINT = "MultiPoint"
|
||||
MULTILINESTRING = "MultiLineString"
|
||||
MULTIPOLYGON = "MultiPolygon"
|
||||
GEOMETRYCOLLECTION = "GeometryCollection"
|
||||
|
||||
|
||||
class SpatialReference(Enum):
|
||||
"""空间参考系统"""
|
||||
WGS84 = "EPSG:4326" # 经纬度
|
||||
WEB_MERCATOR = "EPSG:3857" # Web墨卡托
|
||||
CGCS2000 = "EPSG:4490" # 中国大地坐标系统
|
||||
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""点几何对象"""
|
||||
x: float
|
||||
y: float
|
||||
srid: int = 4326 # 空间参考ID
|
||||
|
||||
def __iter__(self):
|
||||
"""支持解包"""
|
||||
return iter((self.x, self.y))
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.x, self.y))
|
||||
|
||||
def to_tuple(self) -> Tuple[float, float]:
|
||||
"""转换为元组"""
|
||||
return (self.x, self.y)
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON"""
|
||||
return {
|
||||
"type": "Point",
|
||||
"coordinates": [self.x, self.y]
|
||||
}
|
||||
|
||||
def distance_to(self, other: 'Point') -> float:
|
||||
"""计算到另一个点的欧氏距离"""
|
||||
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Point({self.x:.4f}, {self.y:.4f})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LineString:
|
||||
"""线几何对象"""
|
||||
coordinates: List[Point]
|
||||
srid: int = 4326
|
||||
|
||||
@property
|
||||
def length(self) -> float:
|
||||
"""计算线的长度"""
|
||||
if len(self.coordinates) < 2:
|
||||
return 0.0
|
||||
total = 0.0
|
||||
for i in range(len(self.coordinates) - 1):
|
||||
total += self.coordinates[i].distance_to(self.coordinates[i + 1])
|
||||
return total
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON"""
|
||||
return {
|
||||
"type": "LineString",
|
||||
"coordinates": [[p.x, p.y] for p in self.coordinates]
|
||||
}
|
||||
|
||||
def get_point_at(self, ratio: float) -> Point:
|
||||
"""
|
||||
获取线上指定比例位置的点
|
||||
|
||||
Args:
|
||||
ratio: 0到1之间的比例值
|
||||
|
||||
Returns:
|
||||
该位置的点
|
||||
"""
|
||||
if ratio <= 0:
|
||||
return self.coordinates[0]
|
||||
if ratio >= 1:
|
||||
return self.coordinates[-1]
|
||||
|
||||
target_length = self.length * ratio
|
||||
accumulated = 0.0
|
||||
|
||||
for i in range(len(self.coordinates) - 1):
|
||||
segment_length = self.coordinates[i].distance_to(self.coordinates[i + 1])
|
||||
if accumulated + segment_length >= target_length:
|
||||
# 在此段上
|
||||
segment_ratio = (target_length - accumulated) / segment_length
|
||||
p1 = self.coordinates[i]
|
||||
p2 = self.coordinates[i + 1]
|
||||
return Point(
|
||||
p1.x + (p2.x - p1.x) * segment_ratio,
|
||||
p1.y + (p2.y - p1.y) * segment_ratio
|
||||
)
|
||||
accumulated += segment_length
|
||||
|
||||
return self.coordinates[-1]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"LineString({len(self.coordinates)} points)"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Polygon:
|
||||
"""面几何对象"""
|
||||
exterior: List[Point] # 外环
|
||||
interiors: List[List[Point]] = field(default_factory=list) # 内环(空洞)
|
||||
srid: int = 4326
|
||||
|
||||
@property
|
||||
def area(self) -> float:
|
||||
"""使用鞋带公式计算多边形面积"""
|
||||
return self._ring_area(self.exterior) - sum(
|
||||
self._ring_area(interior) for interior in self.interiors
|
||||
)
|
||||
|
||||
def _ring_area(self, ring: List[Point]) -> float:
|
||||
"""计算环的面积(绝对值)"""
|
||||
if len(ring) < 3:
|
||||
return 0.0
|
||||
area = 0.0
|
||||
for i in range(len(ring)):
|
||||
j = (i + 1) % len(ring)
|
||||
area += ring[i].x * ring[j].y
|
||||
area -= ring[j].x * ring[i].y
|
||||
return abs(area) / 2
|
||||
|
||||
@property
|
||||
def centroid(self) -> Point:
|
||||
"""计算多边形质心"""
|
||||
if len(self.exterior) < 3:
|
||||
return self.exterior[0] if self.exterior else Point(0, 0)
|
||||
|
||||
# 简化:使用外环的顶点平均值
|
||||
avg_x = sum(p.x for p in self.exterior) / len(self.exterior)
|
||||
avg_y = sum(p.y for p in self.exterior) / len(self.exterior)
|
||||
return Point(avg_x, avg_y)
|
||||
|
||||
def contains_point(self, point: Point) -> bool:
|
||||
"""
|
||||
判断点是否在多边形内(射线法)
|
||||
|
||||
Args:
|
||||
point: 待判断的点
|
||||
|
||||
Returns:
|
||||
是否在多边形内
|
||||
"""
|
||||
return self._point_in_ring(point, self.exterior) and \
|
||||
all(not self._point_in_ring(point, interior)
|
||||
for interior in self.interiors)
|
||||
|
||||
def _point_in_ring(self, point: Point, ring: List[Point]) -> bool:
|
||||
"""射线法判断点是否在环内"""
|
||||
if len(ring) < 3:
|
||||
return False
|
||||
|
||||
x, y = point.x, point.y
|
||||
inside = False
|
||||
|
||||
for i in range(len(ring)):
|
||||
j = (i + 1) % len(ring)
|
||||
xi, yi = ring[i].x, ring[i].y
|
||||
xj, yj = ring[j].x, ring[j].y
|
||||
|
||||
# 检查射线与边的交点
|
||||
if ((yi > y) != (yj > y)) and \
|
||||
(x < (xj - xi) * (y - yi) / (yj - yi + 1e-10) + xi):
|
||||
inside = not inside
|
||||
|
||||
return inside
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON"""
|
||||
coords = [[[p.x, p.y] for p in self.exterior]]
|
||||
coords.extend([[[p.x, p.y] for p in interior] for interior in self.interiors])
|
||||
|
||||
return {
|
||||
"type": "Polygon",
|
||||
"coordinates": coords
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
holes = len(self.interiors)
|
||||
return f"Polygon({len(self.exterior)} vertices{f', {holes} holes' if holes > 0 else ''})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Envelope:
|
||||
"""包围盒"""
|
||||
min_x: float
|
||||
max_x: float
|
||||
min_y: float
|
||||
max_y: float
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
return self.max_x - self.min_x
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
return self.max_y - self.min_y
|
||||
|
||||
@property
|
||||
def area(self) -> float:
|
||||
return self.width * self.height
|
||||
|
||||
@property
|
||||
def center(self) -> Point:
|
||||
return Point(
|
||||
(self.min_x + self.max_x) / 2,
|
||||
(self.min_y + self.max_y) / 2
|
||||
)
|
||||
|
||||
def contains(self, point: Point) -> bool:
|
||||
"""判断点是否在包围盒内"""
|
||||
return (self.min_x <= point.x <= self.max_x and
|
||||
self.min_y <= point.y <= self.max_y)
|
||||
|
||||
def intersects(self, other: 'Envelope') -> bool:
|
||||
"""判断是否与另一个包围盒相交"""
|
||||
return not (self.max_x < other.min_x or self.min_x > other.max_x or
|
||||
self.max_y < other.min_y or self.min_y > other.max_y)
|
||||
|
||||
def union(self, other: 'Envelope') -> 'Envelope':
|
||||
"""计算与另一个包围盒的并集"""
|
||||
return Envelope(
|
||||
min_x=min(self.min_x, other.min_x),
|
||||
max_x=max(self.max_x, other.max_x),
|
||||
min_y=min(self.min_y, other.min_y),
|
||||
max_y=max(self.max_y, other.max_y)
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Envelope([{self.min_x:.2f}, {self.min_y:.2f}] -> [{self.max_x:.2f}, {self.max_y:.2f}])"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间特征对象
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class SpatialFeature:
|
||||
"""
|
||||
空间特征
|
||||
|
||||
包含几何和属性信息的完整空间对象。
|
||||
"""
|
||||
id: str
|
||||
geometry: Any # Point, LineString, Polygon等
|
||||
properties: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON Feature"""
|
||||
geom_data = None
|
||||
if isinstance(self.geometry, Point):
|
||||
geom_data = self.geometry.to_geojson()
|
||||
elif isinstance(self.geometry, LineString):
|
||||
geom_data = self.geometry.to_geojson()
|
||||
elif isinstance(self.geometry, Polygon):
|
||||
geom_data = self.geometry.to_geojson()
|
||||
|
||||
return {
|
||||
"type": "Feature",
|
||||
"id": self.id,
|
||||
"geometry": geom_data,
|
||||
"properties": self.properties
|
||||
}
|
||||
|
||||
def to_geojson_collection(self) -> Dict:
|
||||
"""转换为GeoJSON FeatureCollection"""
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": [self.to_geojson()]
|
||||
}
|
||||
|
||||
def envelope(self) -> Envelope:
|
||||
"""计算包围盒"""
|
||||
if isinstance(self.geometry, Point):
|
||||
return Envelope(
|
||||
self.geometry.x, self.geometry.x,
|
||||
self.geometry.y, self.geometry.y
|
||||
)
|
||||
elif isinstance(self.geometry, (LineString, list)):
|
||||
coords = self.geometry.coordinates if isinstance(self.geometry, LineString) else self.geometry
|
||||
xs = [p.x for p in coords]
|
||||
ys = [p.y for p in coords]
|
||||
return Envelope(min(xs), max(xs), min(ys), max(ys))
|
||||
elif isinstance(self.geometry, Polygon):
|
||||
xs = [p.x for p in self.geometry.exterior]
|
||||
ys = [p.y for p in self.geometry.exterior]
|
||||
return Envelope(min(xs), max(xs), min(ys), max(ys))
|
||||
else:
|
||||
raise ValueError(f"Unsupported geometry type: {type(self.geometry)}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SpatialFeature(id={self.id}, geom={type(self.geometry).__name__})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间关系
|
||||
# ============================================================================
|
||||
|
||||
class SpatialRelation(Enum):
|
||||
"""空间关系类型"""
|
||||
EQUALS = "equals"
|
||||
DISJOINT = "disjoint"
|
||||
INTERSECTS = "intersects"
|
||||
TOUCHES = "touches"
|
||||
CROSSES = "crosses"
|
||||
WITHIN = "within"
|
||||
CONTAINS = "contains"
|
||||
OVERLAPS = "overlaps"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TopologyRelation:
|
||||
"""
|
||||
拓扑关系描述
|
||||
|
||||
基于DE-9IM (Dimensionally Extended nine-Intersection Model)模型。
|
||||
"""
|
||||
relation: SpatialRelation
|
||||
confidence: float = 1.0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TopologyRelation({self.relation.value}, conf={self.confidence:.2f})"
|
||||
|
||||
|
||||
def calculate_relation(geom1: Any, geom2: Any) -> TopologyRelation:
|
||||
"""
|
||||
计算两个几何对象的空间关系
|
||||
|
||||
Args:
|
||||
geom1: 第一个几何对象
|
||||
geom2: 第二个几何对象
|
||||
|
||||
Returns:
|
||||
拓扑关系
|
||||
"""
|
||||
# 点-点关系
|
||||
if isinstance(geom1, Point) and isinstance(geom2, Point):
|
||||
if geom1.x == geom2.x and geom1.y == geom2.y:
|
||||
return TopologyRelation(SpatialRelation.EQUALS)
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
# 点-多边形关系
|
||||
if isinstance(geom1, Point) and isinstance(geom2, Polygon):
|
||||
if geom2.contains_point(geom1):
|
||||
return TopologyRelation(SpatialRelation.WITHIN)
|
||||
# 检查是否在边界上
|
||||
for i in range(len(geom2.exterior)):
|
||||
p1, p2 = geom2.exterior[i], geom2.exterior[(i + 1) % len(geom2.exterior)]
|
||||
if point_on_segment(geom1, p1, p2):
|
||||
return TopologyRelation(SpatialRelation.TOUCHES)
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
# 多边形-点关系
|
||||
if isinstance(geom1, Polygon) and isinstance(geom2, Point):
|
||||
relation = calculate_relation(geom2, geom1)
|
||||
if relation.relation == SpatialRelation.WITHIN:
|
||||
return TopologyRelation(SpatialRelation.CONTAINS)
|
||||
return relation
|
||||
|
||||
# 多边形-多边形关系(简化版:只检查相交)
|
||||
if isinstance(geom1, Polygon) and isinstance(geom2, Polygon):
|
||||
env1 = envelope_of_polygon(geom1)
|
||||
env2 = envelope_of_polygon(geom2)
|
||||
|
||||
if not env1.intersects(env2):
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
# 简化:检查是否有交点
|
||||
if geom1.contains_point(geom2.exterior[0]):
|
||||
return TopologyRelation(SpatialRelation.CONTAINS)
|
||||
if geom2.contains_point(geom1.exterior[0]):
|
||||
return TopologyRelation(SpatialRelation.WITHIN)
|
||||
|
||||
return TopologyRelation(SpatialRelation.INTERSECTS)
|
||||
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
|
||||
def point_on_segment(point: Point, seg_start: Point, seg_end: Point,
|
||||
tolerance: float = 1e-6) -> bool:
|
||||
"""判断点是否在线段上"""
|
||||
# 检查点是否在线段的包围盒内
|
||||
if not (min(seg_start.x, seg_end.x) - tolerance <= point.x <=
|
||||
max(seg_start.x, seg_end.x) + tolerance and
|
||||
min(seg_start.y, seg_end.y) - tolerance <= point.y <=
|
||||
max(seg_start.y, seg_end.y) + tolerance):
|
||||
return False
|
||||
|
||||
# 检查三点共线
|
||||
cross = (seg_end.x - seg_start.x) * (point.y - seg_start.y) - \
|
||||
(seg_end.y - seg_start.y) * (point.x - seg_start.x)
|
||||
return abs(cross) < tolerance
|
||||
|
||||
|
||||
def envelope_of_polygon(polygon: Polygon) -> Envelope:
|
||||
"""计算多边形的包围盒"""
|
||||
xs = [p.x for p in polygon.exterior]
|
||||
ys = [p.y for p in polygon.exterior]
|
||||
return Envelope(min(xs), max(xs), min(ys), max(ys))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间索引
|
||||
# ============================================================================
|
||||
|
||||
class RTreeNode:
|
||||
"""R树节点"""
|
||||
def __init__(self, is_leaf: bool = False):
|
||||
self.is_leaf = is_leaf
|
||||
self.envelope: Optional[Envelope] = None
|
||||
self.children: List['RTreeNode'] = []
|
||||
self.features: List[SpatialFeature] = []
|
||||
|
||||
def update_envelope(self):
|
||||
"""更新节点的包围盒"""
|
||||
if self.is_leaf and self.features:
|
||||
envelopes = [f.envelope() for f in self.features]
|
||||
self.envelope = envelopes[0]
|
||||
for env in envelopes[1:]:
|
||||
self.envelope = self.envelope.union(env)
|
||||
elif not self.is_leaf and self.children:
|
||||
self.envelope = self.children[0].envelope
|
||||
for child in self.children[1:]:
|
||||
if child.envelope:
|
||||
self.envelope = self.envelope.union(child.envelope)
|
||||
|
||||
|
||||
class RTree:
|
||||
"""
|
||||
R树空间索引
|
||||
|
||||
用于加速空间查询的树状索引结构。
|
||||
"""
|
||||
|
||||
def __init__(self, max_children: int = 4):
|
||||
"""
|
||||
初始化R树
|
||||
|
||||
Args:
|
||||
max_children: 每个节点的最大子节点数
|
||||
"""
|
||||
self.max_children = max_children
|
||||
self.root = RTreeNode(is_leaf=True)
|
||||
self.size = 0
|
||||
|
||||
def insert(self, feature: SpatialFeature) -> None:
|
||||
"""插入空间特征"""
|
||||
self._insert(self.root, feature)
|
||||
self.size += 1
|
||||
|
||||
def _insert(self, node: RTreeNode, feature: SpatialFeature) -> None:
|
||||
"""递归插入"""
|
||||
feature_env = feature.envelope()
|
||||
|
||||
if node.is_leaf:
|
||||
node.features.append(feature)
|
||||
node.update_envelope()
|
||||
|
||||
# 如果超过容量,分裂节点
|
||||
if len(node.features) > self.max_children:
|
||||
self._split(node)
|
||||
else:
|
||||
# 选择最佳子节点
|
||||
best_child = self._choose_best_child(node, feature_env)
|
||||
self._insert(best_child, feature)
|
||||
node.update_envelope()
|
||||
|
||||
def _choose_best_child(self, node: RTreeNode, env: Envelope) -> RTreeNode:
|
||||
"""选择插入代价最小的子节点"""
|
||||
best = None
|
||||
best_increase = float('inf')
|
||||
|
||||
for child in node.children:
|
||||
if child.envelope is None:
|
||||
continue
|
||||
union_env = child.envelope.union(env)
|
||||
increase = union_env.area - child.envelope.area
|
||||
|
||||
if increase < best_increase:
|
||||
best_increase = increase
|
||||
best = child
|
||||
|
||||
return best or node.children[0]
|
||||
|
||||
def _split(self, node: RTreeNode) -> None:
|
||||
"""分裂节点 (简化版)"""
|
||||
if node.is_leaf:
|
||||
# 简单分裂:将特征分成两组
|
||||
mid = len(node.features) // 2
|
||||
group1 = node.features[:mid]
|
||||
group2 = node.features[mid:]
|
||||
|
||||
node.features = group1
|
||||
|
||||
new_leaf = RTreeNode(is_leaf=True)
|
||||
new_leaf.features = group2
|
||||
new_leaf.update_envelope()
|
||||
|
||||
# 更新父节点
|
||||
if node == self.root and not node.children:
|
||||
# 根节点分裂
|
||||
new_root = RTreeNode(is_leaf=False)
|
||||
new_root.children = [node, new_leaf]
|
||||
new_root.update_envelope()
|
||||
self.root = new_root
|
||||
else:
|
||||
# 简化:不处理非根节点的分裂
|
||||
pass
|
||||
|
||||
def query(self, envelope: Envelope) -> List[SpatialFeature]:
|
||||
"""查询与包围盒相交的所有特征"""
|
||||
results = []
|
||||
self._query(self.root, envelope, results)
|
||||
return results
|
||||
|
||||
def _query(self, node: RTreeNode, envelope: Envelope,
|
||||
results: List[SpatialFeature]) -> None:
|
||||
"""递归查询"""
|
||||
if node.envelope and not node.envelope.intersects(envelope):
|
||||
return
|
||||
|
||||
if node.is_leaf:
|
||||
for feature in node.features:
|
||||
if feature.envelope().intersects(envelope):
|
||||
results.append(feature)
|
||||
else:
|
||||
for child in node.children:
|
||||
self._query(child, envelope, results)
|
||||
|
||||
def nearest_neighbor(self, point: Point, k: int = 1) -> List[Tuple[SpatialFeature, float]]:
|
||||
"""
|
||||
K近邻查询
|
||||
|
||||
Args:
|
||||
point: 查询点
|
||||
k: 返回的最近邻数量
|
||||
|
||||
Returns:
|
||||
(特征, 距离) 列表
|
||||
"""
|
||||
candidates = []
|
||||
self._collect_candidates(self.root, candidates)
|
||||
|
||||
# 计算距离并排序
|
||||
distances = []
|
||||
for feature in candidates:
|
||||
if isinstance(feature.geometry, Point):
|
||||
dist = feature.geometry.distance_to(point)
|
||||
distances.append((feature, dist))
|
||||
else:
|
||||
# 非点几何:使用包围盒中心距离
|
||||
env = feature.envelope()
|
||||
center = env.center
|
||||
dist = math.sqrt((center.x - point.x)**2 + (center.y - point.y)**2)
|
||||
distances.append((feature, dist))
|
||||
|
||||
distances.sort(key=lambda x: x[1])
|
||||
return distances[:k]
|
||||
|
||||
def _collect_candidates(self, node: RTreeNode, results: List[SpatialFeature]) -> None:
|
||||
"""收集所有候选特征"""
|
||||
if node.is_leaf:
|
||||
results.extend(node.features)
|
||||
else:
|
||||
for child in node.children:
|
||||
self._collect_candidates(child, results)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间场模型
|
||||
# ============================================================================
|
||||
|
||||
class GridField:
|
||||
"""
|
||||
栅格场模型
|
||||
|
||||
使用规则网格表示连续空间现象。
|
||||
"""
|
||||
|
||||
def __init__(self, bounds: Envelope, rows: int, cols: int,
|
||||
nodata: float = -9999):
|
||||
"""
|
||||
初始化栅格场
|
||||
|
||||
Args:
|
||||
bounds: 空间范围
|
||||
rows: 行数
|
||||
cols: 列数
|
||||
nodata: 无数据值
|
||||
"""
|
||||
self.bounds = bounds
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.nodata = nodata
|
||||
self.data = [[nodata for _ in range(cols)] for _ in range(rows)]
|
||||
|
||||
@property
|
||||
def cell_width(self) -> float:
|
||||
"""获取单元格宽度"""
|
||||
return self.bounds.width / self.cols
|
||||
|
||||
@property
|
||||
def cell_height(self) -> float:
|
||||
"""获取单元格高度"""
|
||||
return self.bounds.height / self.rows
|
||||
|
||||
def get_cell_index(self, point: Point) -> Optional[Tuple[int, int]]:
|
||||
"""
|
||||
获取点对应的栅格索引
|
||||
|
||||
Args:
|
||||
point: 空间点
|
||||
|
||||
Returns:
|
||||
(行索引, 列索引) 或 None
|
||||
"""
|
||||
if not self.bounds.contains(point):
|
||||
return None
|
||||
|
||||
col = int((point.x - self.bounds.min_x) / self.cell_width)
|
||||
row = int((self.bounds.max_y - point.y) / self.cell_height)
|
||||
|
||||
col = max(0, min(col, self.cols - 1))
|
||||
row = max(0, min(row, self.rows - 1))
|
||||
|
||||
return (row, col)
|
||||
|
||||
def set_value(self, row: int, col: int, value: float) -> None:
|
||||
"""设置栅格值"""
|
||||
if 0 <= row < self.rows and 0 <= col < self.cols:
|
||||
self.data[row][col] = value
|
||||
|
||||
def get_value(self, row: int, col: int) -> float:
|
||||
"""获取栅格值"""
|
||||
if 0 <= row < self.rows and 0 <= col < self.cols:
|
||||
return self.data[row][col]
|
||||
return self.nodata
|
||||
|
||||
def get_value_at_point(self, point: Point) -> float:
|
||||
"""获取点位置的值(最近邻)"""
|
||||
idx = self.get_cell_index(point)
|
||||
if idx:
|
||||
return self.get_value(idx[0], idx[1])
|
||||
return self.nodata
|
||||
|
||||
def interpolate_at_point(self, point: Point) -> float:
|
||||
"""双线性插值获取点位置的值"""
|
||||
idx = self.get_cell_index(point)
|
||||
if not idx:
|
||||
return self.nodata
|
||||
|
||||
row, col = idx
|
||||
|
||||
# 获取四个角点的值
|
||||
values = []
|
||||
for r in range(row, min(row + 2, self.rows)):
|
||||
for c in range(col, min(col + 2, self.cols)):
|
||||
values.append((r, c, self.data[r][c]))
|
||||
|
||||
if len(values) < 4 or any(v[2] == self.nodata for v in values):
|
||||
return self.get_value(row, col) # 回退到最近邻
|
||||
|
||||
# 双线性插值
|
||||
# 简化实现
|
||||
return self.get_value(row, col)
|
||||
|
||||
def get_statistics(self) -> Dict[str, float]:
|
||||
"""获取统计信息"""
|
||||
values = [v for row in self.data for v in row if v != self.nodata]
|
||||
|
||||
if not values:
|
||||
return {"count": 0, "min": self.nodata, "max": self.nodata,
|
||||
"mean": self.nodata, "std": 0}
|
||||
|
||||
import statistics
|
||||
return {
|
||||
"count": len(values),
|
||||
"min": min(values),
|
||||
"max": max(values),
|
||||
"mean": statistics.mean(values),
|
||||
"std": statistics.stdev(values) if len(values) > 1 else 0
|
||||
}
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON(简化:输出为点集)"""
|
||||
features = []
|
||||
for r in range(self.rows):
|
||||
for c in range(self.cols):
|
||||
val = self.data[r][c]
|
||||
if val != self.nodata:
|
||||
# 计算点坐标
|
||||
x = self.bounds.min_x + (c + 0.5) * self.cell_width
|
||||
y = self.bounds.max_y - (r + 0.5) * self.cell_height
|
||||
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {"type": "Point", "coordinates": [x, y]},
|
||||
"properties": {"value": val}
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": features
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示空间表征的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("空间表征示例演示")
|
||||
print("="*70)
|
||||
|
||||
# ========================================================================
|
||||
# 1. 基础几何对象
|
||||
# ========================================================================
|
||||
print("\n[部分 1] 基础几何对象")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建点
|
||||
point1 = Point(120.5, 30.2)
|
||||
point2 = Point(121.0, 30.5)
|
||||
print(f"\n点对象:")
|
||||
print(f" point1 = {point1}")
|
||||
print(f" point2 = {point2}")
|
||||
print(f" 距离 = {point1.distance_to(point2):.4f}")
|
||||
|
||||
# 创建线
|
||||
linestring = LineString([
|
||||
Point(120.0, 30.0),
|
||||
Point(120.5, 30.2),
|
||||
Point(121.0, 30.5),
|
||||
Point(121.5, 30.3)
|
||||
])
|
||||
print(f"\n线对象:")
|
||||
print(f" {linestring}")
|
||||
print(f" 长度 = {linestring.length:.4f}")
|
||||
print(f" 中点 = {linestring.get_point_at(0.5)}")
|
||||
|
||||
# 创建多边形
|
||||
polygon = Polygon(exterior=[
|
||||
Point(120.0, 30.0),
|
||||
Point(121.0, 30.0),
|
||||
Point(121.0, 31.0),
|
||||
Point(120.0, 31.0),
|
||||
Point(120.0, 30.0)
|
||||
])
|
||||
print(f"\n多边形对象:")
|
||||
print(f" {polygon}")
|
||||
print(f" 面积 = {polygon.area:.4f}")
|
||||
print(f" 质心 = {polygon.centroid}")
|
||||
|
||||
# 点包含测试
|
||||
test_inside = Point(120.5, 30.5)
|
||||
test_outside = Point(121.5, 30.5)
|
||||
print(f" {test_inside} 在多边形内: {polygon.contains_point(test_inside)}")
|
||||
print(f" {test_outside} 在多边形内: {polygon.contains_point(test_outside)}")
|
||||
|
||||
# ========================================================================
|
||||
# 2. 空间特征
|
||||
# ========================================================================
|
||||
print("\n\n[部分 2] 空间特征")
|
||||
print("-" * 50)
|
||||
|
||||
feature1 = SpatialFeature(
|
||||
id="poi_001",
|
||||
geometry=Point(120.5, 30.2),
|
||||
properties={
|
||||
"name": "杭州西湖",
|
||||
"type": "scenic_spot",
|
||||
"rating": 4.8
|
||||
}
|
||||
)
|
||||
|
||||
feature2 = SpatialFeature(
|
||||
id="zone_001",
|
||||
geometry=polygon,
|
||||
properties={
|
||||
"name": "开发区A",
|
||||
"type": "industrial_zone",
|
||||
"area_ha": 10000
|
||||
}
|
||||
)
|
||||
|
||||
print(f"\n特征1: {feature1}")
|
||||
print(f" 属性: {feature1.properties}")
|
||||
print(f" 包围盒: {feature1.envelope()}")
|
||||
|
||||
print(f"\n特征2: {feature2}")
|
||||
print(f" 属性: {feature2.properties}")
|
||||
print(f" 包围盒: {feature2.envelope()}")
|
||||
|
||||
# GeoJSON输出
|
||||
print(f"\nGeoJSON输出:")
|
||||
print(json.dumps(feature1.to_geojson(), ensure_ascii=False, indent=2))
|
||||
|
||||
# ========================================================================
|
||||
# 3. 空间关系
|
||||
# ========================================================================
|
||||
print("\n\n[部分 3] 空间关系")
|
||||
print("-" * 50)
|
||||
|
||||
# 点与多边形的关系
|
||||
point_a = Point(120.5, 30.5) # 在多边形内
|
||||
point_b = Point(121.5, 30.5) # 在多边形外
|
||||
point_c = Point(120.0, 30.5) # 在边界上
|
||||
|
||||
print(f"\n点-多边形关系测试:")
|
||||
print(f" {point_a} 与多边形: {calculate_relation(point_a, polygon)}")
|
||||
print(f" {point_b} 与多边形: {calculate_relation(point_b, polygon)}")
|
||||
print(f" {point_c} 与多边形: {calculate_relation(point_c, polygon)}")
|
||||
|
||||
# 多边形-多边形关系
|
||||
poly2 = Polygon(exterior=[
|
||||
Point(120.5, 29.5),
|
||||
Point(121.5, 29.5),
|
||||
Point(121.5, 30.5),
|
||||
Point(120.5, 30.5),
|
||||
Point(120.5, 29.5)
|
||||
])
|
||||
print(f"\n多边形-多边形关系:")
|
||||
print(f" poly1 与 poly2: {calculate_relation(polygon, poly2)}")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 空间索引
|
||||
# ========================================================================
|
||||
print("\n\n[部分 4] R树空间索引")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建R树
|
||||
rtree = RTree(max_children=4)
|
||||
|
||||
# 插入一些特征
|
||||
features = []
|
||||
for i in range(20):
|
||||
x = random.uniform(119, 122)
|
||||
y = random.uniform(29, 32)
|
||||
feature = SpatialFeature(
|
||||
id=f"feature_{i:03d}",
|
||||
geometry=Point(x, y),
|
||||
properties={"value": random.uniform(0, 100)}
|
||||
)
|
||||
features.append(feature)
|
||||
rtree.insert(feature)
|
||||
|
||||
print(f"\n已插入 {rtree.size} 个特征到R树")
|
||||
|
||||
# 范围查询
|
||||
query_env = Envelope(120, 121, 30, 31)
|
||||
results = rtree.query(query_env)
|
||||
print(f"\n范围查询 {query_env}:")
|
||||
print(f" 找到 {len(results)} 个特征")
|
||||
for f in results[:5]:
|
||||
print(f" - {f.id}: {f.geometry}")
|
||||
|
||||
# K近邻查询
|
||||
query_point = Point(120.5, 30.5)
|
||||
neighbors = rtree.nearest_neighbor(query_point, k=5)
|
||||
print(f"\nK近邻查询 (中心点: {query_point}):")
|
||||
for i, (f, dist) in enumerate(neighbors, 1):
|
||||
print(f" {i}. {f.id}: 距离 = {dist:.4f}")
|
||||
|
||||
# ========================================================================
|
||||
# 5. 栅格场模型
|
||||
# ========================================================================
|
||||
print("\n\n[部分 5] 栅格场模型")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建栅格场
|
||||
grid = GridField(
|
||||
bounds=Envelope(119, 122, 29, 32),
|
||||
rows=30,
|
||||
cols=30,
|
||||
nodata=-9999
|
||||
)
|
||||
|
||||
# 填充一些模拟数据 (温度场)
|
||||
for r in range(grid.rows):
|
||||
for c in range(grid.cols):
|
||||
# 计算坐标
|
||||
x = grid.bounds.min_x + (c + 0.5) * grid.cell_width
|
||||
y = grid.bounds.max_y - (r + 0.5) * grid.cell_height
|
||||
|
||||
# 模拟温度场 (简单的径向基函数)
|
||||
center_x, center_y = 120.5, 30.5
|
||||
dist = math.sqrt((x - center_x)**2 + (y - center_y)**2)
|
||||
temp = 25 - dist * 2 # 中心25度,向外递减
|
||||
|
||||
grid.set_value(r, c, round(temp, 2))
|
||||
|
||||
print(f"\n栅格场信息:")
|
||||
print(f" 范围: {grid.bounds}")
|
||||
print(f" 尺寸: {grid.rows} x {grid.cols}")
|
||||
print(f" 单元格大小: {grid.cell_width:.4f} x {grid.cell_height:.4f}")
|
||||
|
||||
stats = grid.get_statistics()
|
||||
print(f" 统计: {stats}")
|
||||
|
||||
# 点查询
|
||||
sample_point = Point(120.5, 30.5)
|
||||
value = grid.get_value_at_point(sample_point)
|
||||
print(f"\n点位置 {sample_point} 的值: {value}")
|
||||
|
||||
# ========================================================================
|
||||
# 6. GeoJSON导出
|
||||
# ========================================================================
|
||||
print("\n\n[部分 6] GeoJSON导出")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建特征集合
|
||||
feature_collection = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
feature1.to_geojson(),
|
||||
feature2.to_geojson()
|
||||
]
|
||||
}
|
||||
|
||||
print("\n特征集合:")
|
||||
print(json.dumps(feature_collection, ensure_ascii=False, indent=2))
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user