quaquel / pyNetLogo

BSD 3-Clause "New" or "Revised" License
81 stars 22 forks source link

Modify slider values #63

Open EwoutH opened 1 year ago

EwoutH commented 1 year ago

Is it possible to vary the values of sliders using pyNetLogo? If so how?

In the docs I can only find .write_NetLogo_attriblist(), but that seems to modify agent values.

EwoutH commented 1 year ago

So setting global variables, including sliders and switches is very easy with pyNetLogo. Figured it out in 5 minutes, I was looking for a separate function in pyNetLogo but you can just use the .command() function.

I thought let's update the tutorial quickly, but of course that came crashing down hard. So the rest of the hour was spent trying to fix that, unfortunately to moderate succes. I filed a bug here: https://github.com/quaquel/pyNetLogo/issues/65.

And of course no debug process is complete without an IDE bug and having to roll back PyCharm.

Anyway, the updated tutorial, which crashes halfway, is available here. The main takeaway is, for anyone finding this issue or wanting to use it as reference:

Setting global variables

To run scenarios or experiments, you can directly set NetLogo global variables (including sliders and switches) by using the .command() function. For example, you can modify the initial number of sheep by:

netlogo.command('set initial-number-sheep 50')

To set many initial variables at once, you can create a dictionary and loop through that using F-strings:

# Create a dictionary with variables and values to set
variable_dict = {
    "initial-number-sheep": 200,
    "initial-number-wolves": 75,
    "grass-regrowth-time": 20
}
# Loop through them using F-strings
for variable, value in variable_dict.items():
    netlogo.command(f'set {variable} {value}')
EwoutH commented 1 year ago

@quaquel I think this part can be a bit clearer in the tutorial. When #65 is fixed, I can open a PR to add it if you like.

quaquel commented 1 year ago

I fixed #65 and have already made a few tutorial updates to reflect this fix and other changes. Any additional suggestions for the tutorial are still very welcome.

EwoutH commented 1 year ago

Do you know if it's also possible to read-out current, minimum and maximum slider values? They are present in the model code, so it should be possible.

That would be a great feature, because then you can just ask to vary though the pre-defined ranges.

quaquel commented 1 year ago

How would you query these within NetLogo?

EwoutH commented 1 year ago

My approach would be to directly take them from the NetLogo model file itself, they are clearly in there:

SLIDER
16
602
245
635
average-parent-contacts-per-child
average-parent-contacts-per-child
0
10
10.0
1
1
NIL
HORIZONTAL

SLIDER
6
922
187
955
chance-of-moving-out
chance-of-moving-out
5
25
15.0
1
1
%
HORIZONTAL

Once you have those you can do all kind of things, filling a hypervolume, sensitive analysis, etc.

quaquel commented 1 year ago

So, as far as you know, there is no command or other way to get these numbers out? In that case, it will be hard to get them out to Python without writing a lot of additional novel java code.

EwoutH commented 1 year ago

No, but I also haven't really searched for it. We can also asked it on the NetLogo repo.

quaquel commented 1 year ago

The scope of pynetlogo, as with the mathematical link, is to send NetLogo commands to NetLogo and query reporters. So, if what you want can be done within this scope it is okay. My suggestion would indeed be to check the NetLogo repo.

EwoutH commented 1 year ago

I dove a bit deeper into this. The thing we need to read out are called the widgets. They are basically all interface elements, but also saved in the .nlogo file in plain text. So we can also interact with them in headless mode.

There are two general approaches to this.

The extending the NetLogoLink.java could look a bit like this (GPT generated, for inspiration):

Java part ```Java import org.nlogo.api.*; import org.nlogo.headless.HeadlessWorkspace; import org.nlogo.workspace.AbstractWorkspace; import org.nlogo.window.GUIWorkspace; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class NetLogoLink { private org.nlogo.workspace.Controllable workspace = null; private java.io.IOException caughtEx = null; private boolean isGUIworkspace; private static boolean blockExit = true; public NetLogoLink(Boolean isGUImode, Boolean is3d) { // Existing constructor implementation } // Other existing methods ... public List> getInputProperties() { List> inputProperties = new ArrayList<>(); LogoList widgets; try { widgets = (LogoList) workspace.report("widgets"); } catch (CompilerException | LogoException e) { e.printStackTrace(); return inputProperties; } for (Object widget : widgets) { String[] lines = widget.toString().split("\n"); String widgetType = lines[0]; Map properties = new HashMap<>(); if ("SLIDER".equals(widgetType)) { String name = lines[1]; double min = Double.parseDouble(lines[6]); double max = Double.parseDouble(lines[7]); double value = Double.parseDouble(lines[8]); double increment = Double.parseDouble(lines[9]); properties.put("type", "SLIDER"); properties.put("name", name); properties.put("min", min); properties.put("max", max); properties.put("value", value); properties.put("increment", increment); } else if ("CHOOSER".equals(widgetType)) { String name = lines[1]; String[] choices = lines[6].split(" "); String value = lines[7]; properties.put("type", "CHOOSER"); properties.put("name", name); properties.put("choices", choices); properties.put("value", value); } else if ("SWITCH".equals(widgetType)) { String name = lines[1]; boolean value = Boolean.parseBoolean(lines[6]); properties.put("type", "SWITCH"); properties.put("name", name); properties.put("value", value); } else { continue; } inputProperties.add(properties); } return inputProperties; } } ```
Python part ```Python import pandas as pd from jnius import autoclass class NetLogoLink: def __init__(self, isGUImode=True, is3d=False): # Existing constructor implementation # Other existing methods ... def get_input_properties(self): JavaList = autoclass('java.util.ArrayList') JavaMap = autoclass('java.util.HashMap') java_input_properties = self.workspace.getInputProperties() input_properties_list = [] for i in range(java_input_properties.size()): java_map = java_input_properties.get(i) properties = {} for key in java_map.keySet(): value = java_map.get(key) if key == "choices": value = [value.get(i) for i in range(value.size())] properties[key] = value input_properties_list.append(properties) input_properties_df = pd.DataFrame(input_properties_list) return input_properties_df ```

And the pure-Python implementation more like this:

Python implantation ```Python import re import pandas as pd class NetLogoLink: def __init__(self, model_file=None): self.model_file = model_file def load_model(self, model_file): self.model_file = model_file def get_input_properties_pure_python(self): input_properties_list = [] if not self.model_file: raise ValueError("Model file not loaded. Call load_model() first.") with open(model_file, 'r') as file: content = file.read() slider_pattern = re.compile(r'SLIDER\n([^@]+)') sliders = slider_pattern.findall(content) for slider in sliders: lines = slider.split('\n') properties = { 'type': 'SLIDER', 'name': lines[0], 'min': float(lines[5]), 'max': float(lines[6]), 'value': float(lines[7]), 'increment': float(lines[8]), } input_properties_list.append(properties) chooser_pattern = re.compile(r'CHOOSER\n([^@]+)') choosers = chooser_pattern.findall(content) for chooser in choosers: lines = chooser.split('\n') properties = { 'type': 'CHOOSER', 'name': lines[0], 'choices': lines[5].split(' '), 'value': lines[6], } input_properties_list.append(properties) switch_pattern = re.compile(r'SWITCH\n([^@]+)') switches = switch_pattern.findall(content) for switch in switches: lines = switch.split('\n') properties = { 'type': 'SWITCH', 'name': lines[0], 'value': lines[5].lower() == 'true', } input_properties_list.append(properties) input_properties_df = pd.DataFrame(input_properties_list) return input_properties_df ```

Another thing I would like to implement is a collect_monitor_values() functions, which returns a dictionary with all the values from monitors. That way you can easily collect what you are also observing in the model itself.

The goal from both is to reduce Python code and leave as much of the stuff in NetLogo itself. Given that, you test you NetLogo model with a few lines of code, and if you want to adjust values, you can just start with get_input_properties() and go from there.

Which approach do you prefer?

EwoutH commented 1 year ago

One other thing I thought of is that counters are not readable from the NetLogo file in runtime. On the other hand, the counter variable names (reporters) are, so we could add them as reporters automatically of course.

So when choosing an approach, it probably depends on how far we want to scale this.

A pure-Python scraping approach would be feasible to implement for my by myself, a hybrid Java-Python one using the widgets API I can do the Python part.

quaquel commented 1 year ago

I am inclined to take the hybrid java route. I expect this to be more robust in the long run.