Skip to content

Commit 36cd63f

Browse files
committed
Merge branch 'develop' into main
2 parents 7b1286e + 7a58a6f commit 36cd63f

10 files changed

Lines changed: 2275 additions & 134 deletions

File tree

Binary file not shown.

app/modules/generator/routes.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,31 @@ def validate_step3_form(form, max_features: int = 10000):
732732
except Exception:
733733
errors["prob_len"] = "Value must be a decimal between 0 and 1."
734734

735+
# 8b) CTC TYPE DISTRIBUTION (CTC_DIST_BOOLEAN/INTEGER/REAL/STRING)
736+
# Only required if the user has enabled any non-Boolean level in step2.
737+
# Otherwise the section is hidden and the default 100% boolean holds.
738+
arithmetic_level_checked = form.get("arithmetic_level") in ["on", "true", "1", True]
739+
if arithmetic_level_checked or type_level_checked:
740+
ctc_fields = [
741+
("ctc_dist_boolean", True),
742+
("ctc_dist_integer", arithmetic_level_checked),
743+
("ctc_dist_real", arithmetic_level_checked),
744+
("ctc_dist_string", type_level_checked and string_constraints_checked),
745+
]
746+
active_sum = 0.0
747+
for field, active in ctc_fields:
748+
v = _safe_float(form.get(field), 0.0)
749+
if not active:
750+
v = 0.0
751+
if not (0.0 <= v <= 1.0):
752+
errors[field] = "Value must be between 0 and 1."
753+
values[field] = v
754+
if active:
755+
active_sum += v
756+
if abs(active_sum - 1.0) > 0.001:
757+
errors["ctc_dist_sum"] = f"Current sum: {active_sum:.4f}. Active type probabilities must total 1.0."
758+
values["ctc_dist_sum"] = f"{active_sum:.4f}"
759+
735760
# 9) GUARDAR TODOS LOS VALORES PARA REPINTAR EL FORMULARIO
736761
for k in form:
737762
values[k] = form[k]
@@ -921,6 +946,28 @@ def step3():
921946
else:
922947
params_dict["PROB_LEN_FUNCTION"] = 0.0
923948

949+
# Cross-tree constraint TYPE distribution (CTC_DIST_*). Inactive
950+
# levels are pinned to 0 so a stale POST value can't skew the mix.
951+
params_dict["CTC_DIST_BOOLEAN"] = _safe_float(request.form.get("ctc_dist_boolean"), 0.7)
952+
if arithmetic_level_enabled:
953+
params_dict["CTC_DIST_INTEGER"] = _safe_float(request.form.get("ctc_dist_integer"), 0.2)
954+
params_dict["CTC_DIST_REAL"] = _safe_float(request.form.get("ctc_dist_real"), 0.1)
955+
else:
956+
params_dict["CTC_DIST_INTEGER"] = 0.0
957+
params_dict["CTC_DIST_REAL"] = 0.0
958+
if type_level_enabled and string_constraints_enabled:
959+
params_dict["CTC_DIST_STRING"] = _safe_float(request.form.get("ctc_dist_string"), 0.0)
960+
else:
961+
params_dict["CTC_DIST_STRING"] = 0.0
962+
_ctc_keys = ["CTC_DIST_BOOLEAN", "CTC_DIST_INTEGER", "CTC_DIST_REAL", "CTC_DIST_STRING"]
963+
_ctc_total = sum(params_dict[k] for k in _ctc_keys)
964+
if _ctc_total > 0:
965+
for k in _ctc_keys:
966+
params_dict[k] = round(params_dict[k] / _ctc_total, 6)
967+
params_dict[_ctc_keys[0]] += round(1.0 - sum(params_dict[k] for k in _ctc_keys), 6)
968+
else:
969+
params_dict["CTC_DIST_BOOLEAN"] = 1.0
970+
924971
# Renormalise the Boolean-connective probabilities to sum EXACTLY 1.0.
925972
# Params.__post_init__ enforces a 1e-6 tolerance; the form-side
926973
# normaliser rounds to 4 decimals, which can leave residue.
@@ -967,6 +1014,11 @@ def step3():
9671014
"boolop_sum": "1.0000",
9681015
"arithmetic_sum": "1.0000",
9691016
"cmp_sum": "1.0000",
1017+
"ctc_dist_sum": "1.0000",
1018+
"ctc_dist_boolean": params_dict.get("CTC_DIST_BOOLEAN", 0.7),
1019+
"ctc_dist_integer": params_dict.get("CTC_DIST_INTEGER", 0.2),
1020+
"ctc_dist_real": params_dict.get("CTC_DIST_REAL", 0.1),
1021+
"ctc_dist_string": params_dict.get("CTC_DIST_STRING", 0.0),
9701022
"arithmetic_level": params_dict.get("ARITHMETIC_LEVEL", False),
9711023
"aggregate_functions": params_dict.get("AGGREGATE_FUNCTIONS", False),
9721024
"type_level": params_dict.get("TYPE_LEVEL", False),
@@ -1085,6 +1137,29 @@ def validate_step4_form(form, params_dict=None):
10851137
values["min_attributes"] = min_attr_val
10861138
values["max_attributes"] = max_attr_val
10871139

1140+
# Attribute-type distribution: must total 1.0 across active types.
1141+
# Disabled types are forced to 0 server-side so a stale POST value
1142+
# can't skew the distribution.
1143+
dist_fields = [
1144+
("dist_boolean", True),
1145+
("dist_integer", arithmetic_level_enabled),
1146+
("dist_real", arithmetic_level_enabled),
1147+
("dist_string", type_level_enabled),
1148+
]
1149+
active_total = 0.0
1150+
for field, active in dist_fields:
1151+
v = _safe_float(form.get(field), 0.0)
1152+
if not active:
1153+
v = 0.0
1154+
if not (0.0 <= v <= 1.0):
1155+
errors[field] = "Value must be between 0 and 1."
1156+
values[field] = v
1157+
if active:
1158+
active_total += v
1159+
if abs(active_total - 1.0) > 0.001:
1160+
errors["attr_dist_sum"] = f"Current sum: {active_total:.4f}. Active type probabilities must total 1.0."
1161+
values["attr_dist_sum"] = f"{active_total:.4f}"
1162+
10881163
else:
10891164
values["min_attributes"] = ""
10901165
values["max_attributes"] = ""
@@ -1300,6 +1375,27 @@ def _apply_step4_form(params_dict, form):
13001375
params_dict["ATTRIBUTES_LIST"] = []
13011376
params_dict["ATTRIBUTE_ATTACH_PROBS"] = []
13021377
params_dict["ATTRIBUTE_IN_CONSTRAINTS"] = []
1378+
# Attribute-type distribution. Inactive types (gated by step2 levels)
1379+
# are forced to 0 here so the dataclass invariant "sum over active
1380+
# kinds == 1" survives even if the user tampers with the form.
1381+
arith_on = bool(params_dict.get("ARITHMETIC_LEVEL", False))
1382+
type_on = bool(params_dict.get("TYPE_LEVEL", False))
1383+
dist = {
1384+
"DIST_BOOLEAN": _safe_float(form.get("dist_boolean"), 0.7),
1385+
"DIST_INTEGER": _safe_float(form.get("dist_integer"), 0.0) if arith_on else 0.0,
1386+
"DIST_REAL": _safe_float(form.get("dist_real"), 0.0) if arith_on else 0.0,
1387+
"DIST_STRING": _safe_float(form.get("dist_string"), 0.0) if type_on else 0.0,
1388+
}
1389+
active_sum = sum(v for v in dist.values())
1390+
if active_sum > 0:
1391+
for k in dist:
1392+
dist[k] = round(dist[k] / active_sum, 6)
1393+
# Absorb rounding residue into the dominant active component so
1394+
# the sum lands exactly on 1.0.
1395+
residue = round(1.0 - sum(dist.values()), 6)
1396+
dominant = max(dist, key=dist.get)
1397+
dist[dominant] = round(dist[dominant] + residue, 6)
1398+
params_dict.update(dist)
13031399
else:
13041400
attrs, probs, in_ctc = _collect_step4_attributes(form, params_dict)
13051401
params_dict["MIN_ATTRIBUTES"] = None
@@ -1343,6 +1439,11 @@ def step4():
13431439
"min_attributes": params_dict.get("MIN_ATTRIBUTES", 1),
13441440
"max_attributes": params_dict.get("MAX_ATTRIBUTES", 5),
13451441
"attributes_list": params_dict.get("ATTRIBUTES_LIST", []),
1442+
"dist_boolean": params_dict.get("DIST_BOOLEAN", 0.7),
1443+
"dist_integer": params_dict.get("DIST_INTEGER", 0.1),
1444+
"dist_real": params_dict.get("DIST_REAL", 0.1),
1445+
"dist_string": params_dict.get("DIST_STRING", 0.1),
1446+
"attr_dist_sum": "1.0000",
13461447
}
13471448

13481449
values = load_step_state(4, default_values)

app/modules/generator/templates/generator/_macros.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ <h4 class="fw-bold text-gray-900 mb-1">{{ title }}</h4>
117117
</div>
118118
{%- endmacro %}
119119

120-
{%- macro probability_field(name, label, tip, values, errors, default=0.0, symbol=None) -%}
121-
<div class="col">
120+
{%- macro probability_field(name, label, tip, values, errors, default=0.0, symbol=None, disabled=False) -%}
121+
<div class="col{% if disabled %} opacity-50{% endif %}">
122122
<label for="{{ name }}" class="form-label fs-7 text-gray-700 d-flex align-items-center">
123123
{% if symbol %}<span class="fs-5 fw-bold text-gray-900 me-1">{{ symbol }}</span>{% endif %}
124124
{{ label }}
@@ -129,7 +129,13 @@ <h4 class="fw-bold text-gray-900 mb-1">{{ title }}</h4>
129129
name="{{ name }}"
130130
class="form-control form-control-solid"
131131
step="0.01" min="0" max="1"
132-
value="{{ values.get(name, default) }}">
132+
value="{{ values.get(name, 0.0 if disabled else default) }}"
133+
{% if disabled %}disabled data-force-zero="1"{% endif %}>
134+
{% if disabled %}
135+
{# A disabled input won't submit; mirror the value so the server still
136+
sees 0 for this field and the distribution stays consistent. #}
137+
<input type="hidden" name="{{ name }}" value="0.0">
138+
{% endif %}
133139
{% if errors.get(name) %}
134140
<div class="text-danger fs-7 mt-1">{{ errors[name] }}</div>
135141
{% endif %}

app/modules/generator/templates/generator/step3.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@
3131
'Maximum times a feature can appear in each constraint. Must be ≤ Max variables per constraint.',
3232
values, errors, default=1, min=1, max=10000) }}
3333

34+
{% if values.get('arithmetic_level') or values.get('type_level') %}
35+
<div class="separator separator-dashed my-9"></div>
36+
37+
{{ section('Constraint type distribution',
38+
'How often a constraint is Boolean, arithmetic (integer/real attributes) or string. Disabled levels always score 0. Active values must add up to 1.') }}
39+
40+
<div class="row g-3 mb-2">
41+
{{ probability_field('ctc_dist_boolean', 'Boolean',
42+
'Probability of a pure Boolean constraint (features and negations only).',
43+
values, errors, default=0.7, symbol='B') }}
44+
{{ probability_field('ctc_dist_integer', 'Integer',
45+
'Probability of a constraint involving integer attributes. Requires Arithmetic level.',
46+
values, errors, default=0.2, symbol='ℤ',
47+
disabled=(not values.get('arithmetic_level'))) }}
48+
{{ probability_field('ctc_dist_real', 'Real',
49+
'Probability of a constraint involving real attributes. Requires Arithmetic level.',
50+
values, errors, default=0.1, symbol='ℝ',
51+
disabled=(not values.get('arithmetic_level'))) }}
52+
{{ probability_field('ctc_dist_string', 'String',
53+
'Probability of a constraint involving string attributes. Requires Type level.',
54+
values, errors, default=0.0, symbol='S',
55+
disabled=(not values.get('string_constraints'))) }}
56+
</div>
57+
{{ sum_badge('ctc_dist_sum', 'Sum of active type probabilities', initial=values.get('ctc_dist_sum', '1.0000')) }}
58+
{% if errors.get('ctc_dist_sum') %}
59+
<div class="text-danger fs-7 mt-1">{{ errors['ctc_dist_sum'] }}</div>
60+
{% endif %}
61+
{% endif %}
62+
3463
<div class="separator separator-dashed my-9"></div>
3564

3665
<!-- Boolean operators -->
@@ -204,6 +233,22 @@
204233
});
205234
updateSum(cmp_ids, 'cmp_sum');
206235
}
236+
237+
// CTC type distribution — sum only the *enabled* (non-disabled) inputs
238+
// so the section stays coherent with step2's major/minor level toggles.
239+
if (document.getElementById('ctc_dist_sum')) {
240+
const ctc_ids = ['ctc_dist_boolean', 'ctc_dist_integer', 'ctc_dist_real', 'ctc_dist_string'];
241+
const activeCtc = () => ctc_ids.filter(id => {
242+
const el = document.getElementById(id);
243+
return el && !el.disabled;
244+
});
245+
const updateCtcSum = () => updateSum(activeCtc(), 'ctc_dist_sum');
246+
ctc_ids.forEach(id => {
247+
const el = document.getElementById(id);
248+
if (el) el.addEventListener('input', updateCtcSum);
249+
});
250+
updateCtcSum();
251+
}
207252
})();
208253
</script>
209254
{% endblock %}

app/modules/generator/templates/generator/step4.html

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "generator/index.html" %}
2-
{% from "generator/_macros.html" import info, section, range_field %}
2+
{% from "generator/_macros.html" import info, section, range_field, probability_field, sum_badge %}
33

44
{% block title %}Generate random feature models{% endblock %}
55

@@ -36,7 +36,32 @@
3636
{{ range_field('min_attributes', 'max_attributes', 'Number of attributes',
3737
'Min / Max number of attributes sampled per model when the random mode is on.',
3838
values, errors, default_min=1, default_max=5, hard_min=1, hard_max=10000) }}
39-
<div class="text-gray-600 fs-7">Min must be ≤ Max.</div>
39+
<div class="text-gray-600 fs-7 mb-5">Min must be ≤ Max.</div>
40+
41+
{{ section('Attribute type distribution',
42+
'When sampling each attribute, how likely is each UVL type to be chosen. Integer/Real are only active with the Arithmetic level; String is only active with the Type level. Active values must add up to 1.') }}
43+
44+
<div class="row g-3 mb-2">
45+
{{ probability_field('dist_boolean', 'Boolean',
46+
'Probability of a boolean attribute.',
47+
values, errors, default=0.7, symbol='B') }}
48+
{{ probability_field('dist_integer', 'Integer',
49+
'Probability of an integer attribute. Requires Arithmetic level.',
50+
values, errors, default=0.1, symbol='ℤ',
51+
disabled=(not arithmetic_level)) }}
52+
{{ probability_field('dist_real', 'Real',
53+
'Probability of a real-valued attribute. Requires Arithmetic level.',
54+
values, errors, default=0.1, symbol='ℝ',
55+
disabled=(not arithmetic_level)) }}
56+
{{ probability_field('dist_string', 'String',
57+
'Probability of a string attribute. Requires Type level.',
58+
values, errors, default=0.1, symbol='S',
59+
disabled=(not type_level)) }}
60+
</div>
61+
{{ sum_badge('attr_dist_sum', 'Sum of active type probabilities', initial=values.get('attr_dist_sum', '1.0000')) }}
62+
{% if errors.get('attr_dist_sum') %}
63+
<div class="text-danger fs-7 mt-1">{{ errors['attr_dist_sum'] }}</div>
64+
{% endif %}
4065
</div>
4166

4267
<!-- Manual mode -->
@@ -316,6 +341,33 @@ <h5 class="fw-bold text-gray-900 mb-0">Attribute #${idx + 1}</h5>
316341
}
317342
updateManualAttributesGlobalError();
318343

344+
// ─── Attribute-type distribution live sum ───────────────────────────────
345+
(function () {
346+
const badge = document.getElementById('attr_dist_sum');
347+
if (!badge) return;
348+
const active = ['dist_boolean']
349+
.concat(arithmeticLevelEnabled ? ['dist_integer', 'dist_real'] : [])
350+
.concat(typeLevelEnabled ? ['dist_string'] : []);
351+
function update() {
352+
let s = 0;
353+
active.forEach((id) => {
354+
const el = document.getElementById(id);
355+
if (el && !el.disabled) s += parseFloat(el.value) || 0;
356+
});
357+
badge.innerText = s.toFixed(4);
358+
badge.classList.remove('badge-light-success', 'badge-light-danger');
359+
badge.classList.add(Math.abs(s - 1.0) < 0.001 ? 'badge-light-success' : 'badge-light-danger');
360+
}
361+
active.forEach((id) => {
362+
const el = document.getElementById(id);
363+
if (el) {
364+
el.addEventListener('input', update);
365+
el.addEventListener('change', update);
366+
}
367+
});
368+
update();
369+
})();
370+
319371
function updateUseInConstraintsAvailability(card, type, idx) {
320372
const cb = card.querySelector(`[name="attr_use_in_constraints_${idx}"]`);
321373
const hint = card.querySelector('.attr-use-in-constraints-hint');

0 commit comments

Comments
 (0)