ommx-openjij-adapter 2.0.0b3__tar.gz → 2.0.0rc2__tar.gz
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.
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/PKG-INFO +3 -3
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/ommx_openjij_adapter/__init__.py +137 -41
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/ommx_openjij_adapter.egg-info/PKG-INFO +3 -3
- ommx_openjij_adapter-2.0.0rc2/ommx_openjij_adapter.egg-info/requires.txt +2 -0
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/pyproject.toml +2 -2
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/tests/test_sample.py +76 -2
- ommx_openjij_adapter-2.0.0b3/ommx_openjij_adapter.egg-info/requires.txt +0 -2
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/README.md +0 -0
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/ommx_openjij_adapter.egg-info/SOURCES.txt +0 -0
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/ommx_openjij_adapter.egg-info/dependency_links.txt +0 -0
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/ommx_openjij_adapter.egg-info/top_level.txt +0 -0
- {ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ommx_openjij_adapter
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.0rc2
|
4
4
|
Summary: OMMX Adapter for OpenJij.
|
5
5
|
Author-email: "Jij Inc." <info@j-ij.com>
|
6
6
|
Project-URL: Repository, https://github.com/Jij-Inc/ommx
|
@@ -14,8 +14,8 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
15
15
|
Requires-Python: <3.13,>=3.9
|
16
16
|
Description-Content-Type: text/markdown
|
17
|
-
Requires-Dist: ommx<3.0.0,>=2.0.
|
18
|
-
Requires-Dist: openjij
|
17
|
+
Requires-Dist: ommx<3.0.0,>=2.0.0rc1
|
18
|
+
Requires-Dist: openjij<0.10.0,>=0.9.2
|
19
19
|
|
20
20
|
OMMX Adapter for OpenJij
|
21
21
|
=========================
|
{ommx_openjij_adapter-2.0.0b3 → ommx_openjij_adapter-2.0.0rc2}/ommx_openjij_adapter/__init__.py
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from ommx.v1 import Instance, State, Samples, SampleSet
|
3
|
+
from ommx.v1 import Instance, State, Samples, SampleSet, DecisionVariable, Constraint
|
4
4
|
from ommx.adapter import SamplerAdapter
|
5
5
|
import openjij as oj
|
6
6
|
from typing_extensions import deprecated
|
@@ -10,21 +10,21 @@ import copy
|
|
10
10
|
|
11
11
|
class OMMXOpenJijSAAdapter(SamplerAdapter):
|
12
12
|
"""
|
13
|
-
Sampling QUBO with Simulated Annealing (SA) by `openjij.SASampler <https://openjij.github.io/OpenJij/reference/openjij/index.html#openjij.SASampler>`_
|
13
|
+
Sampling QUBO or HUBO with Simulated Annealing (SA) by `openjij.SASampler <https://openjij.github.io/OpenJij/reference/openjij/index.html#openjij.SASampler>`_
|
14
14
|
"""
|
15
15
|
|
16
16
|
ommx_instance: Instance
|
17
17
|
"""
|
18
|
-
ommx.v1.Instance representing a QUBO problem
|
18
|
+
ommx.v1.Instance representing a QUBO or HUBO problem
|
19
19
|
|
20
|
-
The input `instance` must be a QUBO (Quadratic Unconstrained Binary Optimization) problem, i.e.
|
20
|
+
The input `instance` must be a QUBO (Quadratic Unconstrained Binary Optimization) or HUBO (Higher-order Unconstrained Binary Optimization) problem, i.e.
|
21
21
|
|
22
|
-
-
|
23
|
-
- No
|
24
|
-
- Objective function is quadratic
|
22
|
+
- All decision variables are binary
|
23
|
+
- No constraints
|
24
|
+
- Objective function is quadratic (QUBO) or higher (HUBO).
|
25
25
|
- Minimization problem
|
26
26
|
|
27
|
-
You can convert an instance to QUBO via :meth:`ommx.v1.Instance.penalty_method` or other corresponding method.
|
27
|
+
You can convert an instance to QUBO or HUBO via :meth:`ommx.v1.Instance.penalty_method` or other corresponding method.
|
28
28
|
"""
|
29
29
|
|
30
30
|
beta_min: float | None = None
|
@@ -36,24 +36,26 @@ class OMMXOpenJijSAAdapter(SamplerAdapter):
|
|
36
36
|
num_reads: int | None = None
|
37
37
|
""" number of reads """
|
38
38
|
schedule: list | None = None
|
39
|
-
""" list of inverse temperature """
|
39
|
+
""" list of inverse temperature (parameter only used if problem is QUBO)"""
|
40
40
|
initial_state: list | dict | None = None
|
41
|
-
""" initial state """
|
41
|
+
""" initial state (parameter only used if problem is QUBO)"""
|
42
42
|
updater: str | None = None
|
43
43
|
""" updater algorithm """
|
44
44
|
sparse: bool | None = None
|
45
|
-
""" use sparse matrix or not """
|
45
|
+
""" use sparse matrix or not (parameter only used if problem is QUBO)"""
|
46
46
|
reinitialize_state: bool | None = None
|
47
|
-
""" if true reinitialize state for each run """
|
47
|
+
""" if true reinitialize state for each run (parameter only used if problem is QUBO)"""
|
48
48
|
seed: int | None = None
|
49
49
|
""" seed for Monte Carlo algorithm """
|
50
50
|
|
51
51
|
uniform_penalty_weight: Optional[float] = None
|
52
|
-
""" Weight for uniform penalty, passed to ``Instance.to_qubo`` """
|
52
|
+
""" Weight for uniform penalty, passed to ``Instance.to_qubo`` or ``Instance.to_hubo`` """
|
53
53
|
penalty_weights: dict[int, float] = {}
|
54
|
-
""" Penalty weights for each constraint, passed to ``Instance.to_qubo`` """
|
54
|
+
""" Penalty weights for each constraint, passed to ``Instance.to_qubo`` or ``Instance.to_hubo`` """
|
55
55
|
inequality_integer_slack_max_range: int = 32
|
56
|
-
""" Max range for integer slack variables in inequality constraints, passed to ``Instance.to_qubo`` """
|
56
|
+
""" Max range for integer slack variables in inequality constraints, passed to ``Instance.to_qubo`` or ``Instance.to_hubo`` """
|
57
|
+
|
58
|
+
_instance_prepared: bool = False
|
57
59
|
|
58
60
|
@classmethod
|
59
61
|
def sample(
|
@@ -139,30 +141,123 @@ class OMMXOpenJijSAAdapter(SamplerAdapter):
|
|
139
141
|
return decode_to_samples(data)
|
140
142
|
|
141
143
|
@property
|
142
|
-
def sampler_input(self) -> dict[tuple[int,
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
return qubo
|
144
|
+
def sampler_input(self) -> dict[tuple[int, ...], float]:
|
145
|
+
self._prepare_convert()
|
146
|
+
if self._is_hubo:
|
147
|
+
return self._hubo
|
148
|
+
else:
|
149
|
+
return self._qubo
|
149
150
|
|
150
151
|
def _sample(self) -> oj.Response:
|
151
152
|
sampler = oj.SASampler()
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
153
|
+
input = self.sampler_input
|
154
|
+
if self._is_hubo:
|
155
|
+
return sampler.sample_hubo(
|
156
|
+
input, # type: ignore
|
157
|
+
vartype="BINARY",
|
158
|
+
beta_min=self.beta_min,
|
159
|
+
beta_max=self.beta_max,
|
160
|
+
# maintaining default parameters in openjij impl if None passed
|
161
|
+
num_sweeps=self.num_sweeps or 1000,
|
162
|
+
num_reads=self.num_reads or 1,
|
163
|
+
updater=self.updater or "METROPOLIS",
|
164
|
+
seed=self.seed,
|
165
|
+
)
|
166
|
+
|
167
|
+
else:
|
168
|
+
return sampler.sample_qubo(
|
169
|
+
input, # type: ignore
|
170
|
+
beta_min=self.beta_min,
|
171
|
+
beta_max=self.beta_max,
|
172
|
+
num_sweeps=self.num_sweeps,
|
173
|
+
num_reads=self.num_reads,
|
174
|
+
schedule=self.schedule,
|
175
|
+
initial_state=self.initial_state,
|
176
|
+
updater=self.updater,
|
177
|
+
sparse=self.sparse,
|
178
|
+
reinitialize_state=self.reinitialize_state,
|
179
|
+
seed=self.seed,
|
180
|
+
)
|
181
|
+
|
182
|
+
# Manually perform the conversion process to QUBO/HUBO, instead of using
|
183
|
+
# `to_hubo` or `to_qubo`.
|
184
|
+
#
|
185
|
+
# This is so that we can manually call `as_hubo_format()`, check if the
|
186
|
+
# finalized instance is higher-order, and if not, call
|
187
|
+
# `as_qubo_format()`.
|
188
|
+
#
|
189
|
+
# We could do alternative methods like simply checking the degrees of
|
190
|
+
# the objective function and all constraints. But some instances will
|
191
|
+
# see to be higher-order but ultimately representable as QUBO after the
|
192
|
+
# conversion (eg. due to simplifying binary `x * x`). So we chose to do
|
193
|
+
# it this way.
|
194
|
+
def _prepare_convert(self):
|
195
|
+
if self._instance_prepared:
|
196
|
+
return
|
197
|
+
|
198
|
+
self.ommx_instance.as_minimization_problem()
|
199
|
+
|
200
|
+
continuous_variables = [
|
201
|
+
var.id
|
202
|
+
for var in self.ommx_instance.decision_variables
|
203
|
+
if var.kind == DecisionVariable.CONTINUOUS
|
204
|
+
]
|
205
|
+
if len(continuous_variables) > 0:
|
206
|
+
raise ValueError(
|
207
|
+
f"Continuous variables are not supported in HUBO conversion: IDs={continuous_variables}"
|
208
|
+
)
|
209
|
+
|
210
|
+
# Prepare inequality constraints
|
211
|
+
ineq_ids = [
|
212
|
+
c.id
|
213
|
+
for c in self.ommx_instance.constraints
|
214
|
+
if c.equality == Constraint.LESS_THAN_OR_EQUAL_TO_ZERO
|
215
|
+
]
|
216
|
+
for ineq_id in ineq_ids:
|
217
|
+
try:
|
218
|
+
self.ommx_instance.convert_inequality_to_equality_with_integer_slack(
|
219
|
+
ineq_id, self.inequality_integer_slack_max_range
|
220
|
+
)
|
221
|
+
except RuntimeError:
|
222
|
+
self.ommx_instance.add_integer_slack_to_inequality(
|
223
|
+
ineq_id, self.inequality_integer_slack_max_range
|
224
|
+
)
|
225
|
+
|
226
|
+
# Penalty method
|
227
|
+
if self.ommx_instance.constraints:
|
228
|
+
if self.uniform_penalty_weight is not None and self.penalty_weights:
|
229
|
+
raise ValueError(
|
230
|
+
"Both uniform_penalty_weight and penalty_weights are specified. Please choose one."
|
231
|
+
)
|
232
|
+
if self.penalty_weights:
|
233
|
+
pi = self.ommx_instance.penalty_method()
|
234
|
+
weights = {
|
235
|
+
p.id: self.penalty_weights[p.subscripts[0]] for p in pi.parameters
|
236
|
+
}
|
237
|
+
unconstrained = pi.with_parameters(weights)
|
238
|
+
else:
|
239
|
+
if self.uniform_penalty_weight is None:
|
240
|
+
# If both are None, defaults to uniform_penalty_weight = 1.0
|
241
|
+
self.uniform_penalty_weight = 1.0
|
242
|
+
pi = self.ommx_instance.uniform_penalty_method()
|
243
|
+
weight = pi.parameters[0]
|
244
|
+
unconstrained = pi.with_parameters(
|
245
|
+
{weight.id: self.uniform_penalty_weight}
|
246
|
+
)
|
247
|
+
self.ommx_instance.raw = unconstrained.raw
|
248
|
+
|
249
|
+
self.ommx_instance.log_encode()
|
250
|
+
|
251
|
+
hubo, _ = self.ommx_instance.as_hubo_format()
|
252
|
+
if any(len(k) > 2 for k in hubo.keys()):
|
253
|
+
self._is_hubo = True
|
254
|
+
self._hubo = hubo
|
255
|
+
else:
|
256
|
+
self._is_hubo = False
|
257
|
+
qubo, _ = self.ommx_instance.as_qubo_format()
|
258
|
+
self._qubo = qubo
|
259
|
+
|
260
|
+
self._instance_prepared = True
|
166
261
|
|
167
262
|
|
168
263
|
@deprecated("Renamed to `decode_to_samples`")
|
@@ -177,23 +272,24 @@ def decode_to_samples(response: oj.Response) -> Samples:
|
|
177
272
|
"""
|
178
273
|
Convert `openjij.Response <https://openjij.github.io/OpenJij/reference/openjij/index.html#openjij.Response>`_ to :class:`Samples`
|
179
274
|
"""
|
180
|
-
#
|
275
|
+
# Create empty samples and append each state with its sample IDs
|
181
276
|
# Since OpenJij does not issue the sample ID, we need to generate it in the responsibility of this OMMX Adapter
|
277
|
+
samples = Samples({}) # Create empty samples
|
182
278
|
sample_id = 0
|
183
|
-
entries = []
|
184
279
|
|
185
280
|
num_reads = len(response.record.num_occurrences)
|
186
281
|
for i in range(num_reads):
|
187
282
|
sample = response.record.sample[i]
|
188
|
-
state = State(entries=zip(response.variables, sample))
|
283
|
+
state = State(entries=zip(response.variables, sample))
|
189
284
|
# `num_occurrences` is encoded into sample ID list.
|
190
285
|
# For example, if `num_occurrences` is 2, there are two samples with the same state, thus two sample IDs are generated.
|
191
286
|
ids = []
|
192
287
|
for _ in range(response.record.num_occurrences[i]):
|
193
288
|
ids.append(sample_id)
|
194
289
|
sample_id += 1
|
195
|
-
|
196
|
-
|
290
|
+
samples.append(ids, state)
|
291
|
+
|
292
|
+
return samples
|
197
293
|
|
198
294
|
|
199
295
|
@deprecated("Use `OMMXOpenJijSAAdapter.sample` instead")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ommx_openjij_adapter
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.0rc2
|
4
4
|
Summary: OMMX Adapter for OpenJij.
|
5
5
|
Author-email: "Jij Inc." <info@j-ij.com>
|
6
6
|
Project-URL: Repository, https://github.com/Jij-Inc/ommx
|
@@ -14,8 +14,8 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
15
15
|
Requires-Python: <3.13,>=3.9
|
16
16
|
Description-Content-Type: text/markdown
|
17
|
-
Requires-Dist: ommx<3.0.0,>=2.0.
|
18
|
-
Requires-Dist: openjij
|
17
|
+
Requires-Dist: ommx<3.0.0,>=2.0.0rc1
|
18
|
+
Requires-Dist: openjij<0.10.0,>=0.9.2
|
19
19
|
|
20
20
|
OMMX Adapter for OpenJij
|
21
21
|
=========================
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "ommx_openjij_adapter"
|
7
|
-
version = "2.0.
|
7
|
+
version = "2.0.0rc2"
|
8
8
|
|
9
9
|
description = "OMMX Adapter for OpenJij."
|
10
10
|
authors = [{ name = "Jij Inc.", email = "info@j-ij.com" }]
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
22
22
|
"License :: OSI Approved :: Apache Software License",
|
23
23
|
"License :: OSI Approved :: MIT License",
|
24
24
|
]
|
25
|
-
dependencies = ["ommx >= 2.0.
|
25
|
+
dependencies = ["ommx >= 2.0.0rc1, < 3.0.0", "openjij >= 0.9.2, < 0.10.0"]
|
26
26
|
|
27
27
|
[project.urls]
|
28
28
|
Repository = "https://github.com/Jij-Inc/ommx"
|
@@ -95,6 +95,68 @@ def integer_inequality():
|
|
95
95
|
return pytest.param(instance, ans, id="integer_inequality")
|
96
96
|
|
97
97
|
|
98
|
+
def hubo_binary_no_constraint_minimize():
|
99
|
+
x0 = DecisionVariable.binary(0, name="x", subscripts=[0])
|
100
|
+
x1 = DecisionVariable.binary(1, name="x", subscripts=[1])
|
101
|
+
x2 = DecisionVariable.binary(2, name="x", subscripts=[2])
|
102
|
+
instance = Instance.from_components(
|
103
|
+
decision_variables=[x0, x1, x2],
|
104
|
+
objective=x0 + x0 * x1 * x2,
|
105
|
+
constraints=[],
|
106
|
+
sense=Instance.MINIMIZE,
|
107
|
+
)
|
108
|
+
ans = {(0,): 0.0, (1,): 0.0, (2,): 0.0}
|
109
|
+
return pytest.param(instance, ans, id="hubo_binary_no_constraint_minimize")
|
110
|
+
|
111
|
+
|
112
|
+
def hubo_binary_no_constraint_maximize():
|
113
|
+
x0 = DecisionVariable.binary(0, name="x", subscripts=[0])
|
114
|
+
x1 = DecisionVariable.binary(1, name="x", subscripts=[1])
|
115
|
+
x2 = DecisionVariable.binary(2, name="x", subscripts=[2])
|
116
|
+
instance = Instance.from_components(
|
117
|
+
decision_variables=[x0, x1, x2],
|
118
|
+
objective=x0 + x0 * x1 * x2,
|
119
|
+
constraints=[],
|
120
|
+
sense=Instance.MAXIMIZE,
|
121
|
+
)
|
122
|
+
ans = {(0,): 1.0, (1,): 1.0, (2,): 1.0}
|
123
|
+
return pytest.param(instance, ans, id="hubo_binary_no_constraint_maximize")
|
124
|
+
|
125
|
+
|
126
|
+
def hubo_binary_equality():
|
127
|
+
x0 = DecisionVariable.binary(0, name="x", subscripts=[0])
|
128
|
+
x1 = DecisionVariable.binary(1, name="x", subscripts=[1])
|
129
|
+
x2 = DecisionVariable.binary(2, name="x", subscripts=[2])
|
130
|
+
|
131
|
+
instance = Instance.from_components(
|
132
|
+
decision_variables=[x0, x1, x2],
|
133
|
+
objective=x0 + 2 * x1 + 3 * x2 + x0 * x1 * x2,
|
134
|
+
constraints=[x1 * x2 == 0],
|
135
|
+
sense=Instance.MAXIMIZE,
|
136
|
+
)
|
137
|
+
|
138
|
+
# x0 = x2 = 1, x1 = 0 is maximum
|
139
|
+
ans = {(0,): 1.0, (1,): 0.0, (2,): 1.0}
|
140
|
+
return pytest.param(instance, ans, id="hubo_binary_equality")
|
141
|
+
|
142
|
+
|
143
|
+
def hubo_binary_inequality():
|
144
|
+
x0 = DecisionVariable.binary(0, name="x", subscripts=[0])
|
145
|
+
x1 = DecisionVariable.binary(1, name="x", subscripts=[1])
|
146
|
+
x2 = DecisionVariable.binary(2, name="x", subscripts=[2])
|
147
|
+
|
148
|
+
instance = Instance.from_components(
|
149
|
+
decision_variables=[x0, x1, x2],
|
150
|
+
objective=x0 + 2 * x1 + 3 * x2 + x0 * x1 * x2,
|
151
|
+
constraints=[x0 + x1 + x2 <= 2],
|
152
|
+
sense=Instance.MAXIMIZE,
|
153
|
+
)
|
154
|
+
|
155
|
+
# x1 = x2 = 1, x0 = 0 is maximum
|
156
|
+
ans = {(0,): 0.0, (1,): 1.0, (2,): 1.0}
|
157
|
+
return pytest.param(instance, ans, id="hubo_binary_inequality")
|
158
|
+
|
159
|
+
|
98
160
|
@pytest.mark.parametrize(
|
99
161
|
"instance, ans",
|
100
162
|
[
|
@@ -104,11 +166,15 @@ def integer_inequality():
|
|
104
166
|
binary_inequality(),
|
105
167
|
integer_equality(),
|
106
168
|
integer_inequality(),
|
169
|
+
hubo_binary_no_constraint_minimize(),
|
170
|
+
hubo_binary_no_constraint_maximize(),
|
171
|
+
hubo_binary_equality(),
|
172
|
+
hubo_binary_inequality(),
|
107
173
|
],
|
108
174
|
)
|
109
175
|
def test_sample(instance, ans):
|
110
176
|
sample_set = OMMXOpenJijSAAdapter.sample(
|
111
|
-
instance, num_reads=1, uniform_penalty_weight=3.0, seed=
|
177
|
+
instance, num_reads=1, uniform_penalty_weight=3.0, seed=999
|
112
178
|
)
|
113
179
|
assert sample_set.extract_decision_variables("x", 0) == ans
|
114
180
|
|
@@ -122,11 +188,15 @@ def test_sample(instance, ans):
|
|
122
188
|
binary_inequality(),
|
123
189
|
integer_equality(),
|
124
190
|
integer_inequality(),
|
191
|
+
hubo_binary_no_constraint_minimize(),
|
192
|
+
hubo_binary_no_constraint_maximize(),
|
193
|
+
hubo_binary_equality(),
|
194
|
+
hubo_binary_inequality(),
|
125
195
|
],
|
126
196
|
)
|
127
197
|
def test_solve(instance, ans):
|
128
198
|
solution = OMMXOpenJijSAAdapter.solve(
|
129
|
-
instance, num_reads=1, uniform_penalty_weight=3.0, seed=
|
199
|
+
instance, num_reads=1, uniform_penalty_weight=3.0, seed=999
|
130
200
|
)
|
131
201
|
assert solution.extract_decision_variables("x") == ans
|
132
202
|
|
@@ -140,6 +210,10 @@ def test_solve(instance, ans):
|
|
140
210
|
binary_inequality(),
|
141
211
|
integer_equality(),
|
142
212
|
integer_inequality(),
|
213
|
+
hubo_binary_no_constraint_minimize(),
|
214
|
+
hubo_binary_no_constraint_maximize(),
|
215
|
+
hubo_binary_equality(),
|
216
|
+
hubo_binary_inequality(),
|
143
217
|
],
|
144
218
|
)
|
145
219
|
def test_sample_twice(instance, ans):
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|