Coverage for io/tabular.py: 66%

1"""

2 CSV Tabular Data Input / Output

3===============================

4

5 Define input / output utilities for reading and writing spectral data and

6 spectral distributions from/to *CSV* tabular data files.

7

8- :func:`colour.read_spectral_data_from_csv_file`

9- :func:`colour.read_sds_from_csv_file`

10- :func:`colour.write_sds_to_csv_file`

11"""

12

13 from __future__ import annotations

14

15 import csv

16 import os

17 import tempfile

18 import typing

19

20 import numpy as np

21

22 from colour.colorimetry import SpectralDistribution

23 from colour.constants import DTYPE_FLOAT_DEFAULT

24

25 if typing.TYPE_CHECKING:

26 from colour.hints import Any, Dict, NDArrayFloat, PathLike

27

28 from colour.hints import cast

29 from colour.utilities import filter_kwargs

30

31 __author__ = "Colour Developers"

32 __copyright__ = "Copyright 2013 Colour Developers"

33 __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"

34 __maintainer__ = "Colour Developers"

35 __email__ = "colour-developers@colour-science.org"

36 __status__ = "Production"

37

38 __all__ = [

39 "read_spectral_data_from_csv_file",

40 "read_sds_from_csv_file",

41 "write_sds_to_csv_file",

42]

43

44

45 def read_spectral_data_from_csv_file(

46 path: str | PathLike, **kwargs: Any

47) -> Dict[str, NDArrayFloat]:

48 """

49 Read spectral data from the specified *CSV* file in the following form::

50

51 390, 4.15003e-04, 3.68349e-04, 9.54729e-03

52 395, 1.05192e-03, 9.58658e-04, 2.38250e-02

53 400, 2.40836e-03, 2.26991e-03, 5.66498e-02

54 ...

55 830, 9.74306e-07, 9.53411e-08, 0.00000

56

57 and convert it to a *dict* as follows::

58

59 {

60 'wavelength': ndarray,

61 'field 1': ndarray,

62 'field 2': ndarray,

63 ...,

64 'field n': ndarray

65 }

66

67 Parameters

68 ----------

69 path

70 *CSV* file path.

71

72 Other Parameters

73 ----------------

74 kwargs

75 Keywords arguments passed to :func:`numpy.recfromcsv` definition.

76

77 Returns

78 -------

79 :class:`dict`

80 *CSV* file content.

81

82 Raises

83 ------

84 IOError

85 If the file cannot be read.

86

87 Notes

88 -----

89 - A *CSV* spectral data file should at least define two fields: one

90 for the wavelengths and one for the associated values of one

91 spectral distribution.

92

93 Examples

94 --------

95 >>> import os

96 >>> from pprint import pprint

97 >>> csv_file = os.path.join(

98 ... os.path.dirname(__file__),

99 ... "tests",

100 ... "resources",

101 ... "colorchecker_n_ohta.csv",

102 ... )

103 >>> sds_data = read_spectral_data_from_csv_file(csv_file)

104 >>> pprint(list(sds_data.keys()))

105 ['wavelength',

106 '1',

107 '2',

108 '3',

109 '4',

110 '5',

111 '6',

112 '7',

113 '8',

114 '9',

115 '10',

116 '11',

117 '12',

118 '13',

119 '14',

120 '15',

121 '16',

122 '17',

123 '18',

124 '19',

125 '20',

126 '21',

127 '22',

128 '23',

129 '24']

130 """

131

132 path = str(path)

133

134 settings = {

135 "names": True,

136 "delimiter": ",",

137 "case_sensitive": True,

138 # "case_sensitive": "lower",

139 "deletechars": "",

140 "replace_space": " ",

141 "dtype": DTYPE_FLOAT_DEFAULT,

142 }

143 settings.update(**kwargs)

144

145 transpose = settings.get("transpose")

146 if transpose:

147 delimiter = cast("str", settings.get("delimiter", ","))

148

149 with open(path) as csv_file:

150 content = zip(*csv.reader(csv_file, delimiter=delimiter), strict=True)

151

152 settings["delimiter"] = ","

153

154 with tempfile.NamedTemporaryFile(mode="w", delete=False) as transposed_csv_file:

155 path = transposed_csv_file.name

156 csv.writer(transposed_csv_file).writerows(content)

157 transposed_csv_file.close()

158

159 data = np.genfromtxt(path, **filter_kwargs(np.genfromtxt, **settings))

160

161 if transpose:

162 os.unlink(transposed_csv_file.name)

163

164 return {name: data[name] for name in data.dtype.names} # pyright: ignore

165

166

167 def read_sds_from_csv_file(

168 path: str | PathLike, **kwargs: Any

169) -> Dict[str, SpectralDistribution]:

170 """

171 Read spectral data from the specified *CSV* file and convert its content

172 to a *dict* of :class:`colour.SpectralDistribution` class instances.

173

174 Parameters

175 ----------

176 path

177 *CSV* file path.

178

179 Other Parameters

180 ----------------

181 kwargs

182 Keywords arguments passed to :func:`numpy.genfromtxt` definition.

183

184 Returns

185 -------

186 :class:`dict`

187 *dict* of :class:`colour.SpectralDistribution` class instances.

188

189 Raises

190 ------

191 IOError

192 If the file cannot be read.

193

194 Examples

195 --------

196 >>> from colour.utilities import numpy_print_options

197 >>> import os

198 >>> csv_file = os.path.join(

199 ... os.path.dirname(__file__),

200 ... "tests",

201 ... "resources",

202 ... "colorchecker_n_ohta.csv",

203 ... )

204 >>> sds = read_sds_from_csv_file(csv_file)

205 >>> print(tuple(sds.keys()))

206 ('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', \

207'14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24')

208 >>> with numpy_print_options(suppress=True):

209 ... sds["1"] # doctest: +ELLIPSIS

210 ...

211 SpectralDistribution([[ 380. , 0.048],

212 [ 385. , 0.051],

213 [ 390. , 0.055],

214 [ 395. , 0.06 ],

215 [ 400. , 0.065],

216 [ 405. , 0.068],

217 [ 410. , 0.068],

218 [ 415. , 0.067],

219 [ 420. , 0.064],

220 [ 425. , 0.062],

221 [ 430. , 0.059],

222 [ 435. , 0.057],

223 [ 440. , 0.055],

224 [ 445. , 0.054],

225 [ 450. , 0.053],

226 [ 455. , 0.053],

227 [ 460. , 0.052],

228 [ 465. , 0.052],

229 [ 470. , 0.052],

230 [ 475. , 0.053],

231 [ 480. , 0.054],

232 [ 485. , 0.055],

233 [ 490. , 0.057],

234 [ 495. , 0.059],

235 [ 500. , 0.061],

236 [ 505. , 0.062],

237 [ 510. , 0.065],

238 [ 515. , 0.067],

239 [ 520. , 0.07 ],

240 [ 525. , 0.072],

241 [ 530. , 0.074],

242 [ 535. , 0.075],

243 [ 540. , 0.076],

244 [ 545. , 0.078],

245 [ 550. , 0.079],

246 [ 555. , 0.082],

247 [ 560. , 0.087],

248 [ 565. , 0.092],

249 [ 570. , 0.1 ],

250 [ 575. , 0.107],

251 [ 580. , 0.115],

252 [ 585. , 0.122],

253 [ 590. , 0.129],

254 [ 595. , 0.134],

255 [ 600. , 0.138],

256 [ 605. , 0.142],

257 [ 610. , 0.146],

258 [ 615. , 0.15 ],

259 [ 620. , 0.154],

260 [ 625. , 0.158],

261 [ 630. , 0.163],

262 [ 635. , 0.167],

263 [ 640. , 0.173],

264 [ 645. , 0.18 ],

265 [ 650. , 0.188],

266 [ 655. , 0.196],

267 [ 660. , 0.204],

268 [ 665. , 0.213],

269 [ 670. , 0.222],

270 [ 675. , 0.231],

271 [ 680. , 0.242],

272 [ 685. , 0.251],

273 [ 690. , 0.261],

274 [ 695. , 0.271],

275 [ 700. , 0.282],

276 [ 705. , 0.294],

277 [ 710. , 0.305],

278 [ 715. , 0.318],

279 [ 720. , 0.334],

280 [ 725. , 0.354],

281 [ 730. , 0.372],

282 [ 735. , 0.392],

283 [ 740. , 0.409],

284 [ 745. , 0.42 ],

285 [ 750. , 0.436],

286 [ 755. , 0.45 ],

287 [ 760. , 0.462],

288 [ 765. , 0.465],

289 [ 770. , 0.448],

290 [ 775. , 0.432],

291 [ 780. , 0.421]],

292 SpragueInterpolator,

293 {},

294 Extrapolator,

295 {'method': 'Constant', 'left': None, 'right': None})

296 """

297

298 path = str(path)

299

300 data = read_spectral_data_from_csv_file(path, **kwargs)

301

302 fields = list(data.keys())

303 wavelength_field, sd_fields = fields[0], fields[1:]

304

305 return {

306 sd_field: SpectralDistribution(

307 data[sd_field], data[wavelength_field], name=sd_field

308 )

309 for sd_field in sd_fields

310 }

311

312

313 def write_sds_to_csv_file(

314 sds: Dict[str, SpectralDistribution], path: str | PathLike

315) -> bool:

316 """

317 Write spectral distributions to a CSV file.

318

319 Parameters

320 ----------

321 sds

322 Spectral distributions to write to the specified CSV file.

323 path

324 CSV file path.

325

326 Returns

327 -------

328 :class:`bool`

329 Definition success.

330

331 Raises

332 ------

333 ValueError

334 If the specified spectral distributions have different shapes.

335 """

336

337 path = str(path)

338

339 if len(sds) != 1:

340 shapes = [sd.shape for sd in sds.values()]

341 if not all(shape == shapes[0] for shape in shapes):

342 error = (

343 "Cannot write spectral distributions "

344 'with different shapes to "CSV" file!'

345 )

346

347 raise ValueError(error)

348

349 wavelengths = next(iter(sds.values())).wavelengths

350 with open(path, "w") as csv_file:

351 fields = sorted(sds.keys())

352 writer = csv.DictWriter(

353 csv_file,

354 delimiter=",",

355 fieldnames=["wavelength", *fields],

356 lineterminator="\n",

357 )

358

359 writer.writeheader()

360

361 for wavelength in wavelengths:

362 row = {"wavelength": wavelength}

363 row.update({field: sds[field][wavelength] for field in fields})

364 writer.writerow(row)

365

366 return True