|
| 1 | +--- |
| 2 | +jupytext: |
| 3 | + text_representation: |
| 4 | + extension: .md |
| 5 | + format_name: myst |
| 6 | + format_version: 0.13 |
| 7 | + jupytext_version: 1.19.1 |
| 8 | +kernelspec: |
| 9 | + display_name: .venv |
| 10 | + language: python |
| 11 | + name: python3 |
| 12 | +--- |
| 13 | + |
| 14 | +# JijModeling 2.4.0 Release Notes |
| 15 | + |
| 16 | ++++ |
| 17 | + |
| 18 | +## Performance Improvements |
| 19 | + |
| 20 | +### Significant performance improvements for dictionaries |
| 21 | + |
| 22 | +We improved the internal processing of dictionaries, achieving a significant performance improvement of about 30x compared with the previous implementation. |
| 23 | +If you have been avoiding dictionaries because of performance concerns, this is a good opportunity to try using them. |
| 24 | + |
| 25 | ++++ |
| 26 | + |
| 27 | +## Breaking Changes |
| 28 | + |
| 29 | +### Protobuf schema changes |
| 30 | + |
| 31 | +JijModeling 2.4.0 brings the breaking changes to the Protobuf schema for {py:class}`~jijmodeling.Problem`. |
| 32 | +As a result, Problems serialized to Protobuf with version 2.4.0 or later can no longer be loaded by JijModeling versions 2.3.x or earlier. |
| 33 | +On the other hand, Problems serialized with versions 2.3.x or earlier can be loaded by JijModeling versions 2.4.0 or later. |
| 34 | +This may affect data storage and exchange through MINTO, but in that case, updating the dependent JijModeling version to 2.4.0 or later will allow both existing and new data to be loaded without issue. |
| 35 | +Also, this only affects direct use of JijModeling's Protobuf schema; there is no particular impact on the OMMX format. |
| 36 | + |
| 37 | ++++ |
| 38 | + |
| 39 | +## Feature Enhancements |
| 40 | + |
| 41 | ++++ |
| 42 | + |
| 43 | +### Generating arrays with a shape and generator function |
| 44 | + |
| 45 | +Starting with this version, the {py:func}`~jijmodeling.genarray` function can be used to generate arrays by specifying a shape and a generator function. |
| 46 | +This is similar to {py:func}`~numpy.fromfunction` in NumPy. |
| 47 | + |
| 48 | +```{code-cell} ipython3 |
| 49 | +import jijmodeling as jm |
| 50 | +
|
| 51 | +
|
| 52 | +problem = jm.Problem("genarray example") |
| 53 | +N = problem.Natural("N") |
| 54 | +M = problem.Natural("M") |
| 55 | +a = problem.Float("a", shape=(N, M)) |
| 56 | +x = problem.BinaryVar("x", shape=N) |
| 57 | +Sums = problem.NamedExpr("Sums", jm.genarray(lambda i, j: a[i, j] * x[i], (N, M))) |
| 58 | +
|
| 59 | +
|
| 60 | +problem |
| 61 | +``` |
| 62 | + |
| 63 | +When using the Decorator API, you can also use a comprehension syntax with `jm.genarray` as follows: |
| 64 | + |
| 65 | +```{code-cell} ipython3 |
| 66 | +@jm.Problem.define("genarray example") |
| 67 | +def problem(problem): |
| 68 | + N = problem.Natural() |
| 69 | + M = problem.Natural() |
| 70 | + a = problem.Float(shape=(N, M)) |
| 71 | + x = problem.BinaryVar(shape=N) |
| 72 | + Sums = problem.NamedExpr(jm.genarray(a[i, j] * x[i] for i, j in (N, M))) |
| 73 | +
|
| 74 | +
|
| 75 | +problem |
| 76 | +``` |
| 77 | + |
| 78 | +Only one `for .. in ...` clause is allowed in a `genarray` comprehension. |
| 79 | +The following is an example that raises an error because it uses multiple `for` clauses: |
| 80 | + |
| 81 | +```{code-cell} ipython3 |
| 82 | +try: |
| 83 | +
|
| 84 | + @jm.Problem.define("genarray example") |
| 85 | + def problem(problem): |
| 86 | + N = problem.Natural() |
| 87 | + M = problem.Natural() |
| 88 | + a = problem.Float(shape=(N, M)) |
| 89 | + x = problem.BinaryVar(shape=N) |
| 90 | + Sums = problem.NamedExpr(jm.genarray(a[i, j] * x[i] for i in N for j in M)) |
| 91 | +except SyntaxError as e: |
| 92 | + print(str(e)) |
| 93 | +``` |
| 94 | + |
| 95 | +### Support for `min` / `max` along axes |
| 96 | + |
| 97 | +Previously, {py:func}`jm.sum <jijmodeling.sum>` and {py:meth}`Expression.sum <jijmodeling.Expression.sum>` supported taking sums along a specific axis of a multidimensional array via the `axis` keyword argument. |
| 98 | +Starting with this version, the same functionality has been added to {py:func}`jm.min <jijmodeling.min>` and {py:func}`jm.max <jijmodeling.max>` as well as their corresponding `Expression` methods. |
| 99 | + |
| 100 | +```{code-cell} ipython3 |
| 101 | +import jijmodeling as jm |
| 102 | +
|
| 103 | +
|
| 104 | +@jm.Problem.define("min/max along axes example") |
| 105 | +def problem(problem): |
| 106 | + N = problem.Natural() |
| 107 | + M = problem.Natural() |
| 108 | + a = problem.Float(shape=(N, M)) |
| 109 | + a_min_0 = problem.NamedExpr(a.min(axis=0), save_in_ommx=True) |
| 110 | + a_max_1 = problem.NamedExpr(jm.max(a, axis=1), save_in_ommx=True) |
| 111 | + a_min_both = problem.NamedExpr(jm.min(a, axis=[1, 0]), save_in_ommx=True) |
| 112 | +
|
| 113 | +
|
| 114 | +problem |
| 115 | +``` |
| 116 | + |
| 117 | +Now let's create an instance and inspect the included Named Functions together with the value of `a`. |
| 118 | + |
| 119 | +```{code-cell} ipython3 |
| 120 | +import numpy as np |
| 121 | +
|
| 122 | +a_data = np.array([[1, 5, 3], [4, 2, 6]]) |
| 123 | +compiler = jm.Compiler.from_problem(problem, {"N": 2, "M": 3, "a": a_data}) |
| 124 | +instance = compiler.eval_problem(problem) |
| 125 | +
|
| 126 | +display(instance.named_functions_df) |
| 127 | +print(f"a == {a_data}") |
| 128 | +``` |
| 129 | + |
| 130 | +Since the Named Functions in the OMMX Instance are split apart by index, the table above may be a bit hard to read. |
| 131 | +So let's regroup them by variable using `compiler`, build arrays from them, and compare the results. |
| 132 | + |
| 133 | +First, consider `a_min_0 = a.min(axis=0)`, which takes the minimum along axis 0 (columns). |
| 134 | +This leaves axis 1 (rows), producing a vector whose entries are the minima of each column. |
| 135 | + |
| 136 | +```{code-cell} ipython3 |
| 137 | +a_min_0_ids = compiler.get_named_function_id_by_name("a_min_0") |
| 138 | +a_min_0_values = [ |
| 139 | + instance.get_named_function_by_id(a_min_0_ids[(i,)]).function.constant_term |
| 140 | + for i in range(3) |
| 141 | +] |
| 142 | +assert np.all(a_min_0_values == np.min(a_data, axis=0)) # Matches NumPy's behavior! |
| 143 | +print(f"a.min(axis=0) == {a_min_0_values}") |
| 144 | +``` |
| 145 | + |
| 146 | +In contrast, `a_max_1 = a.max(axis=1)` takes the maximum along axis 1 (rows), |
| 147 | +producing a vector whose entries are the maxima of each row. |
| 148 | + |
| 149 | +```{code-cell} ipython3 |
| 150 | +a_max_1_ids = compiler.get_named_function_id_by_name("a_max_1") |
| 151 | +a_max_1_values = [ |
| 152 | + instance.get_named_function_by_id(a_max_1_ids[(i,)]).function.constant_term |
| 153 | + for i in range(2) |
| 154 | +] |
| 155 | +assert np.all(a_max_1_values == np.max(a_data, axis=1)) # Matches NumPy's behavior! |
| 156 | +print(f"a.max(axis=1) == {a_max_1_values}") |
| 157 | +``` |
| 158 | + |
| 159 | +For `a_min_both = a.min(axis=[1, 0])`, the minimum is taken along multiple axes. |
| 160 | +Since the input here is two-dimensional, this simply becomes the overall minimum. |
| 161 | + |
| 162 | +```{code-cell} ipython3 |
| 163 | +a_min_both_ids = compiler.get_named_function_id_by_name("a_min_both") |
| 164 | +a_min_both_value = instance.get_named_function_by_id( |
| 165 | + a_min_both_ids[()] |
| 166 | +).function.constant_term |
| 167 | +assert a_min_both_value == np.min(a_data) # Matches NumPy's behavior! |
| 168 | +print(f"a.min(axis=[1, 0]) == {a_min_both_value}") |
| 169 | +``` |
| 170 | + |
| 171 | +## Bugfixes |
| 172 | + |
| 173 | ++++ |
| 174 | + |
| 175 | +### Bugfixes in random instance data generation |
| 176 | + |
| 177 | +We fixed the following two bugs in random instance data generation: |
| 178 | + |
| 179 | +#### Placeholders that depend on `NamedExpr` were not handled correctly |
| 180 | + |
| 181 | +We fixed a bug where placeholders whose shape (length) or key set depends on `NamedExpr` were not handled correctly. |
| 182 | +For example, consider the following problem: |
| 183 | + |
| 184 | +```{code-cell} ipython3 |
| 185 | +import jijmodeling as jm |
| 186 | +
|
| 187 | +
|
| 188 | +@jm.Problem.define("My Problem") |
| 189 | +def problem(problem: jm.DecoratedProblem): |
| 190 | + a = problem.Float(ndim=1) |
| 191 | + N = problem.NamedExpr(a.len_at(0)) |
| 192 | + b = problem.Natural(shape=(N, None)) |
| 193 | + M = problem.NamedExpr(b.len_at(1)) |
| 194 | + problem += jm.sum(a[i] * b[i, j] for i in N for j in M) |
| 195 | +
|
| 196 | +
|
| 197 | +problem |
| 198 | +``` |
| 199 | + |
| 200 | +In previous versions, calling `generate_random_dataset()` on this `problem` raised an exception. Starting with this release, the data is generated correctly. |
| 201 | + |
| 202 | +```{code-cell} ipython3 |
| 203 | +problem.generate_random_dataset(seed=17) |
| 204 | +``` |
| 205 | + |
| 206 | +#### Fixed a bug where generation failed when unused placeholders were present |
| 207 | + |
| 208 | +Data generation failed when there were unused placeholders not included in `used_placeholder()`. |
| 209 | +For example, in the following code, `N` is defined but never used, and previous versions raised a runtime exception. |
| 210 | + |
| 211 | +```{code-cell} ipython3 |
| 212 | +import jijmodeling as jm |
| 213 | +
|
| 214 | +problem = jm.Problem("My Problem") |
| 215 | +N = problem.Natural("N") |
| 216 | +
|
| 217 | +problem.generate_random_dataset(seed=17) |
| 218 | +``` |
| 219 | + |
| 220 | +Starting with this release, data is generated successfully in cases like the example above. |
| 221 | + |
| 222 | +### Fixed a bug where `latex` specifications were ignored in LaTeX output for decision variable bounds |
| 223 | + |
| 224 | +We fixed a bug where the values of the `latex=` keyword argument for other variables were ignored when outputting decision variable bounds in $\LaTeX$. |
| 225 | + |
| 226 | +```{code-cell} ipython3 |
| 227 | +import jijmodeling as jm |
| 228 | +
|
| 229 | +problem = jm.Problem("LaTeX bugfix example") |
| 230 | +L = problem.Float("L", latex=r"\ell") |
| 231 | +U = problem.Float("U", latex=r"\mathcal{U}") |
| 232 | +x = problem.ContinuousVar("x", lower_bound=L, upper_bound=U) |
| 233 | +problem += x |
| 234 | +
|
| 235 | +problem |
| 236 | +``` |
| 237 | + |
| 238 | +In previous releases, the `latex` specifications were ignored in the code above, and the bounds were displayed as $L \leq x \leq U$. |
| 239 | +Starting with this release, the settings are preserved as shown above, and the bounds are displayed as $\ell \leq x \leq \mathcal{U}$. |
| 240 | + |
| 241 | +### Fixed a bug where problem evaluation with constraint detection crashed when decision variables were subscripted by tuples |
| 242 | + |
| 243 | +We fixed a bug where `eval_problem` crashed when decision variables were subscripted with tuples and constraint detection was enabled (this is the case by default, or when the `constraint_detection` keyword argument was set to something other than `False`). For example, the following code used to crash in previous versions: |
| 244 | + |
| 245 | +```{code-cell} ipython3 |
| 246 | +import jijmodeling as jm |
| 247 | +
|
| 248 | +
|
| 249 | +@jm.Problem.define("dict-keyed binary var with tuple subscripts") |
| 250 | +def problem(problem: jm.DecoratedProblem): |
| 251 | + N = problem.Natural() |
| 252 | + K = problem.Placeholder(ndim=1, dtype=(jm.DataType.NATURAL, jm.DataType.NATURAL)) |
| 253 | + x = problem.BinaryVar(dict_keys=K) |
| 254 | +
|
| 255 | + problem += problem.Constraint( |
| 256 | + "sweeps", |
| 257 | + (jm.sum(x[k] for k in K if k[0] == i) <= 1 for i in jm.range(N)), |
| 258 | + ) |
| 259 | +
|
| 260 | +
|
| 261 | +instance_data = { |
| 262 | + "N": 3, |
| 263 | + "K": [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1)], |
| 264 | +} |
| 265 | +
|
| 266 | +compiler = jm.Compiler.from_problem(problem, instance_data) |
| 267 | +instance = compiler.eval_problem(problem, constraint_detection=True) |
| 268 | +``` |
| 269 | + |
| 270 | +### Fixed a bug where the sum of binary `{0, 1}` expressions had type Binary instead of Natural |
| 271 | + |
| 272 | +We fixed a bug where an expression that `sum`s another expression of binary type (`{0, 1}`) was typed as `Binary` instead of `Natural`. For example, the sum $\sum_i x_i$ of binary variables $x_0, x_1, \ldots$ can take values of $2$ or more, so the result type had to be `Natural` instead of `Binary`. |
| 273 | + |
| 274 | +```{code-cell} ipython3 |
| 275 | +import jijmodeling as jm |
| 276 | +
|
| 277 | +problem = jm.Problem("Sum of binary example") |
| 278 | +N = problem.Natural("N") |
| 279 | +x = problem.BinaryVar("x", shape=N) |
| 280 | +problem.infer(x.sum()) |
| 281 | +``` |
| 282 | + |
| 283 | +## Other Changes |
| 284 | + |
| 285 | +- Relaxed version bounds to allow installation on any Python 3 version from Python 3.11 onwards. |
| 286 | +- Error messages for invalid comprehensions used with the Decorator API in `sum` and similar constructs now report the specific location in the source code. |
| 287 | +- {py:meth}`Problem.used_placeholders <jijmodeling.Problem.used_placeholders>` has been deprecated because its purpose is unclear, and {py:class}`~jijmodeling.Compiler` also requires values for all placeholders. Use {py:meth}`Problem.placeholders <jijmodeling.Problem.placeholders>` instead. |
0 commit comments