gamsapi 52.5.0__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. gams/__init__.py +27 -0
  2. gams/_version.py +1 -0
  3. gams/connect/__init__.py +28 -0
  4. gams/connect/agents/__init__.py +24 -0
  5. gams/connect/agents/_excel/__init__.py +32 -0
  6. gams/connect/agents/_excel/excelagent.py +312 -0
  7. gams/connect/agents/_excel/workbook.py +155 -0
  8. gams/connect/agents/_sqlconnectors/__init__.py +42 -0
  9. gams/connect/agents/_sqlconnectors/_accesshandler.py +211 -0
  10. gams/connect/agents/_sqlconnectors/_databasehandler.py +250 -0
  11. gams/connect/agents/_sqlconnectors/_mysqlhandler.py +168 -0
  12. gams/connect/agents/_sqlconnectors/_postgreshandler.py +131 -0
  13. gams/connect/agents/_sqlconnectors/_pyodbchandler.py +112 -0
  14. gams/connect/agents/_sqlconnectors/_sqlalchemyhandler.py +74 -0
  15. gams/connect/agents/_sqlconnectors/_sqlitehandler.py +262 -0
  16. gams/connect/agents/_sqlconnectors/_sqlserverhandler.py +179 -0
  17. gams/connect/agents/concatenate.py +440 -0
  18. gams/connect/agents/connectagent.py +743 -0
  19. gams/connect/agents/csvreader.py +675 -0
  20. gams/connect/agents/csvwriter.py +151 -0
  21. gams/connect/agents/domainwriter.py +143 -0
  22. gams/connect/agents/excelreader.py +756 -0
  23. gams/connect/agents/excelwriter.py +467 -0
  24. gams/connect/agents/filter.py +223 -0
  25. gams/connect/agents/gamsreader.py +112 -0
  26. gams/connect/agents/gamswriter.py +239 -0
  27. gams/connect/agents/gdxreader.py +109 -0
  28. gams/connect/agents/gdxwriter.py +146 -0
  29. gams/connect/agents/labelmanipulator.py +303 -0
  30. gams/connect/agents/projection.py +539 -0
  31. gams/connect/agents/pythoncode.py +71 -0
  32. gams/connect/agents/rawcsvreader.py +248 -0
  33. gams/connect/agents/rawexcelreader.py +312 -0
  34. gams/connect/agents/schema/CSVReader.yaml +92 -0
  35. gams/connect/agents/schema/CSVWriter.yaml +44 -0
  36. gams/connect/agents/schema/Concatenate.yaml +52 -0
  37. gams/connect/agents/schema/DomainWriter.yaml +25 -0
  38. gams/connect/agents/schema/ExcelReader.yaml +121 -0
  39. gams/connect/agents/schema/ExcelWriter.yaml +78 -0
  40. gams/connect/agents/schema/Filter.yaml +74 -0
  41. gams/connect/agents/schema/GAMSReader.yaml +20 -0
  42. gams/connect/agents/schema/GAMSWriter.yaml +47 -0
  43. gams/connect/agents/schema/GDXReader.yaml +23 -0
  44. gams/connect/agents/schema/GDXWriter.yaml +32 -0
  45. gams/connect/agents/schema/LabelManipulator.yaml +99 -0
  46. gams/connect/agents/schema/Projection.yaml +24 -0
  47. gams/connect/agents/schema/PythonCode.yaml +6 -0
  48. gams/connect/agents/schema/RawCSVReader.yaml +34 -0
  49. gams/connect/agents/schema/RawExcelReader.yaml +42 -0
  50. gams/connect/agents/schema/SQLReader.yaml +75 -0
  51. gams/connect/agents/schema/SQLWriter.yaml +103 -0
  52. gams/connect/agents/sqlreader.py +301 -0
  53. gams/connect/agents/sqlwriter.py +276 -0
  54. gams/connect/connectdatabase.py +275 -0
  55. gams/connect/connectvalidator.py +93 -0
  56. gams/connect/errors.py +34 -0
  57. gams/control/__init__.py +136 -0
  58. gams/control/database.py +2231 -0
  59. gams/control/execution.py +1900 -0
  60. gams/control/options.py +2792 -0
  61. gams/control/workspace.py +1198 -0
  62. gams/core/__init__.py +24 -0
  63. gams/core/cfg/__init__.py +26 -0
  64. gams/core/cfg/_cfgmcc.cp312-win_amd64.pyd +0 -0
  65. gams/core/cfg/cfgmcc.py +519 -0
  66. gams/core/dct/__init__.py +26 -0
  67. gams/core/dct/_dctmcc.cp312-win_amd64.pyd +0 -0
  68. gams/core/dct/dctmcc.py +574 -0
  69. gams/core/embedded/__init__.py +26 -0
  70. gams/core/embedded/gamsemb.py +1024 -0
  71. gams/core/emp/__init__.py +24 -0
  72. gams/core/emp/emplexer.py +89 -0
  73. gams/core/emp/empyacc.py +281 -0
  74. gams/core/gdx/__init__.py +26 -0
  75. gams/core/gdx/_gdxcc.cp312-win_amd64.pyd +0 -0
  76. gams/core/gdx/gdxcc.py +866 -0
  77. gams/core/gev/__init__.py +26 -0
  78. gams/core/gev/_gevmcc.cp312-win_amd64.pyd +0 -0
  79. gams/core/gev/gevmcc.py +855 -0
  80. gams/core/gmd/__init__.py +26 -0
  81. gams/core/gmd/_gmdcc.cp312-win_amd64.pyd +0 -0
  82. gams/core/gmd/gmdcc.py +917 -0
  83. gams/core/gmo/__init__.py +26 -0
  84. gams/core/gmo/_gmomcc.cp312-win_amd64.pyd +0 -0
  85. gams/core/gmo/gmomcc.py +2046 -0
  86. gams/core/idx/__init__.py +26 -0
  87. gams/core/idx/_idxcc.cp312-win_amd64.pyd +0 -0
  88. gams/core/idx/idxcc.py +510 -0
  89. gams/core/numpy/__init__.py +29 -0
  90. gams/core/numpy/_gams2numpy.cp312-win_amd64.pyd +0 -0
  91. gams/core/numpy/gams2numpy.py +1048 -0
  92. gams/core/opt/__init__.py +26 -0
  93. gams/core/opt/_optcc.cp312-win_amd64.pyd +0 -0
  94. gams/core/opt/optcc.py +840 -0
  95. gams/engine/__init__.py +204 -0
  96. gams/engine/api/__init__.py +13 -0
  97. gams/engine/api/auth_api.py +7653 -0
  98. gams/engine/api/cleanup_api.py +751 -0
  99. gams/engine/api/default_api.py +887 -0
  100. gams/engine/api/hypercube_api.py +2629 -0
  101. gams/engine/api/jobs_api.py +5229 -0
  102. gams/engine/api/licenses_api.py +2220 -0
  103. gams/engine/api/namespaces_api.py +7783 -0
  104. gams/engine/api/usage_api.py +5627 -0
  105. gams/engine/api/users_api.py +5931 -0
  106. gams/engine/api_client.py +804 -0
  107. gams/engine/api_response.py +21 -0
  108. gams/engine/configuration.py +601 -0
  109. gams/engine/exceptions.py +216 -0
  110. gams/engine/models/__init__.py +86 -0
  111. gams/engine/models/bad_input.py +89 -0
  112. gams/engine/models/cleanable_job_result.py +104 -0
  113. gams/engine/models/cleanable_job_result_page.py +113 -0
  114. gams/engine/models/engine_license.py +107 -0
  115. gams/engine/models/files_not_found.py +93 -0
  116. gams/engine/models/forwarded_token_response.py +112 -0
  117. gams/engine/models/generic_key_value_pair.py +89 -0
  118. gams/engine/models/hypercube.py +160 -0
  119. gams/engine/models/hypercube_page.py +111 -0
  120. gams/engine/models/hypercube_summary.py +91 -0
  121. gams/engine/models/hypercube_token.py +97 -0
  122. gams/engine/models/identity_provider.py +107 -0
  123. gams/engine/models/identity_provider_ldap.py +121 -0
  124. gams/engine/models/identity_provider_oauth2.py +146 -0
  125. gams/engine/models/identity_provider_oauth2_scope.py +89 -0
  126. gams/engine/models/identity_provider_oauth2_with_secret.py +152 -0
  127. gams/engine/models/identity_provider_oidc.py +133 -0
  128. gams/engine/models/identity_provider_oidc_with_secret.py +143 -0
  129. gams/engine/models/inex.py +91 -0
  130. gams/engine/models/invitation.py +136 -0
  131. gams/engine/models/invitation_quota.py +106 -0
  132. gams/engine/models/invitation_token.py +87 -0
  133. gams/engine/models/job.py +165 -0
  134. gams/engine/models/job_no_text_entry.py +138 -0
  135. gams/engine/models/job_no_text_entry_page.py +111 -0
  136. gams/engine/models/license.py +91 -0
  137. gams/engine/models/log_piece.py +96 -0
  138. gams/engine/models/message.py +87 -0
  139. gams/engine/models/message_and_token.py +99 -0
  140. gams/engine/models/message_with_webhook_id.py +89 -0
  141. gams/engine/models/model_auth_token.py +87 -0
  142. gams/engine/models/model_configuration.py +125 -0
  143. gams/engine/models/model_default_instance.py +99 -0
  144. gams/engine/models/model_default_user_instance.py +98 -0
  145. gams/engine/models/model_hypercube_job.py +106 -0
  146. gams/engine/models/model_hypercube_usage.py +130 -0
  147. gams/engine/models/model_instance_info.py +116 -0
  148. gams/engine/models/model_instance_info_full.py +123 -0
  149. gams/engine/models/model_instance_pool_info.py +112 -0
  150. gams/engine/models/model_job_labels.py +179 -0
  151. gams/engine/models/model_job_usage.py +133 -0
  152. gams/engine/models/model_pool_usage.py +124 -0
  153. gams/engine/models/model_usage.py +115 -0
  154. gams/engine/models/model_user.py +96 -0
  155. gams/engine/models/model_userinstance_info.py +119 -0
  156. gams/engine/models/model_userinstancepool_info.py +95 -0
  157. gams/engine/models/model_version.py +91 -0
  158. gams/engine/models/models.py +120 -0
  159. gams/engine/models/namespace.py +104 -0
  160. gams/engine/models/namespace_quota.py +96 -0
  161. gams/engine/models/namespace_with_permission.py +96 -0
  162. gams/engine/models/not_found.py +91 -0
  163. gams/engine/models/password_policy.py +97 -0
  164. gams/engine/models/perm_and_username.py +89 -0
  165. gams/engine/models/quota.py +117 -0
  166. gams/engine/models/quota_exceeded.py +97 -0
  167. gams/engine/models/status_code_meaning.py +89 -0
  168. gams/engine/models/stream_entry.py +89 -0
  169. gams/engine/models/system_wide_license.py +92 -0
  170. gams/engine/models/text_entries.py +87 -0
  171. gams/engine/models/text_entry.py +101 -0
  172. gams/engine/models/time_span.py +95 -0
  173. gams/engine/models/time_span_pool_worker.py +99 -0
  174. gams/engine/models/token_forward_error.py +87 -0
  175. gams/engine/models/user.py +127 -0
  176. gams/engine/models/user_group_member.py +96 -0
  177. gams/engine/models/user_groups.py +108 -0
  178. gams/engine/models/vapid_info.py +87 -0
  179. gams/engine/models/webhook.py +138 -0
  180. gams/engine/models/webhook_parameterized_event.py +99 -0
  181. gams/engine/py.typed +0 -0
  182. gams/engine/rest.py +258 -0
  183. gams/magic/__init__.py +32 -0
  184. gams/magic/gams_magic.py +142 -0
  185. gams/magic/interactive.py +402 -0
  186. gams/tools/__init__.py +30 -0
  187. gams/tools/errors.py +34 -0
  188. gams/tools/toolcollection/__init__.py +24 -0
  189. gams/tools/toolcollection/alg/__init__.py +24 -0
  190. gams/tools/toolcollection/alg/rank.py +51 -0
  191. gams/tools/toolcollection/data/__init__.py +24 -0
  192. gams/tools/toolcollection/data/csvread.py +444 -0
  193. gams/tools/toolcollection/data/csvwrite.py +311 -0
  194. gams/tools/toolcollection/data/exceldump.py +47 -0
  195. gams/tools/toolcollection/data/sqlitewrite.py +276 -0
  196. gams/tools/toolcollection/gdxservice/__init__.py +24 -0
  197. gams/tools/toolcollection/gdxservice/gdxencoding.py +104 -0
  198. gams/tools/toolcollection/gdxservice/gdxrename.py +94 -0
  199. gams/tools/toolcollection/linalg/__init__.py +24 -0
  200. gams/tools/toolcollection/linalg/cholesky.py +57 -0
  201. gams/tools/toolcollection/linalg/eigenvalue.py +56 -0
  202. gams/tools/toolcollection/linalg/eigenvector.py +58 -0
  203. gams/tools/toolcollection/linalg/invert.py +55 -0
  204. gams/tools/toolcollection/linalg/ols.py +138 -0
  205. gams/tools/toolcollection/tooltemplate.py +321 -0
  206. gams/tools/toolcollection/win32/__init__.py +24 -0
  207. gams/tools/toolcollection/win32/excelmerge.py +93 -0
  208. gams/tools/toolcollection/win32/exceltalk.py +76 -0
  209. gams/tools/toolcollection/win32/msappavail.py +49 -0
  210. gams/tools/toolcollection/win32/shellexecute.py +54 -0
  211. gams/tools/tools.py +116 -0
  212. gams/transfer/__init__.py +35 -0
  213. gams/transfer/_abcs/__init__.py +37 -0
  214. gams/transfer/_abcs/container_abcs.py +433 -0
  215. gams/transfer/_internals/__init__.py +63 -0
  216. gams/transfer/_internals/algorithms.py +436 -0
  217. gams/transfer/_internals/casepreservingdict.py +124 -0
  218. gams/transfer/_internals/constants.py +270 -0
  219. gams/transfer/_internals/domainviolation.py +103 -0
  220. gams/transfer/_internals/specialvalues.py +172 -0
  221. gams/transfer/containers/__init__.py +26 -0
  222. gams/transfer/containers/_container.py +1794 -0
  223. gams/transfer/containers/_io/__init__.py +28 -0
  224. gams/transfer/containers/_io/containers.py +164 -0
  225. gams/transfer/containers/_io/gdx.py +1029 -0
  226. gams/transfer/containers/_io/gmd.py +872 -0
  227. gams/transfer/containers/_mixins/__init__.py +26 -0
  228. gams/transfer/containers/_mixins/ccc.py +1274 -0
  229. gams/transfer/syms/__init__.py +33 -0
  230. gams/transfer/syms/_methods/__init__.py +24 -0
  231. gams/transfer/syms/_methods/tables.py +120 -0
  232. gams/transfer/syms/_methods/toDict.py +115 -0
  233. gams/transfer/syms/_methods/toList.py +83 -0
  234. gams/transfer/syms/_methods/toValue.py +60 -0
  235. gams/transfer/syms/_mixins/__init__.py +32 -0
  236. gams/transfer/syms/_mixins/equals.py +626 -0
  237. gams/transfer/syms/_mixins/generateRecords.py +499 -0
  238. gams/transfer/syms/_mixins/pivot.py +313 -0
  239. gams/transfer/syms/_mixins/pve.py +627 -0
  240. gams/transfer/syms/_mixins/sa.py +27 -0
  241. gams/transfer/syms/_mixins/sapve.py +27 -0
  242. gams/transfer/syms/_mixins/saua.py +27 -0
  243. gams/transfer/syms/_mixins/sauapve.py +199 -0
  244. gams/transfer/syms/_mixins/spve.py +1528 -0
  245. gams/transfer/syms/_mixins/ve.py +936 -0
  246. gams/transfer/syms/container_syms/__init__.py +31 -0
  247. gams/transfer/syms/container_syms/_alias.py +984 -0
  248. gams/transfer/syms/container_syms/_equation.py +333 -0
  249. gams/transfer/syms/container_syms/_parameter.py +973 -0
  250. gams/transfer/syms/container_syms/_set.py +604 -0
  251. gams/transfer/syms/container_syms/_universe_alias.py +461 -0
  252. gams/transfer/syms/container_syms/_variable.py +321 -0
  253. gamsapi-52.5.0.dist-info/METADATA +150 -0
  254. gamsapi-52.5.0.dist-info/RECORD +257 -0
  255. gamsapi-52.5.0.dist-info/WHEEL +5 -0
  256. gamsapi-52.5.0.dist-info/licenses/LICENSE +22 -0
  257. gamsapi-52.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1528 @@
1
+ #
2
+ # GAMS - General Algebraic Modeling System Python API
3
+ #
4
+ # Copyright (c) 2017-2026 GAMS Development Corp. <support@gams.com>
5
+ # Copyright (c) 2017-2026 GAMS Software GmbH <support@gams.com>
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+ #
25
+
26
+ from warnings import warn
27
+ import pandas as pd
28
+ import numpy as np
29
+ from pandas.api.types import (
30
+ CategoricalDtype,
31
+ is_float_dtype,
32
+ infer_dtype,
33
+ )
34
+ import gams.transfer._abcs as abcs
35
+ from gams.transfer._internals import (
36
+ GAMS_MAX_INDEX_DIM,
37
+ GAMS_DESCRIPTION_MAX_LENGTH,
38
+ DomainStatus,
39
+ DomainViolation,
40
+ generate_unique_labels,
41
+ )
42
+ from typing import Optional, Union, List
43
+
44
+
45
+ class SPVEMixin:
46
+ def __delitem__(self):
47
+ # TODO: add in some functionality that might relax the symbols down to a different domain
48
+ # This function would mimic the <Container>.removeSymbols() method -- is more pythonic
49
+ del self.container.data[self.name]
50
+
51
+ @property
52
+ def domain_forwarding(self):
53
+ """A boolean indicating whether domain forwarding is enabled"""
54
+ return self._domain_forwarding
55
+
56
+ @domain_forwarding.setter
57
+ def domain_forwarding(self, domain_forwarding):
58
+ if not isinstance(domain_forwarding, (bool, list)):
59
+ raise TypeError("Argument 'domain_forwarding' must be type bool or list")
60
+
61
+ if isinstance(domain_forwarding, list):
62
+ if len(domain_forwarding) != self.dimension:
63
+ raise Exception(
64
+ "Argument 'domain_forwarding' must be of length <symbol>.dimension"
65
+ )
66
+
67
+ if any(not isinstance(i, bool) for i in domain_forwarding):
68
+ raise TypeError(
69
+ "Argument 'domain_forwarding' must only contain type bool"
70
+ )
71
+
72
+ self._domain_forwarding = domain_forwarding
73
+ self.modified = True
74
+ self.container.modified = True
75
+
76
+ @property
77
+ def domain_names(self):
78
+ """String version of domain names"""
79
+ return [
80
+ i.name if isinstance(i, abcs.AnyContainerDomainSymbol) else i
81
+ for i in self.domain
82
+ ]
83
+
84
+ @property
85
+ def domain_labels(self):
86
+ """The column headings for the records DataFrame"""
87
+ if self._records is not None:
88
+ return self._records.columns.tolist()[: self.dimension]
89
+
90
+ @domain_labels.setter
91
+ def domain_labels(self, labels):
92
+ if not isinstance(labels, list):
93
+ labels = [labels]
94
+
95
+ # checks
96
+ if len(labels) != self.dimension:
97
+ raise Exception(
98
+ "Attempting to set symbol 'domain_labels', however, len(domain_labels) != symbol dimension."
99
+ )
100
+
101
+ # make unique labels if necessary
102
+ labels = generate_unique_labels(labels)
103
+
104
+ # set the domain_labels
105
+ if getattr(self, "domain_labels", None) is not None:
106
+ if self._records.columns.tolist() != labels + self._attributes:
107
+ self._records.columns = labels + self._attributes
108
+ self._container._requires_state_check = True
109
+ self._requires_state_check = True
110
+ self.modified = True
111
+
112
+ @property
113
+ def domain(self):
114
+ """
115
+ List of domains given either as string (* for universe set) or as reference to the Set/Alias object
116
+ """
117
+ return self._domain
118
+
119
+ @domain.setter
120
+ def domain(self, domain):
121
+ if not isinstance(domain, list):
122
+ domain = [domain]
123
+
124
+ if not all(isinstance(i, (abcs.AnyContainerDomainSymbol, str)) for i in domain):
125
+ raise TypeError(
126
+ "All 'domain' elements must be type Set, Alias, UniverseAlias, or str"
127
+ )
128
+
129
+ if not all(
130
+ i.dimension == 1
131
+ for i in domain
132
+ if isinstance(i, abcs.AnyContainerDomainSymbol)
133
+ ):
134
+ raise ValueError("All linked 'domain' elements must have dimension == 1")
135
+
136
+ if len(domain) > GAMS_MAX_INDEX_DIM:
137
+ raise ValueError(f"Symbol 'domain' length cannot be > {GAMS_MAX_INDEX_DIM}")
138
+
139
+ # check to see if domain is being changed
140
+ if getattr(self, "domain", None) is not None:
141
+ if self.domain != domain:
142
+ self._requires_state_check = True
143
+ self.modified = True
144
+
145
+ self.container._requires_state_check = True
146
+ self.container.modified = True
147
+
148
+ self._domain = domain
149
+ else:
150
+ self._domain = domain
151
+
152
+ @property
153
+ def description(self):
154
+ """Description of the symbol"""
155
+ return self._description
156
+
157
+ @description.setter
158
+ def description(self, description):
159
+ if not isinstance(description, str):
160
+ raise TypeError("Symbol 'description' must be type str")
161
+
162
+ if len(description) > GAMS_DESCRIPTION_MAX_LENGTH:
163
+ raise TypeError(
164
+ f"Symbol 'description' must have "
165
+ f"length {GAMS_DESCRIPTION_MAX_LENGTH} or smaller"
166
+ )
167
+
168
+ # check to see if _description is being changed
169
+ if getattr(self, "description", None) is not None:
170
+ if self.description != description:
171
+ self._requires_state_check = True
172
+ self.modified = True
173
+
174
+ self.container._requires_state_check = True
175
+ self.container.modified = True
176
+
177
+ # set the description
178
+ self._description = description
179
+
180
+ @property
181
+ def dimension(self):
182
+ """The dimension of symbol"""
183
+ return len(self.domain)
184
+
185
+ @dimension.setter
186
+ def dimension(self, dimension):
187
+ if not isinstance(dimension, int) or dimension < 0:
188
+ raise TypeError(
189
+ "Symbol 'dimension' must be type int (greater than or equal to 0)"
190
+ )
191
+
192
+ if dimension > GAMS_MAX_INDEX_DIM:
193
+ raise ValueError(f"Symbol 'dimension' cannot be > {GAMS_MAX_INDEX_DIM}")
194
+
195
+ if len(self.domain) > dimension:
196
+ self.domain = [i for n, i in enumerate(self.domain) if n < dimension]
197
+ self.modified = True
198
+ elif dimension > len(self.domain):
199
+ new = self.domain
200
+ new.extend(["*"] * (dimension - len(self.domain)))
201
+ self.domain = new
202
+ self.modified = True
203
+ self.container.modified = True
204
+ else:
205
+ pass
206
+
207
+ @property
208
+ def records(self):
209
+ """Records of the symbol"""
210
+ return self._records
211
+
212
+ @records.setter
213
+ def records(self, records):
214
+ # set records
215
+ self._records = records
216
+
217
+ self._requires_state_check = True
218
+ self.modified = True
219
+
220
+ self.container._requires_state_check = True
221
+ self.container.modified = True
222
+
223
+ if self._records is not None:
224
+ if self.domain_forwarding:
225
+ self._domainForwarding()
226
+
227
+ # reset state check flags for all symbols in the container
228
+ for symnam, symobj in self.container.data.items():
229
+ symobj._requires_state_check = True
230
+
231
+ @property
232
+ def number_records(self):
233
+ """Number of records"""
234
+ if self.isValid():
235
+ if self.records is not None:
236
+ return len(self.records)
237
+ else:
238
+ return 0
239
+ else:
240
+ return float("nan")
241
+
242
+ def _getUELCodes(self, dimension, ignore_unused=False):
243
+ if not isinstance(dimension, int):
244
+ raise TypeError("Argument 'dimension' must be type int")
245
+
246
+ if dimension >= self.dimension:
247
+ raise ValueError(
248
+ f"Argument 'dimension' (`{dimension}`) must be < symbol "
249
+ f"dimension (`{self.dimension}`). (NOTE: 'dimension' is indexed from zero)"
250
+ )
251
+
252
+ if not isinstance(ignore_unused, bool):
253
+ raise TypeError("Argument 'ignore_unused' must be type bool")
254
+
255
+ cats = self.getUELs(dimension, ignore_unused=ignore_unused)
256
+ codes = list(range(len(cats)))
257
+ return dict(zip(cats, codes))
258
+
259
+ def getUELs(
260
+ self,
261
+ dimensions: Optional[Union[int, list]] = None,
262
+ codes: Optional[Union[int, list]] = None,
263
+ ignore_unused: bool = False,
264
+ unique_only: bool = False,
265
+ ) -> List[str]:
266
+ """
267
+ Gets UELs from symbol dimensions. If dimensions is None then get UELs from all dimensions (maintains order).
268
+ The argument codes accepts a list of str UELs and will return the corresponding int; must specify a single dimension if passing codes.
269
+
270
+ Parameters
271
+ ----------
272
+ dimensions : int | list, optional
273
+ Symbols' dimensions, by default None
274
+ codes : int | list, optional
275
+ Symbols' codes, by default None
276
+ ignore_unused : bool, optional
277
+ Flag to ignore unused UELs, by default False
278
+ unique_only : bool, optional
279
+ Flag to check only unique UELs, by default False
280
+
281
+ Returns
282
+ -------
283
+ List[str]
284
+ Only UELs in the data if ignore_unused=True, otherwise return all UELs.
285
+ """
286
+ if self.records is not None:
287
+ if not self.isValid():
288
+ raise Exception(
289
+ "Symbol is currently invalid -- must be valid in order to access UELs (categories)."
290
+ )
291
+
292
+ # fastpath for scalars
293
+ if self.dimension == 0:
294
+ return []
295
+
296
+ # ARG: ignore_unused
297
+ if not isinstance(ignore_unused, bool):
298
+ raise TypeError(f"Argument 'ignore_unused' must be type bool")
299
+
300
+ # ARG: unique_only
301
+ if not isinstance(unique_only, bool):
302
+ raise TypeError(f"Argument 'unique_only' must be type bool")
303
+
304
+ # ARG: dimension
305
+ if not isinstance(dimensions, (list, int, type(None))):
306
+ raise TypeError("Argument 'dimensions' must be type int or NoneType")
307
+
308
+ if dimensions is None:
309
+ dimensions = list(range(self.dimension))
310
+
311
+ if isinstance(dimensions, int):
312
+ dimensions = [dimensions]
313
+
314
+ if any(not isinstance(i, int) for i in dimensions):
315
+ raise TypeError("Argument 'dimensions' must only contain type int")
316
+
317
+ for n in dimensions:
318
+ if n >= self.dimension:
319
+ raise ValueError(
320
+ f"Cannot access symbol 'dimension' `{n}`, because `{n}` is >= symbol "
321
+ f"dimension (`{self.dimension}`). (NOTE: symbol 'dimension' is indexed from zero)"
322
+ )
323
+
324
+ # ARG: codes
325
+ if not isinstance(codes, (int, list, type(None))):
326
+ raise TypeError("Argument 'codes' must be type int, list, or NoneType")
327
+
328
+ if isinstance(codes, int):
329
+ codes = [codes]
330
+
331
+ if isinstance(codes, list):
332
+ if any(not isinstance(i, int) for i in codes):
333
+ raise TypeError("Argument 'codes' must only contain type int")
334
+
335
+ # ARG: codes & dimensions
336
+ if codes is not None and dimensions is None:
337
+ raise Exception(
338
+ "User must specify 'dimensions' if retrieving UELs with the 'codes' argument."
339
+ )
340
+
341
+ if codes is not None and len(dimensions) > 1:
342
+ raise Exception(
343
+ "User must specify only one dimension if retrieving UELs with the 'codes' argument"
344
+ )
345
+
346
+ if codes is None:
347
+ if len(dimensions) == 1:
348
+ n = dimensions[0]
349
+ if not ignore_unused:
350
+ cats = self.records.iloc[:, n].cat.categories.tolist()
351
+ else:
352
+ used_codes = np.sort(self.records.iloc[:, n].cat.codes.unique())
353
+ all_cats = self.records.iloc[:, n].cat.categories.tolist()
354
+ cats = [all_cats[i] for i in used_codes]
355
+ elif len(dimensions) > 1:
356
+ cats = {}
357
+ for n in dimensions:
358
+ if not ignore_unused:
359
+ cats.update(
360
+ dict.fromkeys(self.records.iloc[:, n].cat.categories)
361
+ )
362
+ else:
363
+ used_codes = np.sort(
364
+ self.records.iloc[:, n].cat.codes.unique()
365
+ )
366
+ all_cats = self.records.iloc[:, n].cat.categories.tolist()
367
+ cats.update(
368
+ dict.fromkeys([all_cats[i] for i in used_codes])
369
+ )
370
+
371
+ cats = list(cats.keys())
372
+
373
+ if unique_only:
374
+ return list(CasePreservingDict().fromkeys(cats.keys()).keys())
375
+ else:
376
+ return cats
377
+
378
+ else:
379
+ codemap = {
380
+ codes: cats
381
+ for cats, codes in self._getUELCodes(dimensions[0]).items()
382
+ }
383
+
384
+ if len(codes) == 1:
385
+ codes = codes[0]
386
+ return codemap[codes]
387
+
388
+ return [codemap.get(code, None) for code in codes]
389
+
390
+ def _formatUELs(self, method, dimensions=None):
391
+ if self.records is not None:
392
+ if not self.isValid():
393
+ raise Exception(
394
+ "Symbol is currently invalid -- must be valid in order to access UELs (categories)."
395
+ )
396
+
397
+ # ARG: dimension
398
+ if not isinstance(dimensions, (list, int, type(None))):
399
+ raise TypeError("Argument 'dimensions' must be type int or NoneType")
400
+
401
+ if dimensions is None:
402
+ dimensions = list(range(self.dimension))
403
+
404
+ if isinstance(dimensions, int):
405
+ dimensions = [dimensions]
406
+
407
+ if any(not isinstance(i, int) for i in dimensions):
408
+ raise TypeError("Argument 'dimensions' must only contain type int")
409
+
410
+ for n in dimensions:
411
+ if n >= self.dimension:
412
+ raise ValueError(
413
+ f"Cannot access symbol 'dimension' `{n}`, because `{n}` is >= symbol "
414
+ f"dimension (`{self.dimension}`). (NOTE: symbol 'dimension' is indexed from zero)"
415
+ )
416
+
417
+ for n in dimensions:
418
+ old_cats = self.records.iloc[:, n].cat.categories.tolist()
419
+ new_cats = list(map(method, self.records.iloc[:, n].cat.categories))
420
+
421
+ try:
422
+ # fastpath
423
+ self.records.isetitem(
424
+ n, self.records.iloc[:, n].cat.rename_categories(new_cats)
425
+ )
426
+ except:
427
+ # remap data (slow path)
428
+ self.records.isetitem(
429
+ n,
430
+ self.records.iloc[:, n]
431
+ .astype("object")
432
+ .map(dict(zip(old_cats, new_cats))),
433
+ )
434
+
435
+ # de-dup new cats
436
+ new_cats = list(dict.fromkeys(new_cats).keys())
437
+
438
+ # rebuild categorical
439
+ self.records.isetitem(
440
+ n,
441
+ self.records.iloc[:, n].astype(
442
+ pd.CategoricalDtype(categories=new_cats, ordered=True)
443
+ ),
444
+ )
445
+
446
+ def lowerUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
447
+ """
448
+ Will lowercase all UELs in the parent symbol or a subset of specified dimensions in the parent symbol, can be chain with other *UELs string operations
449
+
450
+ Parameters
451
+ ----------
452
+ dimensions : int | List[int], optional
453
+ Symbols' dimensions, by default None
454
+ """
455
+ self._formatUELs(str.lower, dimensions=dimensions)
456
+ return self
457
+
458
+ def upperUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
459
+ """
460
+ Will uppercase all UELs in the parent symbol or a subset of specified dimensions in the parent symbol, can be chain with other *UELs string operations
461
+
462
+ Parameters
463
+ ----------
464
+ dimensions : int | List[int], optional
465
+ Symbols' dimensions, by default None
466
+ """
467
+ self._formatUELs(str.upper, dimensions=dimensions)
468
+ return self
469
+
470
+ def lstripUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
471
+ """
472
+ Will left strip whitespace from all UELs in the parent set or a subset of specified dimensions in the parent set, can be chain with other *UELs string operations
473
+
474
+ Parameters
475
+ ----------
476
+ dimensions : int | List[int], optional
477
+ Symbols' dimensions, by default None
478
+ """
479
+ self._formatUELs(str.lstrip, dimensions=dimensions)
480
+ return self
481
+
482
+ def rstripUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
483
+ """
484
+ Will right strip whitespace from all UELs in the parent set or a subset of specified dimensions in the parent set, can be chain with other *UELs string operations
485
+
486
+ Parameters
487
+ ----------
488
+ dimensions : int | List[int], optional
489
+ Symbols' dimensions, by default None
490
+ """
491
+ self._formatUELs(str.rstrip, dimensions=dimensions)
492
+ return self
493
+
494
+ def stripUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
495
+ """
496
+ Will strip whitespace from all UELs in the parent set or a subset of specified dimensions in the parent set, can be chain with other *UELs string operations
497
+
498
+ Parameters
499
+ ----------
500
+ dimensions : int | List[int], optional
501
+ Symbols' dimensions, by default None
502
+ """
503
+ self._formatUELs(str.strip, dimensions=dimensions)
504
+ return self
505
+
506
+ def capitalizeUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
507
+ """
508
+ Will capitalize all UELs in the Container or a subset of specified symbols, can be chained with other *UELs string operations
509
+
510
+ Parameters
511
+ ----------
512
+ dimensions : int | List[int], optional
513
+ Symbols' dimensions, by default None
514
+ """
515
+ self._formatUELs(str.capitalize, dimensions=dimensions)
516
+ return self
517
+
518
+ def casefoldUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
519
+ """
520
+ Will casefold all UELs in the Container or a subset of specified symbols, can be chained with other *UELs string operations
521
+
522
+ Parameters
523
+ ----------
524
+ dimensions : int | List[int], optional
525
+ Symbols' dimensions, by default None
526
+ """
527
+ self._formatUELs(str.casefold, dimensions=dimensions)
528
+ return self
529
+
530
+ def titleUELs(self, dimensions: Optional[Union[int, List[int]]] = None):
531
+ """
532
+ Will title (capitalize all individual words) in all UELs in the Container or a subset of specified symbols, can be chained with other *UELs string operations
533
+
534
+ Parameters
535
+ ----------
536
+ dimensions : int | List[int], optional
537
+ Symbols' dimensions, by default None
538
+ """
539
+ self._formatUELs(str.title, dimensions=dimensions)
540
+ return self
541
+
542
+ def ljustUELs(
543
+ self,
544
+ length: int,
545
+ fill_character: Optional[str] = None,
546
+ dimensions: Optional[Union[int, List[int]]] = None,
547
+ ):
548
+ """
549
+ Will left justify all UELs in the symbol or a subset of specified dimensions, can be chained with other *UELs string operations
550
+
551
+ Parameters
552
+ ----------
553
+ length : int
554
+
555
+ fill_character : str, optional
556
+ Characters to fill in the empty, by default None
557
+ dimensions : int | List[int], optional
558
+ Symbols' dimensions, by default None
559
+ """
560
+ if fill_character is None:
561
+ fill_character = " "
562
+
563
+ if fill_character == " ":
564
+ warn(
565
+ "Trailing spaces are not significant in GAMS, they will be dropped "
566
+ "automatically if written to GAMS data structures (GDX/GMD). "
567
+ "They are maintained if using generating other data formats (CSV, SQL, etc.)"
568
+ )
569
+
570
+ try:
571
+ self._formatUELs(
572
+ lambda x: x.ljust(length, fill_character), dimensions=dimensions
573
+ )
574
+ except Exception as err:
575
+ raise Exception(
576
+ f"Could not successfully left justify (ljust) categories in `{self.name}`. Reason: {err}"
577
+ )
578
+
579
+ return self
580
+
581
+ def rjustUELs(
582
+ self,
583
+ length: int,
584
+ fill_character: Optional[str] = None,
585
+ dimensions: Optional[Union[int, List[int]]] = None,
586
+ ):
587
+ """
588
+ Will right justify all UELs in the symbol or a subset of specified dimensions, can be chained with other *UELs string operations
589
+
590
+ Parameters
591
+ ----------
592
+ length : int
593
+
594
+ fill_character : str, optional
595
+ Characters to fill in the empty, by default None
596
+ dimensions : int | List[int], optional
597
+ Symbols' dimensions, by default None
598
+ """
599
+ if fill_character is None:
600
+ fill_character = " "
601
+
602
+ try:
603
+ self._formatUELs(
604
+ lambda x: x.rjust(length, fill_character), dimensions=dimensions
605
+ )
606
+ except Exception as err:
607
+ raise Exception(
608
+ f"Could not successfully right justify (rjust) categories in `{self.name}`. Reason: {err}"
609
+ )
610
+
611
+ return self
612
+
613
+ def renameUELs(
614
+ self,
615
+ uels: Union[dict, list, str],
616
+ dimensions: Optional[Union[int, list]] = None,
617
+ allow_merge: bool = False,
618
+ ) -> None:
619
+ """
620
+ Renames UELs (case-sensitive) that appear in the symbol dimensions. If dimensions is None then operate on all dimensions of the symbol. ** All trailing whitespace is trimmed **
621
+
622
+ Parameters
623
+ ----------
624
+ uels : dict | list | str
625
+ List of UELs (case-sensitive) that appear in the symbol dimensions
626
+ dimensions : int | list, optional
627
+ Symbols' dimensions, by default None
628
+ allow_merge : bool, optional
629
+ If True, the categorical object will be re-created to offer additional data flexibility. By default False
630
+ """
631
+ if self.records is not None:
632
+ if not self.isValid():
633
+ raise Exception(
634
+ "Symbol is currently invalid -- must be valid in order to access UELs (categories)."
635
+ )
636
+
637
+ #
638
+ # ARG: uels
639
+ if not isinstance(uels, (dict, list, str)):
640
+ raise TypeError("Argument 'uels' must be type str, list, or dict")
641
+
642
+ if isinstance(uels, str):
643
+ uels = [uels]
644
+
645
+ if isinstance(uels, dict):
646
+ if any(
647
+ not isinstance(k, str) or not isinstance(v, str)
648
+ for k, v in uels.items()
649
+ ):
650
+ raise TypeError(
651
+ "Argument 'uels' dict must have both keys and values "
652
+ "that are type str (i.e., {'<old uel name>':'<new uel name>'})"
653
+ )
654
+
655
+ # trim all trailing whitespace
656
+ uels = {k: v.rstrip() for k, v in uels.items()}
657
+
658
+ if isinstance(uels, list):
659
+ if any(not isinstance(i, str) for i in uels):
660
+ raise TypeError("Argument 'uels' must contain only type str")
661
+
662
+ # trim all trailing whitespace
663
+ uels = list(map(str.rstrip, uels))
664
+
665
+ #
666
+ # ARG: dimensions
667
+ if not isinstance(dimensions, (int, list, type(None))):
668
+ raise TypeError(
669
+ "Argument 'dimensions' must be type int, list, or NoneType"
670
+ )
671
+
672
+ if dimensions is None:
673
+ dimensions = list(range(self.dimension))
674
+
675
+ if isinstance(dimensions, int):
676
+ dimensions = [dimensions]
677
+
678
+ if any(not isinstance(i, int) for i in dimensions):
679
+ raise TypeError("Argument 'dimensions' must only contain type int")
680
+
681
+ for i in dimensions:
682
+ if i >= self.dimension:
683
+ raise ValueError(
684
+ f"Cannot access symbol 'dimension' `{i}`, because `{i}` is >= symbol "
685
+ f"dimension (`{self.dimension}`). (NOTE: symbol 'dimension' is indexed from zero)"
686
+ )
687
+
688
+ #
689
+ # ARG: allow_merge
690
+ if not isinstance(allow_merge, bool):
691
+ raise TypeError("Argument 'allow_merge' must be type bool")
692
+
693
+ #
694
+ # check if uels has right length
695
+ if isinstance(uels, list):
696
+ for n in dimensions:
697
+ if len(uels) != len(self.records.iloc[:, n].cat.categories):
698
+ raise Exception(
699
+ f"Could not rename UELs (categories) in `{self.name}` dimension `{n}`. "
700
+ "Reason: new categories need to have the same "
701
+ "number of items as the old categories!"
702
+ )
703
+
704
+ for n in dimensions:
705
+ if allow_merge:
706
+ if isinstance(uels, list):
707
+ uel_map = dict(
708
+ zip(
709
+ self.records.iloc[:, n].cat.categories.tolist(),
710
+ uels,
711
+ )
712
+ )
713
+ else:
714
+ uel_map = uels
715
+
716
+ if any(
717
+ uel in self.records.iloc[:, n].cat.categories
718
+ for uel in uel_map.keys()
719
+ ):
720
+ is_ordered = self.records.iloc[:, n].dtype.ordered
721
+ old_uels = self.records.iloc[:, n].cat.categories.to_list()
722
+
723
+ # create and de-duplicate new_uels
724
+ new_uels = list(
725
+ dict.fromkeys(
726
+ [
727
+ uel_map[uel] if uel in uel_map.keys() else uel
728
+ for uel in old_uels
729
+ ]
730
+ )
731
+ )
732
+
733
+ # convert dimension back to object and do the string renaming
734
+
735
+ self.records.isetitem(
736
+ n,
737
+ self.records.iloc[:, n]
738
+ .astype("object")
739
+ .map(uel_map)
740
+ .fillna(self.records.iloc[:, n]),
741
+ )
742
+
743
+ # recreate the categorical
744
+ self.records.isetitem(
745
+ n,
746
+ self.records.iloc[:, n].astype(
747
+ CategoricalDtype(
748
+ categories=new_uels, ordered=is_ordered
749
+ )
750
+ ),
751
+ )
752
+
753
+ self.modified = True
754
+ self.container.modified = True
755
+
756
+ else:
757
+ try:
758
+ self.records.isetitem(
759
+ n, self.records.iloc[:, n].cat.rename_categories(uels)
760
+ )
761
+
762
+ self.modified = True
763
+ self.container.modified = True
764
+
765
+ except Exception as err:
766
+ raise Exception(
767
+ f"Could not rename UELs (categories) in `{self.name}` dimension `{n}`. Reason: {err}"
768
+ )
769
+
770
+ def removeUELs(
771
+ self,
772
+ uels: Optional[Union[dict, list, str]] = None,
773
+ dimensions: Optional[Union[int, list]] = None,
774
+ ) -> None:
775
+ """
776
+ Removes UELs that appear in the symbol dimensions, If uels is None then remove all unused UELs (categories). If dimensions is None then operate on all dimensions.
777
+
778
+ Parameters
779
+ ----------
780
+ uels : dict | list | str
781
+ List of UELs (case-sensitive) that appear in the symbol dimensions
782
+ dimensions : int | list, optional
783
+ Symbols' dimensions, by default None
784
+ """
785
+ if self.records is not None:
786
+ if not self.isValid():
787
+ raise Exception(
788
+ "Symbol is currently invalid -- must be valid in order to access UELs (categories)."
789
+ )
790
+
791
+ # ARG: uels
792
+ if not isinstance(uels, (list, str, type(None))):
793
+ raise TypeError("Argument 'uels' must be type list, str for NoneType")
794
+
795
+ if isinstance(uels, str):
796
+ uels = [uels]
797
+
798
+ if isinstance(uels, list):
799
+ if any(not isinstance(i, str) for i in uels):
800
+ raise TypeError("Argument 'uels' must contain only type str")
801
+
802
+ # ARG: dimensions
803
+ if not isinstance(dimensions, (int, list, type(None))):
804
+ raise TypeError(
805
+ "Argument 'dimensions' must be type int, list, or NoneType"
806
+ )
807
+
808
+ if dimensions is None:
809
+ dimensions = list(range(self.dimension))
810
+
811
+ if isinstance(dimensions, int):
812
+ dimensions = [dimensions]
813
+
814
+ if any(not isinstance(i, int) for i in dimensions):
815
+ raise TypeError("Argument 'dimensions' must only contain type int")
816
+
817
+ for i in dimensions:
818
+ if i >= self.dimension:
819
+ raise ValueError(
820
+ f"Cannot access symbol 'dimension' `{i}`, because `{i}` is >= symbol "
821
+ f"dimension (`{self.dimension}`). (NOTE: symbol 'dimension' is indexed from zero)"
822
+ )
823
+
824
+ # method body
825
+ if uels is None:
826
+ for n in dimensions:
827
+ try:
828
+ self.records.isetitem(
829
+ n, self.records.iloc[:, n].cat.remove_unused_categories()
830
+ )
831
+
832
+ self.modified = True
833
+ self.container.modified = True
834
+
835
+ except Exception as err:
836
+ raise Exception(
837
+ f"Could not remove unused UELs (categories) in symbol "
838
+ f"dimension `{n}`. Reason: {err}"
839
+ )
840
+ else:
841
+ for n in dimensions:
842
+ try:
843
+ self.records.isetitem(
844
+ n,
845
+ self.records.iloc[:, n].cat.remove_categories(
846
+ self.records.iloc[:, n].cat.categories.intersection(
847
+ set(uels)
848
+ )
849
+ ),
850
+ )
851
+
852
+ self.modified = True
853
+ self.container.modified = True
854
+
855
+ except Exception as err:
856
+ raise Exception(
857
+ f"Could not remove unused UELs (categories) in symbol "
858
+ f"dimension `{n}`. Reason: {err}"
859
+ )
860
+
861
+ def setUELs(
862
+ self,
863
+ uels: Union[str, List[str]],
864
+ dimensions: Optional[Union[int, list]] = None,
865
+ rename: bool = False,
866
+ ) -> None:
867
+ """
868
+ Set the UELs for symbol dimensions. If dimensions is None then set UELs for all dimensions. ** All trailing whitespace is trimmed **
869
+
870
+ Parameters
871
+ ----------
872
+ uels : str | List[str]
873
+ List of UELs (case-sensitive) that appear in the symbol dimensions
874
+ dimensions : int | list, optional
875
+ Symbols' dimensions, by default None
876
+ rename : bool, optional
877
+ If True, the old UEL names will be renamed with the new UEL names. By default False
878
+ """
879
+ if self.records is not None:
880
+ if not self.isValid():
881
+ raise Exception(
882
+ "Symbol is currently invalid -- must be valid in order to set UELs (categories)."
883
+ )
884
+
885
+ # ARG: uels
886
+ if not isinstance(uels, (str, list)):
887
+ raise TypeError("Argument 'uels' must be type list or str")
888
+
889
+ if isinstance(uels, str):
890
+ uels = [uels]
891
+
892
+ if any(not isinstance(uel, str) for uel in uels):
893
+ raise TypeError("Argument 'uels' must only contain type str")
894
+
895
+ # trim all trailing whitespace
896
+ uels = list(map(str.rstrip, uels))
897
+
898
+ # ARG: dimensions
899
+ if not isinstance(dimensions, (int, list, type(None))):
900
+ raise TypeError(
901
+ "Argument 'dimensions' must be type int, list, or NoneType"
902
+ )
903
+
904
+ if dimensions is None:
905
+ dimensions = list(range(self.dimension))
906
+
907
+ if isinstance(dimensions, int):
908
+ dimensions = [dimensions]
909
+
910
+ if any(not isinstance(i, int) for i in dimensions):
911
+ raise TypeError("Argument 'dimensions' must only contain type int")
912
+
913
+ for i in dimensions:
914
+ if i >= self.dimension:
915
+ raise ValueError(
916
+ f"Cannot access symbol 'dimension' `{i}`, because `{i}` is >= symbol "
917
+ f"dimension (`{self.dimension}`). (NOTE: symbol 'dimension' is indexed from zero)"
918
+ )
919
+
920
+ # ARG: rename
921
+ if not isinstance(rename, bool):
922
+ raise TypeError("Argument 'rename' must be type bool")
923
+
924
+ for n in dimensions:
925
+ try:
926
+ self.records.isetitem(
927
+ n,
928
+ self.records.iloc[:, n].cat.set_categories(
929
+ uels, ordered=True, rename=rename
930
+ ),
931
+ )
932
+
933
+ self.modified = True
934
+ self.container.modified = True
935
+
936
+ except Exception as err:
937
+ raise Exception(
938
+ f"Could not set UELs (categories) in symbol dimension `{n}`. Reason: {err}"
939
+ )
940
+
941
+ def reorderUELs(
942
+ self,
943
+ uels: Optional[Union[str, List[str]]] = None,
944
+ dimensions: Optional[Union[int, list]] = None,
945
+ ) -> None:
946
+ """
947
+ Reorders the UELs in the symbol dimensions. If uels is None, reorder UELs to data order and append any unused categories. If dimensions is None then reorder UELs in all dimensions of the symbol.
948
+
949
+ Parameters
950
+ ----------
951
+ uels : str | List[str], optional
952
+ List of UELs, by default None
953
+ dimensions : int | list, optional
954
+ Symbol dimensions, by default None
955
+ """
956
+ if self.records is not None:
957
+ if not self.isValid():
958
+ raise Exception(
959
+ "Symbol is currently invalid -- must be valid in order to reorder UELs (categories)."
960
+ )
961
+
962
+ # ARG: uels
963
+ if not isinstance(uels, (str, list, type(None))):
964
+ raise TypeError("Argument 'uels' must be type list, str, or NoneType")
965
+
966
+ if isinstance(uels, str):
967
+ uels = [uels]
968
+
969
+ if uels is not None:
970
+ if any(not isinstance(uel, str) for uel in uels):
971
+ raise TypeError("Argument 'uels' must only contain type str")
972
+
973
+ # ARG: dimensions
974
+ if not isinstance(dimensions, (int, list, type(None))):
975
+ raise TypeError(
976
+ "Argument 'dimensions' must be type int, list, or NoneType"
977
+ )
978
+
979
+ if dimensions is None:
980
+ dimensions = list(range(self.dimension))
981
+
982
+ if isinstance(dimensions, int):
983
+ dimensions = [dimensions]
984
+
985
+ if any(not isinstance(i, int) for i in dimensions):
986
+ raise TypeError("Argument 'dimensions' must only contain type int")
987
+
988
+ for i in dimensions:
989
+ if i >= self.dimension:
990
+ raise ValueError(
991
+ f"Cannot access symbol 'dimension' `{i}`, because `{i}` is >= symbol "
992
+ f"dimension (`{self.dimension}`). (NOTE: symbol 'dimension' is indexed from zero)"
993
+ )
994
+
995
+ if uels is not None:
996
+ for n in dimensions:
997
+ try:
998
+ # fastpath
999
+ self.records.isetitem(
1000
+ n,
1001
+ self.records.iloc[:, n].cat.reorder_categories(uels),
1002
+ )
1003
+
1004
+ except:
1005
+ # need to reset categories
1006
+ try:
1007
+ self.records.isetitem(n, self.setUELs(uels, dimensions=n))
1008
+ self.modified = True
1009
+ self.container.modified = True
1010
+
1011
+ except Exception as err:
1012
+ raise Exception(
1013
+ f"Could not reorder UELs (categories) in symbol dimension `{n}`. Reason: {err}"
1014
+ )
1015
+ else:
1016
+ for n in dimensions:
1017
+ old_cats = dict.fromkeys(self.records.iloc[:, n].cat.categories)
1018
+ new_cats = dict.fromkeys(self.records.iloc[:, n].unique())
1019
+
1020
+ uels = new_cats
1021
+ uels.update(dict.fromkeys(old_cats))
1022
+ uels = list(uels.keys())
1023
+
1024
+ try:
1025
+ # fastpath
1026
+ self.records.isetitem(
1027
+ n,
1028
+ self.records.iloc[:, n].cat.reorder_categories(uels),
1029
+ )
1030
+
1031
+ except:
1032
+ # need to reset categories
1033
+ try:
1034
+ self.records.isetitem(n, self.setUELs(uels, dimensions=n))
1035
+ self.modified = True
1036
+ self.container.modified = True
1037
+
1038
+ except Exception as err:
1039
+ raise Exception(
1040
+ f"Could not reorder UELs (categories) in symbol dimension `{n}`. Reason: {err}"
1041
+ )
1042
+
1043
+ def addUELs(
1044
+ self, uels: Union[str, List[str]], dimensions: Optional[Union[int, list]] = None
1045
+ ) -> None:
1046
+ """
1047
+ Adds UELs to the symbol dimensions. If dimensions is None then add UELs to all dimensions. ** All trailing whitespace is trimmed **
1048
+
1049
+ Parameters
1050
+ ----------
1051
+ uels : str | List[str]
1052
+ List of UELs
1053
+ dimensions : int | list, optional
1054
+ Symbol dimensions, by default None
1055
+ """
1056
+ if self.records is not None:
1057
+ if not self.isValid():
1058
+ raise Exception(
1059
+ "Symbol is currently invalid -- must be valid in order to access UELs (categories)."
1060
+ )
1061
+
1062
+ # ARG: uels
1063
+ if not isinstance(uels, (str, list)):
1064
+ raise TypeError("Argument 'uels' must be type list or str")
1065
+
1066
+ if isinstance(uels, str):
1067
+ uels = [uels]
1068
+
1069
+ if any(not isinstance(uel, str) for uel in uels):
1070
+ raise TypeError("Argument 'uels' must only contain type str")
1071
+
1072
+ # trim all trailing whitespace
1073
+ uels = list(map(str.rstrip, uels))
1074
+
1075
+ # ARG: dimensions
1076
+ if not isinstance(dimensions, (int, list, type(None))):
1077
+ raise TypeError(
1078
+ "Argument 'dimensions' must be type int, list, or NoneType"
1079
+ )
1080
+
1081
+ if dimensions is None:
1082
+ dimensions = list(range(self.dimension))
1083
+
1084
+ if isinstance(dimensions, int):
1085
+ dimensions = [dimensions]
1086
+
1087
+ if any(not isinstance(i, int) for i in dimensions):
1088
+ raise TypeError("Argument 'dimensions' must only contain type int")
1089
+
1090
+ for i in dimensions:
1091
+ if i >= self.dimension:
1092
+ raise ValueError(
1093
+ f"Cannot access symbol 'dimension' `{i}`, because `{i}` is >= symbol "
1094
+ f"dimension (`{self.dimension}`). (NOTE: symbol 'dimension' is indexed from zero)"
1095
+ )
1096
+
1097
+ for n in dimensions:
1098
+ try:
1099
+ self.records.isetitem(
1100
+ n, self.records.iloc[:, n].cat.add_categories(uels)
1101
+ )
1102
+
1103
+ self.modified = True
1104
+ self.container.modified = True
1105
+
1106
+ except Exception as err:
1107
+ raise Exception(
1108
+ f"Could not add UELs (categories) to symbol dimension `{n}`. Reason: {err}"
1109
+ )
1110
+
1111
+ def getDomainViolations(self) -> Optional[List["DomainViolation"]]:
1112
+ """
1113
+ Returns a list of DomainViolation objects if any (None otherwise)
1114
+
1115
+ Returns
1116
+ -------
1117
+ Optional[DomainViolation]
1118
+ List of DomainViolation objects if any (None otherwise)
1119
+ """
1120
+ if self.records is None:
1121
+ return None
1122
+
1123
+ else:
1124
+ dvobjs = []
1125
+ for n, symobj in enumerate(self.domain):
1126
+ if isinstance(symobj, abcs.AnyContainerDomainSymbol):
1127
+ if not symobj.isValid():
1128
+ raise Exception(
1129
+ f"Cannot locate domain violations for symbol `{self.name}` "
1130
+ f"because the referenced domain set `{symobj.name}` is not valid"
1131
+ )
1132
+
1133
+ self_elem = pd.Series(self.getUELs(n, ignore_unused=True))
1134
+
1135
+ # domain violations are generated for all elements if the domain set does not have records
1136
+ if symobj.records is not None:
1137
+ domain_elem = pd.Series(symobj.getUELs(ignore_unused=True))
1138
+ else:
1139
+ domain_elem = pd.Series([])
1140
+
1141
+ idx = ~self_elem.map(str.casefold).isin(
1142
+ domain_elem.map(str.casefold)
1143
+ )
1144
+
1145
+ if any(idx):
1146
+ dvobjs.append(
1147
+ DomainViolation(self, n, symobj, self_elem[idx].tolist())
1148
+ )
1149
+
1150
+ if len(dvobjs) != 0:
1151
+ return dvobjs
1152
+
1153
+ def findDomainViolations(self) -> Optional[pd.DataFrame]:
1154
+ """
1155
+ Get a view of the records DataFrame that contain any domain violations
1156
+
1157
+ Returns
1158
+ -------
1159
+ Optional[pd.DataFrame]
1160
+ Records DataFrame that contain any domain violations
1161
+ """
1162
+ if self.records is not None:
1163
+ violations = self.getDomainViolations()
1164
+
1165
+ if violations is not None:
1166
+ for n, v in enumerate(violations):
1167
+ set_v = set(v.violations)
1168
+ if n == 0:
1169
+ idx = self.records.iloc[:, v.dimension].isin(set_v)
1170
+ else:
1171
+ idx = (idx) | (self.records.iloc[:, v.dimension].isin(set_v))
1172
+
1173
+ return self.records.loc[idx, :]
1174
+ else:
1175
+ return self.records.loc[pd.Index([]), :]
1176
+
1177
+ def hasDomainViolations(self) -> bool:
1178
+ """
1179
+ Returns True if there are domain violations in the records, returns False if not.
1180
+
1181
+ Returns
1182
+ -------
1183
+ bool
1184
+ True if there are domain violations in the records, returns False if not.
1185
+ """
1186
+ if self.records is not None:
1187
+ return self.findDomainViolations().empty is False
1188
+ else:
1189
+ return 0
1190
+
1191
+ def countDomainViolations(self) -> int:
1192
+ """
1193
+ Returns the count of how many records contain at least one domain violation
1194
+
1195
+ Returns
1196
+ -------
1197
+ int
1198
+ Count of how many records contain at least one domain violation
1199
+ """
1200
+ if self.records is not None:
1201
+ return len(self.findDomainViolations())
1202
+ else:
1203
+ return 0
1204
+
1205
+ def dropDomainViolations(self):
1206
+ """
1207
+ drop records from the symbol that contain a domain violation
1208
+ """
1209
+ try:
1210
+ self.records.drop(index=self.findDomainViolations().index, inplace=True)
1211
+ except:
1212
+ return None
1213
+
1214
+ def countDuplicateRecords(self) -> int:
1215
+ """
1216
+ Returns the count of how many (case insensitive) duplicate records exist
1217
+
1218
+ Returns
1219
+ -------
1220
+ int
1221
+ Count of how many (case insensitive) duplicate records exist
1222
+ """
1223
+ try:
1224
+ return len(self.findDuplicateRecords())
1225
+ except:
1226
+ return 0
1227
+
1228
+ def findDuplicateRecords(self, keep: Union[str, bool] = "first") -> pd.DataFrame:
1229
+ """
1230
+ Get a view of the records DataFrame that contain any (case insensitive) duplicate domains – keep argument can take values of "first" (finds all duplicates while keeping the first instance as unique), "last" (finds all duplicates while keeping the last instance as unique), or False (finds all duplicates)
1231
+
1232
+ Parameters
1233
+ ----------
1234
+ keep : str | bool, optional
1235
+ Argument 'keep' must be either 'first' (returns duplicates except for the first occurrence), 'last' (returns duplicates except for the last occurrence), or False (returns all duplicates), by default "first"
1236
+
1237
+ Returns
1238
+ -------
1239
+ pd.DataFrame
1240
+ """
1241
+ if keep not in {"first", "last", False}:
1242
+ raise ValueError(
1243
+ "Argument 'keep' must be either 'first' "
1244
+ "(returns duplicates except for the first occurrence), "
1245
+ "'last' (returns duplicates except for the last occurrence), "
1246
+ "or False (returns all duplicates)"
1247
+ )
1248
+
1249
+ # create a temporary copy
1250
+ df2 = self.records.copy()
1251
+
1252
+ # casefold all domains
1253
+ for i in range(self.dimension):
1254
+ df2.isetitem(i, df2.iloc[:, i].map(str.casefold))
1255
+
1256
+ idx = df2.duplicated(subset=df2.columns[: self.dimension], keep=keep)
1257
+
1258
+ return self.records.loc[idx, :]
1259
+
1260
+ def hasDuplicateRecords(self) -> bool:
1261
+ """
1262
+ Returns True if there are (case insensitive) duplicate records in the symbol, returns False if not
1263
+
1264
+ Returns
1265
+ -------
1266
+ bool
1267
+ True if there are (case insensitive) duplicate records in the symbol, returns False if not
1268
+ """
1269
+ return self.countDuplicateRecords() != 0
1270
+
1271
+ def dropDuplicateRecords(self, keep: Union[str, bool] = "first") -> None:
1272
+ """
1273
+ Drop records with (case insensitive) duplicate domains from the symbol
1274
+
1275
+ Parameters
1276
+ ----------
1277
+ keep : str | bool, optional
1278
+ keep argument can take values of "first" (keeps the first instance of a duplicate record), "last" (keeps the last instance of a record), or False (drops all duplicates including the first and last), by default "first"
1279
+ """
1280
+ try:
1281
+ self.records.drop(index=self.findDuplicateRecords(keep).index, inplace=True)
1282
+ except:
1283
+ return None
1284
+
1285
+ @property
1286
+ def domain_type(self):
1287
+ """State of the domain links"""
1288
+ return self._domain_status.name
1289
+
1290
+ @property
1291
+ def _domain_status(self):
1292
+ if (
1293
+ all(isinstance(i, abcs.AnyContainerDomainSymbol) for i in self.domain)
1294
+ and self.dimension != 0
1295
+ ):
1296
+ return DomainStatus.regular
1297
+ elif all(i == "*" for i in self.domain):
1298
+ return DomainStatus.none
1299
+ elif self.dimension == 0:
1300
+ return DomainStatus.none
1301
+ else:
1302
+ return DomainStatus.relaxed
1303
+
1304
+ def _domainForwarding(self):
1305
+ if isinstance(self.container, abcs.ABCContainer):
1306
+ if isinstance(self.domain_forwarding, bool):
1307
+ forwarding = [self.domain_forwarding] * self.dimension
1308
+ else:
1309
+ forwarding = self.domain_forwarding
1310
+
1311
+ for n, (dl, d) in enumerate(zip(self.domain_labels, self.domain)):
1312
+ if forwarding[n]:
1313
+ # find set names to grow (bottom to top)
1314
+ to_grow = []
1315
+ while isinstance(d, abcs.ABCSet):
1316
+ to_grow.append(d.name)
1317
+ d = d.domain[0]
1318
+
1319
+ # grow the sets (top to bottom)
1320
+ to_grow.reverse()
1321
+ for i in to_grow:
1322
+ if self.container[i].records is not None:
1323
+ recs = self.container[i].records
1324
+
1325
+ assert (
1326
+ self.container[i].dimension == 1
1327
+ ), "attempting to forward a domain set that has dimension >1"
1328
+
1329
+ # convert all categoricals back to str to enable concat
1330
+ recs.isetitem(0, recs.iloc[:, 0].astype(str))
1331
+
1332
+ df = pd.DataFrame(self.records[dl])
1333
+ df = df.assign(element_text="")
1334
+ df.columns = recs.columns
1335
+
1336
+ recs = pd.concat([recs, df], ignore_index=True)
1337
+
1338
+ # clean up any non-unique set elements, should they exist
1339
+ recs.drop_duplicates(
1340
+ subset=recs.columns[0],
1341
+ keep="first",
1342
+ inplace=True,
1343
+ ignore_index=True,
1344
+ )
1345
+
1346
+ # convert object back to categorical
1347
+ recs.isetitem(
1348
+ 0,
1349
+ recs.iloc[:, 0].astype(
1350
+ CategoricalDtype(
1351
+ recs.iloc[:, 0].unique(), ordered=True
1352
+ )
1353
+ ),
1354
+ )
1355
+ else:
1356
+ recs = pd.DataFrame(self.records[dl].unique())
1357
+ recs = recs.assign(element_text="")
1358
+
1359
+ # convert object back to unlinked categorical
1360
+ recs.isetitem(
1361
+ 0,
1362
+ recs.iloc[:, 0].astype(
1363
+ CategoricalDtype(
1364
+ recs.iloc[:, 0].unique(), ordered=True
1365
+ )
1366
+ ),
1367
+ )
1368
+
1369
+ # set records
1370
+ self.container[i].records = recs
1371
+ self.container[i].domain_labels = self.container[i].domain_names
1372
+ self.container[i].modified = True
1373
+
1374
+ def _assert_valid_records(self):
1375
+ if self.records is not None:
1376
+ # make sure and all domains have valid categories
1377
+ for i in range(self.dimension):
1378
+ if np.any(self.records.iloc[:, i].cat.codes.to_numpy() == -1):
1379
+ raise Exception(
1380
+ f"Categories are missing from the data in symbol `{self.name}` (dimension {i}) -- "
1381
+ "has resulted in `NaN` domains labels. "
1382
+ "Cannot write symbol until domain labels have been been restored."
1383
+ )
1384
+
1385
+ def _assert_is_valid(self):
1386
+ if self._requires_state_check:
1387
+ # check if symbol has a container
1388
+ if self.container is None:
1389
+ raise Exception(
1390
+ "Symbol is not currently linked to a container, "
1391
+ "must add it to a container in order to be valid"
1392
+ )
1393
+
1394
+ # check domain symbols
1395
+ self_nrecs = 0 if self.records is None else len(self.records)
1396
+ if self.domain_type == "regular" and self_nrecs > 0:
1397
+ for sym in self.domain:
1398
+ if not isinstance(sym, abcs.ABCUniverseAlias):
1399
+ dom_nrecs = 0 if sym.records is None else len(sym.records)
1400
+ if dom_nrecs == 0:
1401
+ raise Exception(
1402
+ f"Symbol has 'regular' domain type, but domain set ('{sym.name}') does not have any records (i.e., domain violation(s) exist)."
1403
+ )
1404
+
1405
+ for i in self.domain:
1406
+ if isinstance(i, abcs.AnyContainerDomainSymbol):
1407
+ # must have valid links
1408
+ if hex(id(i)) not in [
1409
+ hex(id(v)) for _, v in self.container.data.items()
1410
+ ]:
1411
+ raise Exception(
1412
+ f"Symbol defined over domain symbol `{i.name}`, "
1413
+ "however the object reference "
1414
+ f"'{hex(id(i))}' is not in the Container anymore "
1415
+ f"-- must reset domain for symbol '{self.name}'."
1416
+ )
1417
+
1418
+ # must be valid symbols
1419
+ if not i.isValid():
1420
+ raise Exception(
1421
+ f"Symbol defined over domain symbol `{i.name}`, "
1422
+ "however this object is not a valid object in the Container"
1423
+ " -- all domain objects must be valid."
1424
+ )
1425
+
1426
+ # if records exist do some checks
1427
+ if self.records is not None:
1428
+ # check if records are a DataFrame
1429
+ if not isinstance(self.records, pd.DataFrame):
1430
+ raise Exception("Symbol 'records' must be type pandas.DataFrame")
1431
+
1432
+ # check if self.records has the correct number of columns and/or rows
1433
+ r, c = self.records.shape
1434
+ if not c == self.dimension + len(self._attributes):
1435
+ raise ValueError(
1436
+ "Symbol 'records' does not have the correct "
1437
+ " number of columns (<symbol dimension> + 1)"
1438
+ )
1439
+
1440
+ if self.dimension == 0:
1441
+ if r > 1:
1442
+ raise ValueError(
1443
+ "Symbol 'records' can only have 1 row because "
1444
+ f"it has been defined to be a scalar (currently has {r} rows)"
1445
+ )
1446
+
1447
+ # check that all domain_labels are unique
1448
+ if len(self.domain_labels) != len(set(self.domain_labels)):
1449
+ raise Exception(
1450
+ "Domain columns do not have unique names. "
1451
+ "Reset domain column names by setting the `<symbol>.domain_labels` property."
1452
+ )
1453
+
1454
+ # check that all value columns have the same name as _attributes
1455
+ if self.records.columns[self.dimension :].tolist() != self._attributes:
1456
+ raise Exception(
1457
+ f"Value columns in 'records' must be named and ordered as: {self._attributes}. "
1458
+ f"Currently named: {self.records.columns[self.dimension:]}"
1459
+ )
1460
+
1461
+ # check if domain columns are categorical dtype
1462
+ for i in self.domain_labels:
1463
+ if not isinstance(self.records[i].dtype, CategoricalDtype):
1464
+ raise Exception(
1465
+ f"Domain information in column `{i}` for "
1466
+ " 'records' must be categorical type"
1467
+ )
1468
+
1469
+ # check if domain categories are all type str
1470
+ for i in self.domain_labels:
1471
+ typ = infer_dtype(self.records[i].cat.categories)
1472
+ if typ != "empty":
1473
+ if typ != "string":
1474
+ raise TypeError(
1475
+ f"Domain column `{i}` in 'records' contains non-str "
1476
+ "category, all domain categories must be type str."
1477
+ )
1478
+
1479
+ # check if set element_text columns are type str
1480
+ if isinstance(self, abcs.ABCSet):
1481
+ typ = infer_dtype(self.records["element_text"])
1482
+ if typ != "empty":
1483
+ if typ != "string":
1484
+ raise TypeError(
1485
+ "Records 'element_text' column must contain only str type"
1486
+ )
1487
+
1488
+ # check if all data_columns are type float
1489
+ if isinstance(
1490
+ self, (abcs.ABCParameter, abcs.ABCVariable, abcs.ABCEquation)
1491
+ ):
1492
+ for i in self.records.columns[self.dimension :]:
1493
+ if infer_dtype(self.records[i]) != "empty":
1494
+ if not is_float_dtype(self.records[i]):
1495
+ raise Exception(
1496
+ f"Data in column `{i}` for 'records' must be float type"
1497
+ )
1498
+
1499
+ # if no exceptions, then turn self._requires_state_check 'off'
1500
+ self._requires_state_check = False
1501
+
1502
+ def getSparsity(self) -> float:
1503
+ """
1504
+ Get the sparsity of the symbol w.r.t the cardinality
1505
+
1506
+ Returns
1507
+ -------
1508
+ float
1509
+ Sparsity of the symbol w.r.t the cardinality
1510
+ """
1511
+ if self.domain_type in {"relaxed", "none"}:
1512
+ return float("nan")
1513
+ elif self.domain_type == "regular":
1514
+ if not self.isValid():
1515
+ raise Exception(
1516
+ f"Cannot calculate getSparsity because `{self.name}` is not a valid symbol object"
1517
+ "Use `<symbol>.isValid(verbose=True)` to debug further."
1518
+ )
1519
+
1520
+ # if there are any domain symbols that do not have records
1521
+ if any(not n.number_records for n in self.domain):
1522
+ return float("nan")
1523
+ else:
1524
+ dense = 1
1525
+ for i in [n.number_records for n in self.domain]:
1526
+ dense *= i
1527
+
1528
+ return 1 - self.number_records / dense