Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Quick start with Python:
print(im.num_top_modules, im.codelength)
To prefer a certain hierarchy depth without forcing it, use
``preferred_number_of_levels``. This is a soft preference and cannot be
combined with ``two_level=True``.

.. _PyPI: https://pypi.org/project/infomap/
.. _`Infomap Python API`: https://mapequation.github.io/infomap/python/

Expand Down
4 changes: 4 additions & 0 deletions docs/_sources/index.rst.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Quick start with Python:
print(im.num_top_modules, im.codelength)
To prefer a certain hierarchy depth without forcing it, use
``preferred_number_of_levels``. This is a soft preference and cannot be
combined with ``two_level=True``.

.. _PyPI: https://pypi.org/project/infomap/
.. _`Infomap Python API`: https://mapequation.github.io/infomap/python/

Expand Down
4 changes: 4 additions & 0 deletions docs/_sources/python/index.rst.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ configuration across multiple runs:
im2.add_link(2, 3)
im2.run_with_options(options)
``preferred_number_of_levels`` provides a soft hierarchy-depth preference. It
guides result selection toward the requested number of levels but does not
force an exact depth, and it cannot be combined with ``two_level=True``.

NetworkX graphs
"""""""""""""""

Expand Down
3 changes: 3 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ <h3>Python package<a class="headerlink" href="#python-package" title="Link to th
<span class="nb">print</span><span class="p">(</span><span class="n">im</span><span class="o">.</span><span class="n">num_top_modules</span><span class="p">,</span> <span class="n">im</span><span class="o">.</span><span class="n">codelength</span><span class="p">)</span>
</pre></div>
</div>
<p>To prefer a certain hierarchy depth without forcing it, use
<code class="docutils literal notranslate"><span class="pre">preferred_number_of_levels</span></code>. This is a soft preference and cannot be
combined with <code class="docutils literal notranslate"><span class="pre">two_level=True</span></code>.</p>
</section>
<section id="javascript-package">
<h3>JavaScript package<a class="headerlink" href="#javascript-package" title="Link to this heading">¶</a></h3>
Expand Down
3 changes: 3 additions & 0 deletions docs/python/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ <h4>Reusable options<a class="headerlink" href="#reusable-options" title="Link t
<span class="n">im2</span><span class="o">.</span><span class="n">run_with_options</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
</pre></div>
</div>
<p><code class="docutils literal notranslate"><span class="pre">preferred_number_of_levels</span></code> provides a soft hierarchy-depth preference. It
guides result selection toward the requested number of levels but does not
force an exact depth, and it cannot be combined with <code class="docutils literal notranslate"><span class="pre">two_level=True</span></code>.</p>
</section>
<section id="networkx-graphs">
<h4>NetworkX graphs<a class="headerlink" href="#networkx-graphs" title="Link to this heading">¶</a></h4>
Expand Down
8 changes: 5 additions & 3 deletions docs/python/infomap.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/searchindex.js

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions interfaces/js/generated/parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,18 @@
"shortType": "n",
"default": "0"
},
{
"long": "--preferred-number-of-levels",
"short": "",
"description": "Prefer solutions close to this many levels. Soft preference only; does not force an exact hierarchy depth.",
"group": "Algorithm",
"required": true,
"advanced": true,
"incremental": false,
"longType": "integer",
"shortType": "n",
"default": "0"
},
{
"long": "--multilayer-relax-rate",
"short": "",
Expand Down
4 changes: 4 additions & 0 deletions interfaces/js/src/arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type Arguments = Partial<{
variableMarkovTime: boolean;
variableMarkovDamping: number;
preferredNumberOfModules: number;
preferredNumberOfLevels: number;
multilayerRelaxRate: number;
multilayerRelaxLimit: number;
multilayerRelaxLimitUp: number;
Expand Down Expand Up @@ -171,6 +172,9 @@ export default function argumentsToString(args: Arguments) {
if (args.preferredNumberOfModules != null)
result += " --preferred-number-of-modules " + args.preferredNumberOfModules;

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JS wrapper currently serializes twoLevel together with preferredNumberOfLevels, but the native CLI rejects this combination. Since the PR description says public wrappers should reject the invalid combination, consider adding a check in argumentsToString (or earlier in createWorker) that throws a clear error when both are provided, instead of letting the worker fail at runtime.

Suggested change
if (args.twoLevel && args.preferredNumberOfLevels != null) {
throw new Error(
"Invalid arguments: `twoLevel` cannot be combined with `preferredNumberOfLevels`."
);
}

Copilot uses AI. Check for mistakes.
if (args.preferredNumberOfLevels != null)
result += " --preferred-number-of-levels " + args.preferredNumberOfLevels;

if (args.multilayerRelaxRate != null)
result += " --multilayer-relax-rate " + args.multilayerRelaxRate;

Expand Down
3 changes: 2 additions & 1 deletion interfaces/js/test/unit/arguments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ describe("argumentsToString", () => {
expect(
argumentsToString({
twoLevel: true,
preferredNumberOfLevels: 2,
numTrials: 5,
output: ["tree", "clu"],
help: "advanced"
})
).toBe(" -o tree,clu --two-level --num-trials 5 -hh");
).toBe(" -o tree,clu --two-level --preferred-number-of-levels 2 --num-trials 5 -hh");
});
Comment on lines 6 to 15
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unit test now asserts that argumentsToString can produce --two-level --preferred-number-of-levels ..., but the native layer rejects this combination. If the JS wrapper is updated to reject the invalid combination (as described in the PR), update the test to use a valid argument set and add a separate test that the invalid combination throws.

Copilot uses AI. Check for mistakes.
});
94 changes: 94 additions & 0 deletions interfaces/python/generated/infomap_wrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9427,6 +9427,60 @@ SWIGINTERN PyObject *_wrap_Config_preferredNumberOfModules_get(PyObject *self, P
}


SWIGINTERN PyObject *_wrap_Config_preferredNumberOfLevels_set(PyObject *self, PyObject *args) {
PyObject *resultobj = 0;
infomap::Config *arg1 = 0 ;
unsigned int arg2 ;
void *argp1 = 0 ;
int res1 = 0 ;
unsigned int val2 ;
int ecode2 = 0 ;
PyObject *swig_obj[2] ;

(void)self;
if (!SWIG_Python_UnpackTuple(args, "Config_preferredNumberOfLevels_set", 2, 2, swig_obj)) SWIG_fail;
res1 = SWIG_ConvertPtr(swig_obj[0], &argp1,SWIGTYPE_p_infomap__Config, 0 | 0 );
if (!SWIG_IsOK(res1)) {
SWIG_exception_fail(SWIG_ArgError(res1), "in method '" "Config_preferredNumberOfLevels_set" "', argument " "1"" of type '" "infomap::Config *""'");
}
arg1 = reinterpret_cast< infomap::Config * >(argp1);
ecode2 = SWIG_AsVal_unsigned_SS_int(swig_obj[1], &val2);
if (!SWIG_IsOK(ecode2)) {
SWIG_exception_fail(SWIG_ArgError(ecode2), "in method '" "Config_preferredNumberOfLevels_set" "', argument " "2"" of type '" "unsigned int""'");
}
arg2 = static_cast< unsigned int >(val2);
if (arg1) (arg1)->preferredNumberOfLevels = arg2;
resultobj = SWIG_Py_Void();
return resultobj;
fail:
return NULL;
}


SWIGINTERN PyObject *_wrap_Config_preferredNumberOfLevels_get(PyObject *self, PyObject *args) {
PyObject *resultobj = 0;
infomap::Config *arg1 = 0 ;
void *argp1 = 0 ;
int res1 = 0 ;
PyObject *swig_obj[1] ;
unsigned int result;

(void)self;
if (!args) SWIG_fail;
swig_obj[0] = args;
res1 = SWIG_ConvertPtr(swig_obj[0], &argp1,SWIGTYPE_p_infomap__Config, 0 | 0 );
if (!SWIG_IsOK(res1)) {
SWIG_exception_fail(SWIG_ArgError(res1), "in method '" "Config_preferredNumberOfLevels_get" "', argument " "1"" of type '" "infomap::Config *""'");
}
arg1 = reinterpret_cast< infomap::Config * >(argp1);
result = (unsigned int) ((arg1)->preferredNumberOfLevels);
resultobj = SWIG_From_unsigned_SS_int(static_cast< unsigned int >(result));
return resultobj;
fail:
return NULL;
}


SWIGINTERN PyObject *_wrap_Config_entropyBiasCorrection_set(PyObject *self, PyObject *args) {
PyObject *resultobj = 0;
infomap::Config *arg1 = 0 ;
Expand Down Expand Up @@ -44821,6 +44875,43 @@ SWIGINTERN PyObject *_wrap_InfomapConfigInfomapBase_setTuneIterationLimit(PyObje
}


SWIGINTERN PyObject *_wrap_InfomapConfigInfomapBase_setPreferredNumberOfLevels(PyObject *self, PyObject *args) {
PyObject *resultobj = 0;
infomap::InfomapConfig< infomap::InfomapBase > *arg1 = 0 ;
unsigned int arg2 ;
void *argp1 = 0 ;
int res1 = 0 ;
unsigned int val2 ;
int ecode2 = 0 ;
PyObject *swig_obj[2] ;
infomap::InfomapBase *result = 0 ;

(void)self;
if (!SWIG_Python_UnpackTuple(args, "InfomapConfigInfomapBase_setPreferredNumberOfLevels", 2, 2, swig_obj)) SWIG_fail;
res1 = SWIG_ConvertPtr(swig_obj[0], &argp1,SWIGTYPE_p_infomap__InfomapConfigT_infomap__InfomapBase_t, 0 | 0 );
if (!SWIG_IsOK(res1)) {
SWIG_exception_fail(SWIG_ArgError(res1), "in method '" "InfomapConfigInfomapBase_setPreferredNumberOfLevels" "', argument " "1"" of type '" "infomap::InfomapConfig< infomap::InfomapBase > *""'");
}
arg1 = reinterpret_cast< infomap::InfomapConfig< infomap::InfomapBase > * >(argp1);
ecode2 = SWIG_AsVal_unsigned_SS_int(swig_obj[1], &val2);
if (!SWIG_IsOK(ecode2)) {
SWIG_exception_fail(SWIG_ArgError(ecode2), "in method '" "InfomapConfigInfomapBase_setPreferredNumberOfLevels" "', argument " "2"" of type '" "unsigned int""'");
}
arg2 = static_cast< unsigned int >(val2);
{
try {
result = (infomap::InfomapBase *) &(arg1)->setPreferredNumberOfLevels(arg2);
} catch (const std::exception& e) {
SWIG_exception(SWIG_RuntimeError, e.what());
}
}
resultobj = SWIG_NewPointerObj(SWIG_as_voidptr(result), SWIGTYPE_p_infomap__InfomapBase, 0 | 0 );
return resultobj;
fail:
return NULL;
}


SWIGINTERN PyObject *_wrap_InfomapConfigInfomapBase_setFastHierarchicalSolution(PyObject *self, PyObject *args) {
PyObject *resultobj = 0;
infomap::InfomapConfig< infomap::InfomapBase > *arg1 = 0 ;
Expand Down Expand Up @@ -56865,6 +56956,8 @@ static PyMethodDef SwigMethods[] = {
{ "Config_teleportationProbability_get", _wrap_Config_teleportationProbability_get, METH_O, NULL},
{ "Config_preferredNumberOfModules_set", _wrap_Config_preferredNumberOfModules_set, METH_VARARGS, NULL},
{ "Config_preferredNumberOfModules_get", _wrap_Config_preferredNumberOfModules_get, METH_O, NULL},
{ "Config_preferredNumberOfLevels_set", _wrap_Config_preferredNumberOfLevels_set, METH_VARARGS, NULL},
{ "Config_preferredNumberOfLevels_get", _wrap_Config_preferredNumberOfLevels_get, METH_O, NULL},
{ "Config_entropyBiasCorrection_set", _wrap_Config_entropyBiasCorrection_set, METH_VARARGS, NULL},
{ "Config_entropyBiasCorrection_get", _wrap_Config_entropyBiasCorrection_get, METH_O, NULL},
{ "Config_entropyBiasCorrectionMultiplier_set", _wrap_Config_entropyBiasCorrectionMultiplier_set, METH_VARARGS, NULL},
Expand Down Expand Up @@ -57637,6 +57730,7 @@ static PyMethodDef SwigMethods[] = {
{ "InfomapConfigInfomapBase_setVerbosity", _wrap_InfomapConfigInfomapBase_setVerbosity, METH_VARARGS, NULL},
{ "InfomapConfigInfomapBase_setTwoLevel", _wrap_InfomapConfigInfomapBase_setTwoLevel, METH_VARARGS, NULL},
{ "InfomapConfigInfomapBase_setTuneIterationLimit", _wrap_InfomapConfigInfomapBase_setTuneIterationLimit, METH_VARARGS, NULL},
{ "InfomapConfigInfomapBase_setPreferredNumberOfLevels", _wrap_InfomapConfigInfomapBase_setPreferredNumberOfLevels, METH_VARARGS, NULL},
{ "InfomapConfigInfomapBase_setFastHierarchicalSolution", _wrap_InfomapConfigInfomapBase_setFastHierarchicalSolution, METH_VARARGS, NULL},
{ "InfomapConfigInfomapBase_setOnlySuperModules", _wrap_InfomapConfigInfomapBase_setOnlySuperModules, METH_VARARGS, NULL},
{ "InfomapConfigInfomapBase_setNoCoarseTune", _wrap_InfomapConfigInfomapBase_setNoCoarseTune, METH_VARARGS, NULL},
Expand Down
4 changes: 4 additions & 0 deletions interfaces/python/source/python/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ configuration across multiple runs:
im2.add_link(2, 3)
im2.run_with_options(options)
``preferred_number_of_levels`` provides a soft hierarchy-depth preference. It
guides result selection toward the requested number of levels but does not
force an exact depth, and it cannot be combined with ``two_level=True``.

NetworkX graphs
"""""""""""""""

Expand Down
2 changes: 2 additions & 0 deletions interfaces/python/src/infomap/_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def __init__(
variable_markov_time=False,
variable_markov_damping=1.0,
preferred_number_of_modules=None,
preferred_number_of_levels=None,
multilayer_relax_rate=_DEFAULT_MULTILAYER_RELAX_RATE,
multilayer_relax_limit=-1,
multilayer_relax_limit_up=-1,
Expand Down Expand Up @@ -991,6 +992,7 @@ def run(
variable_markov_time=False,
variable_markov_damping=1.0,
preferred_number_of_modules=None,
preferred_number_of_levels=None,
multilayer_relax_rate=_DEFAULT_MULTILAYER_RELAX_RATE,
multilayer_relax_limit=-1,
multilayer_relax_limit_up=-1,
Expand Down
11 changes: 11 additions & 0 deletions interfaces/python/src/infomap/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
"--preferred-number-of-modules",
lambda value: value is not None,
),
(
"value",
"preferred_number_of_levels",
"--preferred-number-of-levels",
lambda value: value is not None,
),
(
"value",
"multilayer_relax_rate",
Expand Down Expand Up @@ -277,6 +283,9 @@ class InfomapOptions:
``1`` means full rescaling to constant transition flow rate.
preferred_number_of_modules : int, optional
Penalize solutions by how much they differ from this number.
preferred_number_of_levels : int, optional
Prefer solutions close to this many levels. This is a soft preference
and cannot be combined with ``two_level=True``.
multilayer_relax_rate : float, optional
Probability to relax the constraint to move only in the current layer.
multilayer_relax_limit : int, optional
Expand Down Expand Up @@ -359,6 +368,7 @@ class InfomapOptions:
variable_markov_time: bool = False
variable_markov_damping: float = 1.0
preferred_number_of_modules: int | None = None
preferred_number_of_levels: int | None = None
multilayer_relax_rate: float = _DEFAULT_MULTILAYER_RELAX_RATE
multilayer_relax_limit: int = -1
multilayer_relax_limit_up: int = -1
Expand Down Expand Up @@ -470,6 +480,7 @@ def _construct_args(
variable_markov_time=False,
variable_markov_damping=1.0,
preferred_number_of_modules=None,
preferred_number_of_levels=None,
multilayer_relax_rate=_DEFAULT_MULTILAYER_RELAX_RATE,
multilayer_relax_limit=-1,
multilayer_relax_limit_up=-1,
Expand Down
4 changes: 4 additions & 0 deletions interfaces/python/src/infomap/_swig.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class Config(object):
regularizationStrength = property(_infomap.Config_regularizationStrength_get, _infomap.Config_regularizationStrength_set)
teleportationProbability = property(_infomap.Config_teleportationProbability_get, _infomap.Config_teleportationProbability_set)
preferredNumberOfModules = property(_infomap.Config_preferredNumberOfModules_get, _infomap.Config_preferredNumberOfModules_set)
preferredNumberOfLevels = property(_infomap.Config_preferredNumberOfLevels_get, _infomap.Config_preferredNumberOfLevels_set)
entropyBiasCorrection = property(_infomap.Config_entropyBiasCorrection_get, _infomap.Config_entropyBiasCorrection_set)
entropyBiasCorrectionMultiplier = property(_infomap.Config_entropyBiasCorrectionMultiplier_get, _infomap.Config_entropyBiasCorrectionMultiplier_set)
seedToRandomNumberGenerator = property(_infomap.Config_seedToRandomNumberGenerator_get, _infomap.Config_seedToRandomNumberGenerator_set)
Expand Down Expand Up @@ -2427,6 +2428,9 @@ def setTwoLevel(self, value):
def setTuneIterationLimit(self, value):
return _infomap.InfomapConfigInfomapBase_setTuneIterationLimit(self, value)

def setPreferredNumberOfLevels(self, value):
return _infomap.InfomapConfigInfomapBase_setPreferredNumberOfLevels(self, value)

def setFastHierarchicalSolution(self, level):
return _infomap.InfomapConfigInfomapBase_setFastHierarchicalSolution(self, level)

Expand Down
Loading
Loading