Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify slider values #63

Open
EwoutH opened this issue Jan 26, 2023 · 12 comments
Open

Modify slider values #63

EwoutH opened this issue Jan 26, 2023 · 12 comments

Comments

@EwoutH
Copy link
Contributor

EwoutH commented Jan 26, 2023

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
Copy link
Contributor Author

EwoutH commented Jan 26, 2023

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: #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
Copy link
Contributor Author

EwoutH commented Mar 7, 2023

@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
Copy link
Owner

quaquel commented Mar 19, 2023

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
Copy link
Contributor Author

EwoutH commented Mar 22, 2023

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
Copy link
Owner

quaquel commented Mar 22, 2023

How would you query these within NetLogo?

@EwoutH
Copy link
Contributor Author

EwoutH commented Mar 22, 2023

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
Copy link
Owner

quaquel commented Mar 22, 2023

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
Copy link
Contributor Author

EwoutH commented Mar 22, 2023

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

@quaquel
Copy link
Owner

quaquel commented Mar 22, 2023

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
Copy link
Contributor Author

EwoutH commented Mar 22, 2023

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.

  • Extending the NetLogoLink.java that uses the widgets API.
  • Writing a text scraper in Python that collectes the string data directly from the .nlogo file.

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

Java part
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<Map<String, Object>> getInputProperties() {
        List<Map<String, Object>> 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<String, Object> 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
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
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
Copy link
Contributor Author

EwoutH commented Mar 24, 2023

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
Copy link
Owner

quaquel commented Mar 24, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants