Coverage for src/sparkle/instance/instances.py: 99%

87 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-08 12:00 +0000

1"""Objects and methods relating to instances for Sparkle.""" 

2 

3from __future__ import annotations 

4from pathlib import Path 

5 

6import csv 

7import numpy as np 

8 

9 

10class InstanceSet: 

11 """Base object representation of a set of instances.""" 

12 

13 def __init__(self: InstanceSet, target: Path | list[str, Path]) -> None: 

14 """Initialise an Instances object from a directory. 

15 

16 Args: 

17 target: The Path, or list of paths to create the instance set from. 

18 """ 

19 self.directory: Path = target 

20 self._instance_names: list[str] = [] 

21 self._instance_paths: list[Path] = [] 

22 

23 @property 

24 def size(self: InstanceSet) -> int: 

25 """Returns the number of instances in the set.""" 

26 return len(self._instance_paths) 

27 

28 @property 

29 def all_paths(self: InstanceSet) -> list[Path]: 

30 """Returns all file paths in the instance set as a flat list.""" 

31 return self._instance_paths 

32 

33 @property 

34 def instance_paths(self: InstanceSet) -> list[Path]: 

35 """Get processed instance paths.""" 

36 return self._instance_paths 

37 

38 @property 

39 def instance_names(self: InstanceSet) -> list[str]: 

40 """Get processed instance names for instances.""" 

41 return self._instance_names 

42 

43 @property 

44 def instance_pairs(self: InstanceSet) -> list[tuple[str, str]]: 

45 """Return list of (set_name, instance_name) tuples.""" 

46 return [(self.directory.name, name) for name in self._instance_names] 

47 

48 @property 

49 def instances(self: InstanceSet) -> list[str]: 

50 """Get instance names with relative path.""" 

51 return [str(p.with_suffix("")) for p in self._instance_paths] 

52 

53 @property 

54 def name(self: InstanceSet) -> str: 

55 """Get instance set name.""" 

56 return self.directory.name 

57 

58 def __str__(self: InstanceSet) -> str: 

59 """Get the string representation of an Instance Set.""" 

60 return self.name 

61 

62 def __repr__(self: InstanceSet) -> str: 

63 """Get detailed representation of an Instance Set.""" 

64 return ( 

65 f"{self.name}:\n" 

66 f"\t- Type: {type(self).__name__}\n" 

67 f"\t- Directory: {self.directory}\n" 

68 f"\t- # Instances: {self.size}" 

69 ) 

70 

71 def get_path_by_name(self: InstanceSet, name: str) -> Path | list[Path]: 

72 """Retrieves an instance paths by its name. Returns None upon failure.""" 

73 for idx, instance_name in enumerate(self._instance_names): 

74 if instance_name == name: 

75 return self._instance_paths[idx] 

76 return None 

77 

78 

79class FileInstanceSet(InstanceSet): 

80 """Object representation of a set of single-file instances.""" 

81 

82 def __init__(self: FileInstanceSet, target: Path) -> None: 

83 """Initialise an InstanceSet, where each instance is a file in the directory. 

84 

85 Args: 

86 target: Path to the instances directory. If multiple files are found, 

87 they are assumed to have the same number of instances per file. 

88 """ 

89 super().__init__(target) 

90 self._name: str = target.stem 

91 if target.is_file(): 

92 # Single instance set 

93 self._instance_paths = [target] 

94 self.directory = target.parent 

95 # NOTE: We name instances by their stem, assuming every instance in the set 

96 # shares the same extension (which is the case in general). If a set ever 

97 # mixes extensions on the same stem (e.g. instance1.cnf and instance1.csv), 

98 # the bare stems would collide; in that case keep the full filename 

99 # (target.name) here to disambiguate. 

100 self._instance_names = [target.stem] 

101 else: 

102 # Default situation, treat each file in the directory as an instance 

103 self._instance_paths = [p for p in self.directory.iterdir()] 

104 self._instance_names = [p.stem for p in self._instance_paths] 

105 

106 @property 

107 def name(self: FileInstanceSet) -> str: 

108 """Get instance set name.""" 

109 return self._name 

110 

111 

112class MultiFileInstanceSet(InstanceSet): 

113 """Object representation of a set of multi-file instances.""" 

114 

115 instance_csv = "instances.csv" 

116 

117 def __init__(self: MultiFileInstanceSet, target: Path) -> None: 

118 """Initialise an Instances object from a directory. 

119 

120 Args: 

121 target: Path to the instances directory. Will read from instances.csv. 

122 """ 

123 target_dir = target.parent if not target.is_dir() else target 

124 super().__init__(target_dir) 

125 # A path pointing to the directory of instances 

126 self.instance_file = self.directory / MultiFileInstanceSet.instance_csv 

127 # Read from instance_file 

128 if not target.is_dir(): 

129 # Single file 

130 instance_list = [ 

131 line 

132 for line in csv.reader(self.instance_file.open()) 

133 if target.stem in line 

134 ] 

135 else: 

136 # Multi file 

137 instance_list = [line for line in csv.reader(self.instance_file.open())] 

138 

139 for instance in instance_list: 

140 self._instance_names.append(instance[0]) 

141 self._instance_paths.append( 

142 [(self.directory / f) if isinstance(f, str) else f for f in instance[1:]] 

143 ) 

144 

145 @property 

146 def all_paths(self: MultiFileInstanceSet) -> list[Path]: 

147 """Returns all file paths in the instance set as a flat list.""" 

148 return [p for instance in self._instance_paths for p in instance] + [ 

149 self.instance_file 

150 ] 

151 

152 @property 

153 def instances(self: InstanceSet) -> list[str]: 

154 """Get instance names with relative path for multi-file instances.""" 

155 return [self.directory / inst_name for inst_name in self.instance_names] 

156 

157 

158class IterableFileInstanceSet(InstanceSet): 

159 """Object representation of files containing multiple instances.""" 

160 

161 supported_filetypes = set([".csv", ".npy"]) 

162 

163 def __init__(self: IterableFileInstanceSet, target: Path) -> None: 

164 """Initialise an InstanceSet from a single file. 

165 

166 Args: 

167 target: Path to the instances directory. If multiple files are found, 

168 they are assumed to have the same number of instances. 

169 """ 

170 super().__init__(target) 

171 self._instance_paths = [ 

172 path 

173 for path in self.directory.iterdir() 

174 if path.suffix in IterableFileInstanceSet.supported_filetypes 

175 ] 

176 self._size = IterableFileInstanceSet.__determine_size__(self._instance_paths[0]) 

177 self._instance_names = [path.name for path in self._instance_paths] 

178 

179 @property 

180 def size(self: IterableFileInstanceSet) -> int: 

181 """Returns the number of instances in the set.""" 

182 return self._size 

183 

184 @staticmethod 

185 def __determine_size__(file: Path) -> int: 

186 """Determine the number of instances in a file.""" 

187 match file.suffix: 

188 case ".csv": 

189 return len(file.open().readlines()) 

190 case ".npy": 

191 return len(np.load(file))