text:
# 3D Tic Tac Toe/Connect Four game with AI

I came up with this during English lessons many years ago to kill some time, and just decided to try code it. It's basically a 3D version of Connect Four/Noughts and Crosses/Tic Tac Toe, but the grid can also flip itself, which means the people good at spacial awareness have a chance against people good at planning ahead.

Here's the general concept of how it works: it supposed to be 4x4x4 (coded to allow other sizes, though be warned an odd number gives player 1 a huge advantage), and you can get a point from making any complete row in any direction. The game ends once all available spaces are taken, and obviously the player with the most points wins.

A quick example with paint showing where the points are mid-game:

Anyway, I'm not sure how to do user interfaces yet, so it's literally just a printed output and a raw_input to choose where to go (works with coordinates or the cell ID, eg. first cell is (1,1,1) or 0), which admittedly doesn't make it very easy to play. Ideally it should be colour coded and you just click where to go.

You can run the game by using Connect3D().play(), though it'd be safer to do something like c3d = Connect3D, then c3d.play(), so you can resume a game if you quit it (for now, quit by hitting escape or cancel and it'll throw an error, I'll sort this out later).

The grid_data is stored as a 1D list where a bit of maths is used to calculate how to go each direction. To make sure it doesn't loop to the other side, the edge points of each direction are stored, so if any edge point is met, it knows it's reached the end of the line, and will look in the opposite direction. I know I haven't explained that well, but for example. with the above picture, for the direction up, the edges would be 0-15.

I'm aware a 3D list would have been much easier, but I quite liked the idea of storing it in a 1D list so that it'd be easy to save and recreate the grid with a single string.

Using the above picture, here's how the game would look in its current state. I'd missed one the line going from 2,2,1 to 2,2,4 in the picture, which is why the scores don't match.

I'm trying to get better at proper formatting and comments and stuff, so I'm mostly looking for feedback on what I'm still not doing so well. That said, any suggestions on bits I could have coded better or optimised are also welcome.

Player X: ////  Player O: /////
________________
/ X /   /   / O /|
/___/___/___/___/ |
/ O / X /   / X /  |
/___/___/___/___/   |
/   / O / X / O /    |
/___/___/___/___/     |
/   /   /   / O /      |
/___/___/___/___/       |
|       |________|_______|
|       /   /   /|O /   /|
|      /___/___/_|_/___/ |
|     / X / X / O|/ X /  |
|    /___/___/___|___/   |
|   /   / O / O /|  /    |
|  /___/___/___/_|_/     |
| /   /   / O /  |/      |
|/___/___/___/___|       |
|       |________|_______|
|       / X / O /|  /   /|
|      /___/___/_|_/___/ |
|     / X / X / O|/   /  |
|    /___/___/___|___/   |
|   /   / O / X /|  /    |
|  /___/___/___/_|_/     |
| /   /   /   / X|/      |
|/___/___/___/___|       |
|       |________|_______|
|       / O /   /|  /   /
|      /___/___/_|_/___/
|     / X / X / X|/ X /
|    /___/___/___|___/
|   / O / O /   /|  /
|  /___/___/___/_|_/
| / O /   /   / X|/
|/___/___/___/___|
Player O's turn...

Which can be continued by using:

Connect3D(_raw_data='X  OOX X OXO   O  O XXOX OO   O XO  XXO  OX    XO   XXXXOO  O  X.1').play()

The code is here (change both values at the bottom to false to see the computer players face off):

import itertools
import operator
import random
import time
from collections import defaultdict
class Connect3DError(Exception):
pass

class Connect3D(object):
"""Class to store the Connect3D game data.
The data is stored in a 1D list, but is converted to a 3D representation for the user.
"""
player_symbols = 'XO'
grid_size_recommended = 4

def __init__(self, grid_size=grid_size_recommended):
"""Set up the grid and which player goes first.

Parameters:
grid_size (int): How long each side of the grid should be.
The game works best with even numbers, 4 is recommended.
"""

try:
self.current_player
except AttributeError:
self.current_player = random.randint(0, 1)

self._display_score = False

try:
self.grid_size = int(grid_size)
except TypeError:
raise TypeError('grid_size must be an integer')
self.grid_data = ['' for i in range(pow(grid_size, 3))]

self.grid_size_squared = pow(self.grid_size, 2)

#Calculate the edge numbers for each direction
self.direction_edges = {}
self.direction_edges['U'] = range(self.grid_size_squared)
self.direction_edges['D'] = range(self.grid_size_squared*(self.grid_size-1), self.grid_size_squared*self.grid_size)
self.direction_edges['R'] = [i*self.grid_size+self.grid_size-1 for i in range(self.grid_size_squared)]
self.direction_edges['L'] = [i*self.grid_size for i in range(self.grid_size_squared)]
self.direction_edges['F'] = [i*self.grid_size_squared+j+self.grid_size_squared-self.grid_size for i in range(self.grid_size) for j in range(self.grid_size)]
self.direction_edges['B'] = [i*self.grid_size_squared+j for i in range(self.grid_size) for j in range(self.grid_size)]
self.direction_edges[' '] = []

#Calculate the addition needed to move in each direction
self.direction_maths = {}
self.direction_maths['D'] = self.grid_size_squared
self.direction_maths['R'] = 1
self.direction_maths['F'] = self.grid_size
self.direction_maths['U'] = -self.direction_maths['D']
self.direction_maths['L'] = -self.direction_maths['R']
self.direction_maths['B'] = -self.direction_maths['F']
self.direction_maths[' '] = 0

def __repr__(self):
"""Format the data to allow it to be imported again as a new object."""
grid_data_joined = ''.join(str(i).ljust(1) for i in self.grid_data)
return "Connect3D.from_string('{}.{}')".format(grid_data_joined, self.current_player)

def __str__(self):
"""Use the grid_data to output a grid of the correct size.
Each value in grid_data must be 1 character or formatting will be wrong.

>>> grid_data = range(8)

>>> print Connect3D.from_string(''.join(str(i) if i != '' else ' ' for i in grid_data))
________
/ 0 / 1 /|
/___/___/ |
/ 2 / 3 /  |
/___/___/   |
|   |____|___|
|   / 4 /|5 /
|  /___/_|_/
| / 6 / 7|/
|/___/___|

"""
k = 0

grid_range = range(self.grid_size)
grid_output = []

if self._display_score:
grid_output.append(self.show_score())

for j in grid_range:

row_top = ' '*(self.grid_size*2+1) + '_'*(self.grid_size*4)
if j:
row_top = '|' + row_top[:self.grid_size*2-1] + '|' + '_'*(self.grid_size*2) + '|' + '_'*(self.grid_size*2-1) + '|'
grid_output.append(row_top)

for i in grid_range:
row_display = ' '*(self.grid_size*2-i*2) + '/' + ''.join((' ' + str(self.grid_data[k+x]).ljust(1) + ' /') for x in grid_range)
k += self.grid_size
row_bottom = ' '*(self.grid_size*2-i*2-1) + '/' + '___/'*self.grid_size

if j != grid_range[-1]:
row_display += ' '*(i*2) + '|'
row_bottom += ' '*(i*2+1) + '|'
if j:
row_display = row_display[:self.grid_size*4+1] + '|' + row_display[self.grid_size*4+2:]
row_bottom = row_bottom[:self.grid_size*4+1] + '|' + row_bottom[self.grid_size*4+2:]

row_display = '|' + row_display[1:]
row_bottom = '|' + row_bottom[1:]

grid_output += [row_display, row_bottom]

return '\n'.join(grid_output)

def _get_winning_player(self):
"""Return a list of the player(s) with the highest points.

>>> C3D = Connect3D()
>>> C3D.update_score()

When X has a higher score.
>>> C3D.current_points['X'] = 5
>>> C3D.current_points['O'] = 1
>>> C3D._get_winning_player()
['X']

When both scores are the same.
>>> C3D.current_points['O'] = 5
>>> C3D._get_winning_player()
['O', 'X']

When there are no winners.
>>> C3D = Connect3D()
>>> C3D.update_score()
>>> C3D._get_winning_player()
[]
"""
self.update_score()
return get_max_dict_keys(self.current_points)

@classmethod
def from_string(cls, raw_data):
"""Create new Connect3D instance from a string.

Parameters:
raw_data (str): Passed in from __repr__,
contains the grid data and current player.
Will still work if no player is defined.
Format: "joined(grid_data).current_player"
"""
split_data = raw_data.split('.')
grid_data = [i if i != ' ' else '' for i in split_data[0]]
new_c3d_instance = cls(calculate_grid_size(grid_data))

new_c3d_instance.grid_data = grid_data

if len(split_data) > 1:
new_c3d_instance.current_player = split_data[1]

return new_c3d_instance

@classmethod
def from_list(cls, grid_data, player=None):
"""Create new Connect3D instance from a list.

Parameters:
grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.

player (int or None): Current player to continue the game with.
"""
new_c3d_instance = cls(calculate_grid_size(grid_data))

new_c3d_instance.grid_data = [i if i != ' ' else '' for i in grid_data]

if player is not None:
new_c3d_instance.current_player = player

return new_c3d_instance

def play(self, player1=True, player2=False, grid_shuffle_chance=None):
"""Start or continue a game.
If using computer players, there is a minimum time delay to avoid it instantly making moves.

Parameters:
player1 (bool): If player 1 is a human player.

player2 (bool): If player 2 is a human player.

grid_shuffle_chance (int, float or None, optional): Percentage chance to shuffle
the grid after each turn.
Reverts to the default chance if left as None.
"""

self.current_player = int(not self.current_player)
min_time_update = 0.1

#Game loop
while True:

current_time = time.time()

#Switch current player
self.current_player = int(not self.current_player)

was_flipped = self.shuffle(chance=grid_shuffle_chance)

#Display score and grid
self.update_score()
self._display_score = True
print self
self._display_score = False

if was_flipped:
print "Grid was flipped!"

#Check if no spaces are left
if '' not in self.grid_data:
winning_player = self._get_winning_player()
if len(winning_player) == 1:
print 'Player {} won!'.format(winning_player[0])
else:
print 'The game was a draw!'

#Ask to play again and check if answer is a variant of 'yes' or 'ok'
print 'Play again?'
play_again = raw_input().lower()
if any(i in play_again for i in ('y', 'k')):
self.reset()
else:
return
break

#Player takes a move, function returns True if it updates the grid, otherwise loop again
print "Player {}'s turn...".format(self.player_symbols[self.current_player])
if (player1 and not self.current_player) or (player2 and self.current_player):
while not self.make_move(self.player_symbols[self.current_player], raw_input().replace(',', ' ').replace('.', ' ').split()):
print "Grid cell is not available, try again."
else:
ai_go = SimpleC3DAI(self, self.current_player).calculate_next_move()
if not self.make_move(self.player_symbols[self.current_player], ai_go):
raise Connect3DError('Something unknown went wrong with the AI')
else:
print "AI moved to point {}.".format(PointConversion(self.grid_size, ai_go).to_3d())

#Wait a short while
time.sleep(max(0, min_time_update, time.time()-current_time))
print

def make_move(self, id, *args):
"""Update the grid data with a new move.

Parameters:
id (str): Character to write into the grid.

args (int, tuple or list): Where in the grid to place the ID.
Can be input as an integer (grid cell number), 3 integers,
a tuple or list (3D coordinates)

>>> C3D = Connect3D(2)

>>> C3D.make_move('a', 1)
True
>>> C3D.make_move('b', 1)
False
>>> C3D.make_move('c', -1)
False
>>> C3D.make_move('d', 2, 2, 2)
True
>>> C3D.make_move('e', [1, 1, 2])
True
>>> C3D.make_move('f', (1, 1, 3))
False

>>> C3D.grid_data
['', 'a', '', '', 'e', '', '', 'd']
>>> print C3D
________
/   / a /|
/___/___/ |
/   /   /  |
/___/___/   |
|   |____|___|
|   / e /|  /
|  /___/_|_/
| /   / d|/
|/___/___|
"""

#Convert points to the grid cell ID
if len(args) == 1:
if not str(args[0]).replace('-','').isdigit():
if len(args[0]) == 1:
try:
i = int(args[0][0])
except ValueError:
return False
else:
i = PointConversion(self.grid_size, args[0]).to_int()
else:
i = int(args[0])
else:
i = PointConversion(self.grid_size, tuple(args)).to_int()

#Add to grid if cell is empty
if 0 <= i <len(self.grid_data) and not self.grid_data[i] and i is not None:
self.grid_data[i] = id
return True
else:
return False

def shuffle(self, chance=None, second_chance=None, repeats=None, no_shuffle=[]):
"""Mirror the grid in the X, Y, or Z axis.

Each time one of the directions is flipped, there is a 50% chance of it happening again.
This means it has the same overall chance to flip, but it is not limited to a single axis.

Parameters:
chance:
Percent chance of a flip happening.
Default: 10
Type: int/float

second_chance:
Percent chance of subsequent flips happening after the first.
Default: 50
Type: int/float

repeats:
Number of attempts to flip at the above chance.
Default: 3
Type: int

no_shuffle:
List of directions already flipped so it won't reverse anything.
Type: list
"""
#Set defaults
if chance is None:
chance = 33
if second_chance is None:
second_chance = 50
if repeats is None:
repeats = 3

#Calculate range of random numbers
chance = min(100, chance)
if chance > 0:
chance = int(round(400/chance))-1

#Attempt to flip grid
for i in range(repeats):
shuffle_num = random.randint(0, chance)
if shuffle_num in (0, 1, 2, 3) and shuffle_num not in no_shuffle:
no_shuffle.append(shuffle_num)
if shuffle_num == 0:
self.grid_data = SwapGridData(self.grid_data).x()
if shuffle_num == 1:
self.grid_data = SwapGridData(self.grid_data).y()
if shuffle_num == 2:
self.grid_data = SwapGridData(self.grid_data).z()
if shuffle_num == 3:
self.grid_data = SwapGridData(self.grid_data).reverse()
if self.shuffle(chance=second_chance, no_shuffle=no_shuffle) or not not no_shuffle:
return True

def update_score(self):
"""Recalculate the score.

There are 26 total directions from each point, or 13 lines, calculated in
the DirectionCalculation() class. For each of the 13 lines, look both ways
and count the number of values that match the current player.

This will find any matches from one point, so it's simple to then iterate
through every point. A hash of each line is stored to avoid duplicates.
"""

try:
self.grid_data_last_updated
except AttributeError:
self.grid_data_last_updated = None

if self.grid_data_last_updated != hash(tuple(self.grid_data)):

#Store hash of grid_data in it's current state to avoid unnecessarily running the code again when there's been no changes
self.grid_data_last_updated = hash(tuple(self.grid_data))

self.current_points = defaultdict(int)
all_matches = set()

#Loop through each point
for starting_point in range(len(self.grid_data)):

current_player = self.grid_data[starting_point]

if current_player:

for i in DirectionCalculation().opposite_direction:

#Get a list of directions and calculate movement amount
possible_directions = [list(i)]
possible_directions += [[j.replace(i, '') for i in possible_directions[0] for j in DirectionCalculation().direction_group.values() if i in j]]
direction_movement = sum(self.direction_maths[j] for j in possible_directions[0])

#Build list of invalid directions
invalid_directions = [[self.direction_edges[j] for j in possible_directions[k]] for k in (0, 1)]
invalid_directions = [join_list(j) for j in invalid_directions]

num_matches = 1
list_match = [starting_point]

#Use two loops for the opposite directions
for j in (0, 1):

current_point = starting_point

while current_point not in invalid_directions[j] and 0 < current_point < len(self.grid_data):
current_point += direction_movement * int('-'[:j] + '1')
if self.grid_data[current_point] == current_player:
num_matches += 1
list_match.append(current_point)
else:
break

#Add a point if enough matches
if num_matches == self.grid_size:

list_match = hash(tuple(sorted(list_match)))
if list_match not in all_matches:
all_matches.add(list_match)
self.current_points[current_player] += 1

def show_score(self, digits=False, marker='/'):
"""Print the current points.

Parameters:
digits (bool, optional): If the score should be output as a number,
or as individual marks.

marker (str, optional): How each point should be displayed if
digits are not being used.

>>> C3D = Connect3D()
>>> C3D.update_score()
>>> C3D.current_points['X'] = 5
>>> C3D.current_points['O'] = 1

>>> C3D.show_score(False, '/')
'Player X: /////  Player O: /'
>>> C3D.show_score(True)
'Player X: 5  Player O: 1'
"""
self.update_score()
multiply_value = 1 if digits else marker
return 'Player X: {x}  Player O: {o}'.format(x=multiply_value*(self.current_points['X']), o=multiply_value*self.current_points['O'])

def reset(self):
"""Empty the grid without creating a new Connect3D object."""
self.grid_data = ['' for i in range(pow(self.grid_size, 3))]

class DirectionCalculation(object):
"""Calculate which directions are possible to move in, based on the 6 directions.
Any combination is fine, as long as it doesn't go back on itself, hence why X, Y
and Z have been given two values each, as opposed to just using six values.

Because the code to calculate score will look in one direction then reverse it,
the list then needs to be trimmed down to remove any duplicate directions (eg.
up/down and upright/downleft are both duplicates)

The code will output the following results, it is possible to use these instead of the class.
direction_group = {'Y': 'UD', 'X': 'LR', 'Z': 'FB', ' ': ' '}
opposite_direction = ('B', 'D', 'DF', 'LDB', 'DB', 'L', 'LUB', 'LUF', 'LF', 'RU', 'LB', 'LDF', 'RD')
"""

direction_group = {}
direction_group['X'] = 'LR'
direction_group['Y'] = 'UD'
direction_group['Z'] = 'FB'
direction_group[' '] = ' '

#Come up with all possible directions
all_directions = set()
for x in [' ', 'X']:
for y in [' ', 'Y']:
for z in [' ', 'Z']:
x_directions = list(direction_group[x])
y_directions = list(direction_group[y])
z_directions = list(direction_group[z])
for i in x_directions:
for j in y_directions:
for k in z_directions:
all_directions.add((i+j+k).replace(' ', ''))

#Narrow list down to remove any opposite directions
opposite_direction = all_directions.copy()
for i in all_directions:
if i in opposite_direction:
new_direction = ''
for j in list(i):
for k in direction_group.values():
if j in k:
new_direction += k.replace(j, '')
opposite_direction.remove(new_direction)

class PointConversion(object):
"""Used to convert the cell ID to 3D coordinates or vice versa.
Mainly used for inputting the coordinates to make a move.

The cell ID is from 0 to grid_size^3, and coordinates are from 1 to grid_size.
This means an ID of 0 is actually (1,1,1), and 3 would be (4,1,1).

- X -
__1___2_
/  1/ 0 / 1 /|
Y   /___/___/ |
/  2/ 2 / 3 /  |
/___/___/   |
|   |____|___|
| 1|   / 4 /|5 /
Z  |  /___/_|_/
| 2| / 6 / 7|/
|/___/___|

Parameters:
grid_size:
Size of the grid.
Type: int

i:
Cell ID or coordinates.
Type int/tuple/list

Functions:
to_3d
to_int
"""
def __init__(self, grid_size, i):
self.grid_size = grid_size
self.i = i

def to_3d(self):
"""Convert cell ID to a 3D coordinate.

>>> grid_size = 4
>>> cell_id = 16

>>> PointConversion(grid_size, cell_id).to_3d()
(1, 1, 2)
"""
cell_id = int(self.i)
z = cell_id / pow(self.grid_size, 2)
cell_id %= pow(self.grid_size, 2)
y = cell_id / self.grid_size
x = cell_id % self.grid_size
return tuple(cell_id+1 for cell_id in (x, y, z))

def to_int(self):
"""Convert 3D coordinates to the cell ID.

>>> grid_size = 4
>>> coordinates = (4,2,3)

>>> PointConversion(grid_size, coordinates).to_int()
39
"""
x, y, z = [int(i) for i in self.i]
if all(i > 0 for i in (x, y, z)):
return (x-1)*pow(self.grid_size, 0) + (y-1)*pow(self.grid_size, 1) + (z-1)*pow(self.grid_size, 2)
return None

class SwapGridData(object):
"""Use the size of the grid to calculate how flip it on the X, Y, or Z axis.
The flips keep the grid intact but change the perspective of the game.

Parameters:
grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.
"""
def __init__(self, grid_data):
self.grid_data = list(grid_data)
self.grid_size = calculate_grid_size(self.grid_data)

def x(self):
"""Flip on the X axis.

>>> SwapGridData(range(8)).x()
[1, 0, 3, 2, 5, 4, 7, 6]
>>> print Connect3D.from_list(SwapGridData(range(8)).x())
________
/ 1 / 0 /|
/___/___/ |
/ 3 / 2 /  |
/___/___/   |
|   |____|___|
|   / 5 /|4 /
|  /___/_|_/
| / 7 / 6|/
|/___/___|
"""
return join_list(x[::-1] for x in split_list(self.grid_data, self.grid_size))

def y(self):
"""Flip on the Y axis.

>>> SwapGridData(range(8)).y()
[2, 3, 0, 1, 6, 7, 4, 5]
>>> print Connect3D.from_list(SwapGridData(range(8)).y())
________
/ 2 / 3 /|
/___/___/ |
/ 0 / 1 /  |
/___/___/   |
|   |____|___|
|   / 6 /|7 /
|  /___/_|_/
| / 4 / 5|/
|/___/___|
"""
group_split = split_list(self.grid_data, pow(self.grid_size, 2))
return join_list(join_list(split_list(x, self.grid_size)[::-1]) for x in group_split)

def z(self):
"""Flip on the Z axis.

>>> SwapGridData(range(8)).z()
[4, 5, 6, 7, 0, 1, 2, 3]
>>> print Connect3D.from_list(SwapGridData(range(8)).z())
________
/ 4 / 5 /|
/___/___/ |
/ 6 / 7 /  |
/___/___/   |
|   |____|___|
|   / 0 /|1 /
|  /___/_|_/
| / 2 / 3|/
|/___/___|
"""
return join_list(split_list(self.grid_data, pow(self.grid_size, 2))[::-1])

def reverse(self):
"""Reverse the grid.

>>> SwapGridData(range(8)).reverse()
[7, 6, 5, 4, 3, 2, 1, 0]
>>> print Connect3D.from_list(SwapGridData(range(8)).reverse())
________
/ 7 / 6 /|
/___/___/ |
/ 5 / 4 /  |
/___/___/   |
|   |____|___|
|   / 3 /|2 /
|  /___/_|_/
| / 1 / 0|/
|/___/___|
"""
return self.grid_data[::-1]

def calculate_grid_size(grid_data):
"""Cube root the length of grid_data to find the grid size."""
return int(round(pow(len(grid_data), 1.0/3.0), 0))

def split_list(x, n):
"""Split a list by n characters."""
n = int(n)
return [x[i:i+n] for i in range(0, len(x), n)]

def join_list(x):
"""Convert nested lists into one single list."""
return [j for i in x for j in i]

def get_max_dict_keys(x):
"""Return a list of every key containing the max value.

Parameters:
x (dict): Dictionary to sort and get highest value.
It must be a dictionary of integers to work properly.
"""
if x:
sorted_dict = sorted(x.iteritems(), key=operator.itemgetter(1), reverse=True)
if sorted_dict[0][1]:
return sorted([k for k, v in x.iteritems() if v == sorted_dict[0][1]])
return []

class SimpleC3DAI(object):
"""AI coded to play Connect3D."""

def __init__(self, C3DObject, player_num):
"""Set up the AI for a single move using the current state of Connect3D."""
self.C3DObject = C3DObject
self.player_num = player_num
self.player = self.C3DObject.player_symbols[self.player_num]
self.enemy = self.C3DObject.player_symbols[int(not self.player_num)]
self.gd_len = len(self.C3DObject.grid_data)

def max_cell_points(self):
"""Get maximum number of points that can be gained from each empty cell,
that is not blocked by an enemy value.
"""
max_points = defaultdict(int)
filled_grid_data = [i if i else self.player for i in self.C3DObject.grid_data]
for cell_id in range(self.gd_len):
if cell_id == self.player:
max_points[cell_id] += self.check_grid(filled_grid_data, cell_id, self.player)
return get_max_dict_keys(max_points)

def check_for_n_minus_one(self, grid_data=None):
"""Find all places where anyone has n-1 points in a row, by substituting
in a point for each player in every cell.

Parameters:
grid_data (list or None, optional): Pass in a custom grid_data,
leave as None to use the Connect3D one.
"""
if grid_data is None:
grid_data = list(self.C3DObject.grid_data)

matches = defaultdict(list)
for cell_id in range(len(grid_data)):
if not grid_data[cell_id]:
for current_player in (self.player, self.enemy):
if self.check_grid(grid_data, cell_id, current_player):
matches[current_player].append(cell_id)
return matches

def look_ahead(self):
"""Look two moves ahead to detect if someone could get a point.
Uses the check_for_n_minus_one function from within a loop.

Will return 1 as the second parameter if it has looked up more than a single move.
"""
#Try initial check
match = self.check_for_n_minus_one()
if match:
return (match, 0)

#For every grid cell, substitute a player into it, then do the check again
grid_data = list(self.C3DObject.grid_data)
for i in range(self.gd_len):
if not self.C3DObject.grid_data[i]:
old_value = grid_data[i]
for current_player in (self.player, self.enemy):
grid_data[i] = current_player
match = self.check_for_n_minus_one(grid_data)
if match:
return (match, 1)
grid_data[i] = old_value

return (defaultdict(list), 0)

def check_grid(self, grid_data, cell_id, player):
"""Duplicate of the Connect3D.update_score method, but set up to check individual cells.

Parameters:
grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.

cell_id (int): The cell ID, or grid_data index to update.

player (int): Integer representation of the player, can be 0 or 1.
"""
max_points = 0
for i in DirectionCalculation().opposite_direction:

#Get a list of directions and calculate movement amount
possible_directions = [list(i)]
possible_directions += [[j.replace(i, '') for i in possible_directions[0] for j in DirectionCalculation().direction_group.values() if i in j]]
direction_movement = sum(self.C3DObject.direction_maths[j] for j in possible_directions[0])

#Build list of invalid directions
invalid_directions = [[self.C3DObject.direction_edges[j] for j in possible_directions[k]] for k in (0, 1)]
invalid_directions = [join_list(j) for j in invalid_directions]

num_matches = 1

#Use two loops for the opposite directions
for j in (0, 1):

current_point = cell_id

while current_point not in invalid_directions[j] and 0 < current_point < len(grid_data):
current_point += direction_movement * int('-'[:j] + '1')
if grid_data[current_point] == player:
num_matches += 1
else:
break

#Add a point if enough matches
if num_matches == self.C3DObject.grid_size:
max_points += 1

return max_points

def calculate_next_move(self):
"""Groups together the AI methods in order of importance.
Will throw an error if grid_data is full, since the game should have ended by then anyway.
"""

next_moves = []

if len(''.join(self.C3DObject.grid_data)) > (self.C3DObject.grid_size-2) * 2:

point_based_move, far_away = SimpleC3DAI(self.C3DObject, self.player_num).look_ahead()
order_of_importance = [self.enemy, self.player][::int('-'[:int(far_away)]+'1')]
grid_data_len = len(''.join(self.C3DObject.grid_data))

state = None

if point_based_move:
if point_based_move[self.enemy]:
next_moves = point_based_move[self.enemy]
state = 'Blocking opposing player'

elif point_based_move[self.player]:
next_moves = point_based_move[self.player]
state = 'Gaining points'

else:
next_moves = self.max_cell_points()
state = 'Random placement'

if not next_moves:
next_moves = [i for i in range(self.gd_len) if not self.C3DObject.grid_data[i]]
if state is None:
state = 'Struggling'

else:
next_moves = [i for i in range(self.gd_len) if not self.C3DObject.grid_data[i]]
state = 'Starting'

print 'AI Objective: ' + state + '.'

return random.choice(next_moves)

if __name__ == '__main__':
c3d = Connect3D()
c3d.play(True, False)

The AI works in a fairly simple way, and will check certain things in order of importance.

First off, if the opposing player has 3 in a row, that is the most urgent thing to stop. Then if the AI has any 3 of it's own points in a row, it'll complete the row.

If there are no rows of 3, the AI will then check if there's any rows of 2 that either it, or the opposing player can score a point from. Because this is less urgent, the AI will aim to complete its own row first before blocking the other player.

If there are literally no good moves, it will look at each empty cell and calculate the maximum number of rows that can cross through each one (taking into account the current points in the grid). It'll then pick one of the cells with the highest possible rows.

Finally, if even that fails, such as if there are spaces but no more points to be gained, it'll just place a point anywhere.

It's currently a little impossible to beat, even without the grid flipping, so I'm thinking of implementing a percentage chance where it might not notice a row, which will make it feel a little more human. If anyone would like to test what the sweet spot would be with that, please let me know.

## Style

Python has an official style guide, which you should read and follow. Some highlights:

• Import statements should be one module per line, in alphabetical order:

from collections import defaultdict
import itertools
import operator
import random

• Whitespace should be more consistent (e.g. a single blank line between method definitions, spaces after commas, spaces around operators).

• Lines should be 80 characters or fewer in length (and docstrings should be <= 72).

• Function names should be lowercase_with_underscores (i.e. draw_grid not DrawGrid) - and again, consistency is important.

## Documentation

Listing classes and functions (also methods, which you also refer to as functions) in the module docstring and methods in the class docstrings is:

1. Duplicated information; and
2. Redundant, as this basic data is available from e.g. dir(module) or dir(Class).

Instead, the docstrings should provide information that cannot simply be determined by introspection, like the purpose of the module/class; your method docstrings are pretty good, for example. I would be inclined to use an existing format rather than inventing my own, as this makes it easier to use tools like Sphinx to automatically generate HTML/PDF docs from the docstrings. I like the Google style:

def __init__(self, grid_size=4, _raw_data=None):
"""Set up the grid and which player goes first.

Parameters:
grid_size (int, optional): How long each side of the grid should
be. The game works best with even numbers, 4 is the default and
recommended.

_raw_data (str or None, optional): Passed in from __repr__,
contains the grid data and current player.
Format: 'joined(grid_data).current_player'

...

"""

Having test examples in the docstrings is great; I'd be inclined to add:

if __name__ == '__main__':
import doctest
doctest.testmod()

at the end of the script to run them if the file script is executed directly.

## Functionality

• It's not clear why DirectionCalculation, PointConversion and SwapGridData are separate classes; all contain functionality that really belongs to Connect3D, although this would make it quite a long class.

• Implementing DrawGrid as a separate function seems like a very odd choice; the fact that calling this is all that Connect3D.draw does is a bit of a red flag. I would have been inclined to make that the __str__ implementation, so you can simply print c3d rather than print c3d.draw().

• All of the switching to and from strings seems redundant - pick a canonical representation of your data and stick with it.

• It's odd to see a "private by convention" single-leading-underscore parameter like _raw_data. Is the user not supposed to provide it? In general, I would implement a from_string @classmethod to create a new instance from a string representation (see example below), rather than make it an optional __init__ argument.

• Python is dynamically typed, but it still seems a bit odd for player_symbols to be "str/list/tuple/int". Really, you only care if it's an iterable of two characters, so you could do:

player_one, player_two = player_symbols  # ValueError if not two characters
if player_one == player_two:
raise ValueError('symbols must be different')

If the user wants to pass in e.g. 12 for '1' and '2', let them do that conversion themselves before calling your method. Also, note that you calculate len(set(player_symbols)) == 1 twice, rather than reusing same_symbols.

How the from_string might work:

class Connect3D(object):

def __init__(self, grid_size):
self.grid_data = ['' for i in range(pow(grid_size, 3))]
self.current_player = 0
...  # carry out rest of grid_size-based setup

@classmethod
def from_string(cls, raw_data):
grid_data, current_player = raw_data.split('.')
grid_data = [i if i != ' ' else '' for i in grid_data]
grid = cls(calculate_grid_size(grid_data))  # create new instance
...  # populate the instance with grid_data and current_player
return grid  # return new instance

Now Connect3D.from_string(...) will create a new instance, populate it from the raw data and return it for use.

• Thanks a lot for the tips, I'm just trying to get some implemented now so I'll comment as I go along. DirectionCalculation literally only needs to be run once per code, and since PointConversion and SwapGridData all do specific things, I guessed they'd be better as separate classes. DrawGrid (admittedly I shouldn't have used capitals) was kind of meant to be a temporary thing with there being no UI, though I hadn't thought about using __str__, that seems a much better idea. I really like the from_string idea, though I'm kinda having a problem getting it working properly. Sep 3 '15 at 11:06
• direction_edges/maths needs to be calculated once per grid_size, which was done in __init__. With your from_string idea, I'd need to calculate these again, but I don't want to copy and paste the code. I tried moving them into a new function called _init_after, and ran that from both __init__ and from_string, but I got the error message unbound method _init_after() must be called with Connect3D instance as first argument (got nothing instead). Would you happen to know how to fix it? I tried adding that as a classmethod but it didn't receive any values from self. Sep 3 '15 at 11:11
• @Peter I've added an example of the sort of thing I mean - @classmethod is the canonical way to create an alternate constructor in Python. Sep 3 '15 at 11:18
• @Peter good work! Now you can get the AI to play itself and end nuclear conflict. Sep 3 '15 at 14:27
• @Peter note that the class method needs to return the new instance for that to work; I've edited the example accordingly. Sep 4 '15 at 7:49


text:
Merge nested arrays coming from a database and a form

I need to update a nested array of settings. There are several difficulties that I managed to solve, but I'm pretty sure the code can be written much simpler. I just don't know how, which is why I'm asking for a simpler solution.

Problem:

• I have a nested array of settings in the database.
• I have a form with fields that need to update the database.
• The form contains checkboxes. When the form is sent, the empty checkbox fields don't get transmitted as keys in the form $input array. They are just missing. • I have settings in the database that don't get updated with the form and need to be preserved. It is not possible to just use array_merge(). It doesn't work recursively. And, it will not update a checkbox setting from true to false, because the form $input array doesn't contain the key.

Also we must preserve all settings that are not being updated with the form.

What I've done:

1. Create an array with all keys that are not part of the form.
2. Merge that array recursively with the form $input. (This ensures that existing non-form keys get preserved with the next merge function. But, we are still missing the key for the empty checkboxes.) 3. Created a function that iterates recursively through the input array and the database array. If keys on boths sides exist, the form input key overwrites the database key. If the form input key is missing, the database key is set to false. Here is my working code. It contains a few helpers. It tests if the final processed array is equal to the desired output or not. So this should make it easier to jump in and test quickly. $input_from_db = [
'first' => [
'one'   => '123123132', // form textfield textfield with text, change
'two'   => '',          // form, empty textfield, don't change
'three' => 0,           // form, checkbox, false, change to true
'four'  => 1,           // form, checkbox, true, change to false
'five'  => 1,           // form, checkbox, true, keep
'six'   => 0,           // form, checkbox, false, keep
'seven' => 'testdata'   // not in form, must be preserved
],
'second' => [               // test some deep nested arrays too
'one' => 0,             // change to true
'two' => 1,             // change to false with empty key
'three' => [
'a' => 'uno',
'b' => 'due'
]
],
'third'  => '2', // not in form, must be preserved
'fourth' => '3' // not in form, must be preserved
];

$input_from_form = [ 'first' => [ 'one' => 'abababab', 'two' => '', 'three' => 1, // four is missing from the form input because it was sent empty 'five' => 1, // six is missing from the form input because it was sent empty // seven is missing because it is no part of the form ], 'second' => [ //test some nested array too 'one' => 1, // form field, must come back with zero 'three' => [ // a is not in form, preserve 'b' => 'due_new', // c is an empty form checkbox ] ], 'third' => '2', // four not in form, must be preserved ];$must_be_result = [
'first' => [
'one'   => 'abababab', // changed in the form to 'abababab'
'two'   => '',
'three' => 1,
'four'  => 0, // because it IS in the form, but was unchecked, it must be present again, with value zero
'five'  => 1,
'six'   => 0,
'seven' => 'testdata' // not in form, must be preserved
],
'second' => [
'one' => 1,
'two' => 0,
'three' => [
'a' => 'uno',
'b' => 'due_new',
'c' => 0,
]
],
'third' => '2',
'fourth'  => '3'
];

// list of keys that the form will never return because they are not part of the form fields
function non_form_keys($array_existing): array { return [ 'first' => [ 'seven' =>$array_existing['first']['seven'],
],
'second' => [
'three' => [
'a' => 'uno'
]
],
'third' => $array_existing['third'], 'fourth' =>$array_existing['fourth'],
];
}

function update_options($array_existing,$array_input)
{
$output_array = []; foreach ($array_existing as $key =>$value)
{
if(array_key_exists($key,$array_input)){
if(is_array($value)){$output_array[$key] = update_options($value, $array_input[$key]);

} else {
$output_array[$key] = $array_input[$key];
}
} else {
if(is_array($value)){$output_array[$key] = set_to_zero($value);
} else {
$output_array[$key] = 0;
}
}
}

return $output_array; } function set_to_zero($array){

foreach ($array as$key => $value) { if(is_array($value)) {
$array[$key] = set_to_zero($value); } else {$array[$key] = 0; } } return$array;
}

// this is just a test that helps comparing my computed array with
// with an array that contains the result I want to see
function arrayRecursiveDiff($aArray1,$aArray2)
{
$aReturn = array(); foreach ($aArray1 as $mKey =>$mValue) {
if (array_key_exists($mKey,$aArray2)) {
if (is_array($mValue)) {$aRecursiveDiff = arrayRecursiveDiff($mValue,$aArray2[$mKey]); if (count($aRecursiveDiff)) {
$aReturn[$mKey] = $aRecursiveDiff; } } else { if ($mValue != $aArray2[$mKey]) {
$aReturn[$mKey] = $mValue; } } } else {$aReturn[$mKey] =$mValue;
}
}
return $aReturn; } // small helper to either print success or the // diff array function check_result($array_diff)
{
if(empty($array_diff)){ echo 'success' . PHP_EOL; } else { echo 'array diff' . PHP_EOL; print_r($array_diff);
}
}

$input_from_form_updated = array_replace_recursive(non_form_keys($input_from_db), $input_from_form);$result = update_options($input_from_db,$input_from_form_updated);

$array_diff = arrayRecursiveDiff($result, $must_be_result); check_result($array_diff);

• set_to_zero() is a "re-invention of the wheel". Just use the native array_walk_recursive() function to modify leafnodes.

array_walk_recursive(
$array, function(&$leafnode) {
$leafnode = 0; } );  Or from PHP7.4: array_walk_recursive($array, fn(&$leafnode) =>$leafnode = 0);

• To use the above in update_options(), I recommend using reference variables instead of declaring a new array to be returned.

• You should employ consistent method naming styles. I prefer camel-case over snake_case.

• empty($array_diff) can be safely trduced to !$array_diff because the variable is assured to be declared.

• Is there a way to compare two nested arrays with array_walk_recursive()?
– alev
Jan 13 at 4:37
• No, because it does not keep track of where it is while iterating leafnodes. Jan 13 at 8:10

format: plain


text:
# Thread: recursive to explicit equations

1. ## recursive to explicit equations

is there a way of turning recursive equations of the form $U_{n+1}=aU_{n}+b$ where $n_{1}=c$ into an explicit form with variables a, b and c?

2. $U_n=a^{n-1}c+b\sum_{i=0}^{n-2}a^i$, easy induction proof.

3. Hello, Rubberduckzilla!

I know one way . . . it's rather long . . .

Is there a way of turning recursive equations of the form . $U_{n+1}\:=\:aU_{n}+b$
where $U_{1}\:=\:c$ into an explicit form with variables $a, b\text{ and }c$ ?
The first term is: . $U_1 \:=\:c$ . . . The second term is: . $U_2 \:=\:ac+b$

$\begin{array}{ccccc}\text{We have the equation:} & U_{n+1} &=& aU_n + b & {\color{blue}[1]} \\
\text{The next equation is:} & U_{n+2} &=& aU_{n+1} + b & {\color{blue}[2]} \end{array}$

Subtract ${\color{blue}[2] - [1]}\!: \;\;U_{n+2} - U_{n+1} \;=\;aU_{n+1} - aU_n$

. . and we have: . $U_{n+2} - (a+1)U_{n+1} + aU_n$

Let: . $X^n \:=\:U_n\!:\quad X^{n+2} - (a+1)X^{n+1} +aX^n \;=\;0$

Divide by $X^n\!:\quad X^2 - (a+1)X + a \;=\;0$

. . which factors: . $(X - a)(X - 1) \;=\;0$

. . and has roots: . $X \;=\;a,\:1$

Hence, the function contains: . $a^n \text{ and }1^n$

The function is of the form: . $f(n) \;=\;A\!\cdot a^n + B$

We determine $A$ and $B$ by using the first two terms of the sequence.

$\begin{array}{ccccccc}f(1) \:=\:c\!: & Aa + B &=& c & {\color{blue}[3]}\\ f(2) \:=\:ac+b\!: & Aa^2 + B &=& ac + b & {\color{blue}[4]}\end{array}$

Subtract ${\color{blue}[4] - [3]}\!:\quad Aa^2 - Aa \:=\:ac + b - c$

. . $Aa(a-1) \:=\:ac+b - c \quad\Rightarrow\quad A \;=\;\frac{ac+b-c}{a(a-1)}$

Substitute into ${\color{blue}[3]}\!:\frac{ac+b-c}{a(a-1)}\cdot a + B\:=\:c \quad\Rightarrow\quad B \;=\;\frac{\text{-}b}{a-1}$

Therefore: . $f(n) \;=\;\frac{ac+b-c}{a(a-1)}\,a^n - \frac{b}{a-1}$


text:
## Sunday, May 28, 2017

### Access Windows Environment Variables from within Bash in WSL

I have been using my MacBook a lot now that it is my main computer at work. So much so that I found it necessary to invert my scroll wheel on my mouse on my windows desktop to behave like my MacBook. I've also been using bash a lot more since I can have similar experience between the 2 machines.

While in the process of writing the scripts to configure my bash environment on my Windows machine, I found the need to be able to access environment variables that are set in Windows. With WSL, the only environment variables that really come over to bash is PATH.

I googled around for a bit, but didn't find any way to actually do this. Then I remembered that WSL has interop between Windows and WSL. This means that I can execute a Windows executable and redirect the output back to bash. Which means I should be able to execute powershell.exe to get the information I need.

I first started with a test of just doing:

$echo$(powershell.exe -Command "gci ENV:")

And that gave me what I wanted back. Now there are some differences in the paths between WSL and Windows, so I knew I would also have to adjust for that.

What I did was put a file called ~/.env.ps1 in my home path.

#!~/bin/powershell
# Will return all the environment variables in KEY=VALUE format
function Get-EnvironmentVariables {
return (Get-ChildItem ENV: | foreach { "WIN_$(Get-LinuxSafeValue -Value ($_.Name -replace '$$|$$','').ToUpper())=$(Convert-ToWSLPath -Path$_.Value)" })
}

# converts the C:\foo\bar path to the WSL counter part of /mnt/c/foo/bar
function Convert-ToWSLPath {
param (
[Parameter(Mandatory=$true)]$Path
)
(Get-LinuxSafeValue -Value (($Path -split ';' | foreach { if ($_ -ne $null -and$_ -ne '' -and $_.Length -gt 0) { (( (Fix-Path -Path$_) -replace '(^[A-Za-z])\:(.*)', '/mnt/$1$2') -replace '\\','/')
}
} ) -join ':'));
}

function Fix-Path {
param (
[Parameter(Mandatory=$true)]$Path
)
if ( $Path -match '^[A-Z]\:' ) { return$Path.Substring(0,1).ToLower()+$Path.Substring(1); } else { return$Path
}
}

# Ouputs a string of exports that can be evaluated
function Import-EnvironmentVariables {
return (Get-EnvironmentVariables | foreach { "export $_;" }) | Out-String } # Just escapes special characters function Get-LinuxSafeValue { param ( [Parameter(Mandatory=$true)]
$Value ) process { return$Value -replace "(\s|'|"|\$|\#|&|!|~||\*|\?|$$|$$|\|)",'\$1';
}
}

Now in my .bashrc I have the following:

#!/usr/bin/env bash

source ~/.wsl_helper.bash
eval $(winenv) If I run env now, I get output like the following: WIN_ONEDRIVE=/mnt/d/users/rconr/onedrive PATH=~/bin:/foo:/usr/bin WIN_PATH=/mnt/c/windows:/mnt/c/windows/system32 Notice the environment variables that are prefixed with WIN_? These are environment variables directly from Windows. I can now add additional steps to my .bashrc using these variables. ln -s "$WIN_ONEDRIVE" ~/OneDrive

Additionally, I added a script to my ~/bin folder that is in my path called powershell. This will allow me to make "native" style calls to powershell from within bash scripts.
#!/usr/bin/env bash

# rename to powershell
# chmod +x powershell

. ~/.wsl_helper.bash

PS_WORKING_DIR=$(lxssdir) if [ -f "$1" ] && "$1" ~= ".ps1$"; then
powershell.exe  -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content$1))) ${*:2}" elif [ -f "$1" ] && "$1" ~!= "\.ps1$"; then
powershell.exe -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content$1))) ${*:2}" else powershell.exe -NoLogo -ExecutionPolicy ByPass${*:1}
fi
unset PS_WORKING_DIR

In the powershell file, you will see a call to source a file called .wsl_helper.bash. This script has some helper functions that will do things like transform a path from a Windows style path to a linux WSL path, and do the opposite as well.

#!/usr/bin/env bash
# This is the translated path to where the LXSS root directory is
export LXSS_ROOT=/mnt/c/Users/$USER/AppData/Local/lxss # translate to linux path from windows path function windir() { echo "$1" | sed -e 's|^$$[a-z]$$:$$.*$$|/mnt/\L\1\E\2|' -e 's|\\|/|g'
}

# translate the path back to windows path
function wsldir() {
echo "$1" | sed -e 's|^/mnt/$$[a-z]$$/$$.*$$|\U\1\:\\\E\2|' -e 's|/|\\|g' } # gets the lxss path from windows function lxssdir() { if [$# -eq 0 ]; then
if echo "$PWD" | grep "^/mnt/[a-zA-Z]/" > /dev/null 2>&1; then echo "$PWD";
else
echo "$LXSS_ROOT$PWD";
fi
else
echo "$LXSS_ROOT$1";
fi
}

function printwinenv() {
_winenv --get
}

# this will load the output exports of the windows envrionment variables
function winenv() {
_winenv --import
}

function _winenv() {
if [ $# -eq 0 ]; then CMD_VERB="Get" else while test$# -gt 0; do
case "$1" in -g|--get) CMD_VERB="Get" shift ;; -i|--import) CMD_VERB="Import" shift ;; *) CMD_VERB="Get" break ;; esac done fi CMD_DIR=$(wsldir "$LXSS_ROOT$HOME/\.env.ps1")
echo $(powershell.exe -Command "Import-Module -Name$CMD_DIR; $CMD_VERB-EnvironmentVariables") | sed -e 's|\r|\n|g' -e 's|^[\s\t]*||g'; } ## Wednesday, May 17, 2017 ### Jenkins + NPM Install + Git I have been working on setting up Jenkins Pipelines for some projects and had an issue that I think others have had, but I could not find a clear answer on the way to handle it. We have some NPM Packages that are pulled from a private git repo, and all of the accounts have MFA enabled, including the CI user account. This means that SSH authentication is mandatory for CI user. If there is only one host that you need to ssh auth with jenkins, or you use the exact same ssh key for all hosts, then you can just put the private key on your Jenkins server at ~/.ssh/id_rsa. If you need to specify a key dependant upon the host, which is the situation I was in, it was not working to pull the package. The solution for this that I found was to use the ~/.ssh/config. In there you specify the hosts, the user, and what identity file to use. It can look something like this: Host github.com User git IdentityFile ~/.ssh/github.key Host bitbucket.org User git IdentityFile ~/.ssh/bitbucket.key Host tfs.myonprem-domain.com User my-ci-user IdentityFile ~/.ssh/onprem-tfs.key  So now, when running npm install, ssh will know what identity file to use. Bonus tip: Not everyone uses ssh, so in the package.json, it may not be configured to use ssh. You can put options in the global .gitconfig on the Jenkins server that will redirect the https protocol requests to ssh: [url "ssh://git@github.com/"] insteadOf = "https://github.com/" [url "ssh://git@bitbucket.org/"] insteadOf = "https://bitbucket.org/" [url "ssh://tfs.myonprem-domain.com:22/"] instadOf = "https://tfs.myonprem-domain.com/  So with that, when git detects an https request, it will switch to use ssh. ### Access Windows Environment Variables from within Bash in WSL I have been using my MacBook a lot now that it is my main computer at work. So much so that I found it necessary to invert my scroll wheel on my mouse on my windows desktop to behave like my MacBook. I've also been using bash a lot more since I can have similar experience between the 2 machines. While in the process of writing the scripts to configure my bash environment on my Windows machine, I found the need to be able to access environment variables that are set in Windows. With WSL, the only environment variables that really come over to bash is PATH. I googled around for a bit, but didn't find any way to actually do this. Then I remembered that WSL has interop between Windows and WSL. This means that I can execute a Windows executable and redirect the output back to bash. Which means I should be able to execute powershell.exe to get the information I need. I first started with a test of just doing: $ echo $(powershell.exe -Command "gci ENV:") And that gave me what I wanted back. Now there are some differences in the paths between WSL and Windows, so I knew I would also have to adjust for that. What I did was put a file called ~/.env.ps1 in my home path. #!~/bin/powershell # Will return all the environment variables in KEY=VALUE format function Get-EnvironmentVariables { return (Get-ChildItem ENV: | foreach { "WIN_$(Get-LinuxSafeValue -Value ($_.Name -replace '$$|$$','').ToUpper())=$(Convert-ToWSLPath -Path $_.Value)" }) } # converts the C:\foo\bar path to the WSL counter part of /mnt/c/foo/bar function Convert-ToWSLPath { param ( [Parameter(Mandatory=$true)]
$Path ) (Get-LinuxSafeValue -Value (($Path -split ';' | foreach {
if ($_ -ne$null -and $_ -ne '' -and$_.Length -gt 0) {
(( (Fix-Path -Path $_) -replace '(^[A-Za-z])\:(.*)', '/mnt/$1$2') -replace '\\','/') } } ) -join ':')); } function Fix-Path { param ( [Parameter(Mandatory=$true)]
$Path ) if ($Path -match '^[A-Z]\:' ) {
return $Path.Substring(0,1).ToLower()+$Path.Substring(1);
} else {
return $Path } } # Ouputs a string of exports that can be evaluated function Import-EnvironmentVariables { return (Get-EnvironmentVariables | foreach { "export$_;" }) | Out-String
}

# Just escapes special characters
function Get-LinuxSafeValue {
param (
[Parameter(Mandatory=$true)]$Value
)
process {
return $Value -replace "(\s|'|"|\$|\#|&|!|~||\*|\?|$$|$$|\|)",'\$1'; } } Now in my .bashrc I have the following: #!/usr/bin/env bash source ~/.wsl_helper.bash eval$(winenv)

If I run env now, I get output like the following:

WIN_ONEDRIVE=/mnt/d/users/rconr/onedrive
PATH=~/bin:/foo:/usr/bin
WIN_PATH=/mnt/c/windows:/mnt/c/windows/system32

Notice the environment variables that are prefixed with WIN_? These are environment variables directly from Windows. I can now add additional steps to my .bashrc using these variables.

ln -s "$WIN_ONEDRIVE" ~/OneDrive Additionally, I added a script to my ~/bin folder that is in my path called powershell. This will allow me to make "native" style calls to powershell from within bash scripts. #!/usr/bin/env bash # rename to powershell # chmod +x powershell . ~/.wsl_helper.bash PS_WORKING_DIR=$(lxssdir)
if [ -f "$1" ] && "$1" ~= ".ps1$"; then powershell.exe -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1)))${*:2}"
elif [ -f "$1" ] && "$1" ~!= "\.ps1$"; then powershell.exe -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1)))${*:2}"
else
powershell.exe -NoLogo -ExecutionPolicy ByPass ${*:1} fi unset PS_WORKING_DIR In the powershell file, you will see a call to source a file called .wsl_helper.bash. This script has some helper functions that will do things like transform a path from a Windows style path to a linux WSL path, and do the opposite as well. #!/usr/bin/env bash # This is the translated path to where the LXSS root directory is export LXSS_ROOT=/mnt/c/Users/$USER/AppData/Local/lxss

# translate to linux path from windows path
function windir() {
echo "$1" | sed -e 's|^$$[a-z]$$:$$.*$$|/mnt/\L\1\E\2|' -e 's|\\|/|g' } # translate the path back to windows path function wsldir() { echo "$1" | sed -e 's|^/mnt/$$[a-z]$$/$$.*$$|\U\1\:\\\E\2|' -e 's|/|\\|g'
}

# gets the lxss path from windows
function lxssdir() {
if [ $# -eq 0 ]; then if echo "$PWD" | grep "^/mnt/[a-zA-Z]/" > /dev/null 2>&1; then
echo "$PWD"; else echo "$LXSS_ROOT$PWD"; fi else echo "$LXSS_ROOT$1"; fi } function printwinenv() { _winenv --get } # this will load the output exports of the windows envrionment variables function winenv() { _winenv --import } function _winenv() { if [$# -eq 0 ]; then
CMD_VERB="Get"
else
while test $# -gt 0; do case "$1" in
-g|--get)
CMD_VERB="Get"
shift
;;
-i|--import)
CMD_VERB="Import"
shift
;;
*)
CMD_VERB="Get"
break
;;
esac
done
fi
CMD_DIR=$(wsldir "$LXSS_ROOT$HOME/\.env.ps1") echo$(powershell.exe -Command "Import-Module -Name $CMD_DIR;$CMD_VERB-EnvironmentVariables") | sed -e 's|\r|\n|g' -e 's|^[\s\t]*||g';
}

### Jenkins + NPM Install + Git

I have been working on setting up Jenkins Pipelines for some projects and had an issue that I think others have had, but I could not find a clear answer on the way to handle it.

We have some NPM Packages that are pulled from a private git repo, and all of the accounts have MFA enabled, including the CI user account. This means that SSH authentication is mandatory for CI user.

If there is only one host that you need to ssh auth with jenkins, or you use the exact same ssh key for all hosts, then you can just put the private key on your Jenkins server at ~/.ssh/id_rsa. If you need to specify a key dependant upon the host, which is the situation I was in, it was not working to pull the package.

The solution for this that I found was to use the ~/.ssh/config. In there you specify the hosts, the user, and what identity file to use. It can look something like this:

Host github.com
User git
IdentityFile ~/.ssh/github.key

Host bitbucket.org
User git
IdentityFile ~/.ssh/bitbucket.key

Host tfs.myonprem-domain.com
User my-ci-user
IdentityFile ~/.ssh/onprem-tfs.key

So now, when running npm install, ssh will know what identity file to use.

Bonus tip: Not everyone uses ssh, so in the package.json, it may not be configured to use ssh. You can put options in the global .gitconfig on the Jenkins server that will redirect the https protocol requests to ssh:

[url "ssh://git@github.com/"]

So with that, when git detects an https request, it will switch to use ssh.


text:
This is a testing site only. See the live Public Lab site here »

# Nitrogen nitrates

Nitrogen or #nitrate contamination of drinking and surface water poses serious health risks, and can result from agricultural runoff in rivers. It is often associated with algal blooms as well. Also see the topic #phosphate. ## Nitrogen: Nitrates, Nitrite, Ammonia, & Ammonium (moved from the [Common Water Contaminants page](/common-water-contaminants)) Nitrates, Nitrite, Ammonia, & Ammonium are all "fixed" forms of nitrogen available to living organisms, and represent different stages of nitrogen in the [nitrogen cycle.](https://en.wikipedia.org/wiki/Nitrogen_cycle) Nitrogen is a major limiting nutrient in plant growth-- when nitrates occur in large quantities in water from fertilizers, manure, or sewage runoff, they can cause algal blooms that create dead zones. Nitrates have also been linked to increased risks of [cancer, and complications with a number of diseases, including asthma](http://www.ncbi.nlm.nih.gov/pmc/articles/PMC1310926/). The EPA limits drinking water concentrations of Nitrates to 10mg/L or lower, however, health threats can occur even at those levels. Ammonia (NH3) and ammonium ion (NH4+) are related chemically by an acid / base reaction: NH3 + H+ <=> NH4+ The equilibrium between ammonia and ammonium is pH dependent, with the two species being at the same concentration only under fairly alkaline conditions, pH = 9.25. At neutral pH, the concentration of free NH3 is less than 1% that of NH4+. Since this is the case, the concentration of ammonia is usually not significant and can be determined from the concentration of ammonium and the pH. **** ## Questions Questions can be either frequently asked questions, or "next step" challenges we're looking to solve. [questions:nitrogen] ## Activities [activities:nitrogen] ...

Author Comment Last activity Moderation
calitinshop "Cali Tins weed for sale | Buy Cali Tins Online https://calitinshop.com Space Monkey Meds Cali Weed for sale online calitinshop.com Cali Tins weed..." | Read more » almost 3 years ago
Engineerjaykumar " I am telling a advice about to use the Perfect construction technology and proper Admixtures in concrete , if well is constructed with concrete , ..." | Read more » almost 3 years ago
stevie "Also, the drinking water standard for nitrates is 10 milligrams per liter (10 mg/l) nitrate-N, or 45 mg/l nitrate-NO3. Most of the consumer tests f..." | Read more » about 3 years ago
stevie "I just looked into these a little further. All of them use sulphuric acid which can be dangerous. The brown ring test: https://byjus.com/chemistry..." | Read more » about 3 years ago
yp1080030 " different types of nitrate tests: the brown ring test, Devarda's test, diphenylamine test, and copper turning test CLICK HERE " | Read more » about 3 years ago
stevie "Hi! This is helpful thank you!! can you say more about the tests- how you might conduct it, the costs or the supplies you need for each one? " | Read more » about 3 years ago
healthydarkside "There are four different types of nitrate tests: the brown ring test, Devarda's test, diphenylamine test, and copper turning test. The brown ring t..." | Read more » about 3 years ago
Engineerjaykumar "There is so many test for this these are: Nitrate can be removed from drinking water by three methods: distillation, reverse osmosis, and ion excha..." | Read more » about 3 years ago
Bronwen "I think the quickest/cheapest nitrate testing kits may be found in your local pet store (or online retailer) in the aquarium section.. you can get ..." | Read more » about 3 years ago
ch_tavan " Thank you Lisdahl, I will try to find smaller flow cells that are easier to clean. I use a green LED and a photodiode BPW21 for the detector part..." | Read more » over 3 years ago
lizdahl " That cell is likely just too big for what you are trying to do and therefore hard to flush. I like using z-cells and tiny volumes since it is eas..." | Read more » over 3 years ago
ch_tavan " I use a flow cell that I bought. It is 0.48 ml in volume. I have problems filling the cell and cleaning. I think my cell is not suitable for my fe..." | Read more » over 3 years ago
lizdahl " The main cost for for a FIA or SIA would be a peristaltic pump and a flow cell - I've built my own a couple of times. You could make a flow cell ..." | Read more » over 3 years ago
cfastie "I have not worked with water chemistry sensors, but your first impression might be a good one -- a device that can send encoded concentrations of N..." | Read more » almost 6 years ago
jpkearns "Our systems are simpler, lower in cost, and made from common local materials. Please get in touch if you have questions - www.aqsolutions.org " | Read more » about 7 years ago
emontoya57 "Very inspiring nice work!!! " | Read more » about 7 years ago
eustatic "very cool. good question regaring the water pressure control, I've often wondered that myself. " | Read more » about 7 years ago
liz "FYI Public Lab, additional informal conversation on this is also happening on the WeChat app -- let us know if you want to join there " | Read more » about 7 years ago
chemcarr "This is very interesting. I am also working towards this kind of analysis, and especially open source accessible, whether fabrication or software/k..." | Read more » almost 8 years ago
Frikkie "Just noticed video1's name show nitrate. Its meant to be nitrite, sorry about that. " | Read more » almost 8 years ago
Phil "This is wicked Stuff! Thanks for sharing! " | Read more » almost 8 years ago
Frikkie "For anyone who wants to try out the excel tool, here is the video’s for the recordings I made for the colorimetric nitrite analysis, the EC and fla..." | Read more » almost 8 years ago


text:
# Right angle triangle and angle bisector

Let $\triangle ABC$ be a right-angled triangle with $\angle B = 90^\circ$. Let $\overline{AD}$ be the bisector of $\angle A$ with $D$ on $\overline{BC}$.

Let the circumcircle of $\triangle ACD$ intersect $\overline{AB}$ again in $E$, and let the circumcircle of $\triangle ABD$ intersect $\overline{AC}$ again in $F$.

Let $K$ be the reflection of $E$ with respect to the line $\overline{BC}$.

Prove that $|\overline{FK}| = |\overline{BC}|$.

Let $AB=b$ and $BC=a$.
Thus, since $AD$ is a bisector of $\Delta ABC$, we obtain: $$\frac{BD}{DC}=\frac{AB}{AC}$$ or $$\frac{BD}{a-BD}=\frac{b}{\sqrt{a^2+b^2}}$$ or $$BD=\frac{ab}{b+\sqrt{a^2+b^2}}.$$ Now, $$BE\cdot AB=BD\cdot BC$$ or $$BE\cdot b=\frac{a^2b}{b+\sqrt{a^2+b^2}}$$ or $$BE=\frac{a^2}{b+\sqrt{a^2+b^2}}$$ or $$BE=\sqrt{a^2+b^2}-b,$$ which says that $BE=FC$ and from here $BK=FC$.
Thus, since $BD=DF$ and $$\measuredangle KBD=\measuredangle CFD=90^{\circ},$$ we obtain $$\Delta BDK\cong\Delta FDC,$$ which says $D\in KF$ and $BC=KF$.


text:
## Introduction

Waves in random media often exhibit spatial patterns of branched flow1. Spatial focusing and branching of waves in random media have been reported for electron waves2, microwaves3, tsunami waves4,5, light6,7, and ocean hydroacoustics8. The statistics of the spatio-temporal features of propagation, as induced by the randomness of the medium, have been of great current interest in numerous theoretical investigations9,10,11. The phenomenon has been studied for non-linear waves too12. While branching flow of waves has been reported in seemingly unrelated fields, the underlying governing equation in most cases is the classical non-dispersive wave equation $${\partial }_{{tt}}\eta ={c}^{2}{\nabla }^{2}\eta$$, where $$\eta (x,y,t)$$ is the field variable that depends on the position $$x,y$$ and time $$t$$; $$c$$ is the wave speed, which is independent of the wavelength. By contrast, bending waves in elastic plates are dispersive, as shorter waves travel faster. Such structures are frequently encountered in the physics of geological plates, glaciology of ice sheets, thin films, nano-mechanics of 2D materials such as graphene, aerospace skin structures, etc. Waves in thin elastic plates are governed by the bi-harmonic Kirchhoff–Love equation13

$${\partial }_{{tt}}\eta =-{\alpha }^{2}{\nabla }^{4}\eta .$$
(1)

Here, $$\eta$$ is the transverse displacement, $${\alpha }^{2}=E{H}^{2}/12\rho (1-{\nu }^{2})$$ combines the thickness of the plate $$H$$, Young’s modulus $$E$$, Poisson’s ratio $$\nu$$ and the material density $$\rho$$. The group velocity $${c}_{g}$$ is proportional to the wave number $$k$$, i.e. $${c}_{g} \sim \alpha k$$, as evident from the fourth spatial derivative.

Consider plates of thickness much smaller than the wavelength of interest $$\lambda$$, i.e. $$H\ll \lambda$$, so that higher order effects such as shear through the thickness14 can be ignored. Likewise, rotary inertia15,16,17 and kinematic non-linearity due to large rotations associated with the so called von Karman strains18 are neglected. We report the existence of branched flows of flexural waves in such plates when their thickness has modest spatially correlated randomness.

Branched flows lead to regions of localised high amplitude called caustics or focusing events. In this work, we demonstrate the existence of an elegant scaling between the location of the first caustic with a suitably non-dimensionalised measure of the variance in the thickness of the elastic plate, and with the spatial correlation length of the randomness of the thickness. We do so using analytical approaches and two different numerical methods. The scaling we report here is consistent with similar scaling observed in other different physical contexts1.

## Results and discussion

Consider plates with small random non-uniformity in thickness $$H(x,y)$$, described by the variable $$h$$ (see Fig. 1) that denotes small fluctuations over the mean thickness $${H}_{0}$$, i.e. $$H(x,y)={H}_{0}(1-h(x,y))$$; the mean of $$h$$ is zero. While the results are developed here for a plate with non uniform thickness, the results can readily be generalised for plates with other non-uniform properties as long as the non-uniformity is small.

A pulse with carrier frequency $$\omega$$ that is modulated by a Gaussian envelope is launched at the left edge (see Methods section). The excitation thus provided resembles the motion of a rug transversely shaken at the left end. A plane wavefront with a predominant wavelength $$\lambda \approx 2\pi \sqrt{\alpha }{\omega }^{-1/2}$$ thus propagates from left to right.

Figure 2 shows the spatio-temporal evolution, obtained using finite element elastodynamics simulations. A plane wave front is launched at the left end of a plate of non-uniform thickness. The four panels on the left show the amplitude of transverse displacement at four successive instants of time (top to bottom; $$x$$-$$y$$ axes are normalised, see caption). Regions of highest displacement at each successive time instant are indicated by square boxes labelled A, B, C and D. Widening of the wavefront due to the dispersive nature of the medium is apparent. This can also be seen from the energy density plots (shown in light red above each panel in Fig. 2). We observe the emergence of distinct branches as the wave propagates. Regions of high amplitude at each instant of time shown are zoomed into, and reproduced as 3D plots on the right (with labels A, B, C and D, indicating the correspondence with the four figures on the left).

In a homogeneous dispersive medium of average properties, one would observe a monotonic reduction in the highest amplitudes of the wave front as it propagates (see Fig. S1, supplementary information). This follows from energy conservation since the widening of the wavefront must accompany reduction in amplitude. The maximum amplitude seen at the same time instant in a homogeneous plate is shown in the plots on the right with transparent planes. The levels of these planes decrease monotonically with time, as anticipated. However, the highest amplitudes do not decrease for the non-uniform plate. This is due to the emergence of branches that is attributed to scattering of the plane wave by the non-uniformity. These branches interfere and form locations of amplitudes much higher than what one may expect from the dispersive wave dynamics in an equivalent homogeneous medium. Regions with amplitudes that are lower than those for the homogeneous case also appear, as expected from energy conservation. See Supplementary Movie 1 for an animation comparing flexural wave transmission in uniform and non-uniform plates.

### Scaling of the location of the first caustic with randomness

We are now interested in the distance $${l}_{{{{{{\rm{f}}}}}}}$$ of the first high amplitude peak from the left edge, as this quantitatively characterises the emergence of the branching flow of waves in random elastic plates. If the correlation length of randomness field $$h$$ is $${L}_{c}$$, then the three length scales in the problem have the relationship $${H}_{0}\ll \lambda \ll {L}_{c}$$. The first part of the inequality enables us to ignore shear effects through the thickness, whereas the latter justifies the use of the eikonal equation19, which reduces the plate flexure wave dynamics to one of ray dynamics. Then, using arguments from stochastic dynamics (see Methods section), one obtains

$${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}}\propto {L}_{{{{{{\rm{c}}}}}}}{{\langle }}{h}^{2}{{{\rangle}}}^{-1/3},$$
(2)

where the angular brackets denote mean of the respective quantities; $${L}_{{{{{{\rm{c}}}}}}}$$ is the correlation length of non-uniformity; $${l}_{f}$$ is the distance of the focusing point from the left end. Rays form envelopes known as caustics. The first focusing point in the branched structures of caustics will be referred to as the first caustic. In the ray limit, the only length scale in the problem is the correlation length. Hence, the linear scaling $${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}} \sim {L}_{{{{{{\rm{c}}}}}}}$$ follows immediately from dimensional arguments.

### Scaling law from numerical integration of ray equations

The scalings $${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}} \sim {L}_{{{{{{\rm{c}}}}}}}$$ and $${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}} \sim {{\langle }}{h}^{2}{{{\rangle}}}^{-1/3}$$ can be obtained numerically by integrating ray equations using a forward Euler method. As rays propagate, those that were initially parallel to the propagation direction begin to scatter due to inhomogeneities in the thickness of the plate. Some rays eventually cross over causing focusing events as seen in Fig. 3a. These focusing points can be numerically detected as the location where the curve of position vs wavenumber in the $$y$$-direction becomes multi-valued20. In the instance shown in Fig. 3, the location of the first such focusing point has been indicated with a circular marker. By running multiple instances of these ray simulations for different values of $${{\langle }}{h}^{2}{{\rangle}}$$, we obtain the dependence of $${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}}$$ on $${{\langle }}{h}^{2}{{\rangle}}$$ which, is in excellent agreement with Eq. 2 (see, Fig. 3).

The linear scaling $${{\langle }}{l}_{f}{{\rangle}} \sim {L}_{{{{{{\rm{c}}}}}}}$$ is also observed in the ray simulations. In Fig. 3b, taking the $${L}_{{{{{{\rm{c}}}}}}}=0.1$$ m simulations as a reference, we denote the trend predicted from Eq. 2 using dotted lines. The ray simulations for $${L}_{{{{{{\rm{c}}}}}}}=0.07$$ m and $${L}_{{{{{{\rm{c}}}}}}}=0.15$$ m are in excellent agreement with the predicted trend. Ray simulations also confirm that $${{\langle }}{l}_{f}{{\rangle}}$$ is independent of wavelength (Fig. S2, supplementary information). The expected location of the first caustic remains largely unchanged even with a 100% change in the wavelength. Nominal thickness $${H}_{0}=1$$ mm is used for all finite element as well as ray dynamics simulations.

### Scaling law from elastodynamic simulations

Let us return to the full wave dynamics using finite element simulations, as the true dispersive character of the flexural waves propagating in elastic plates is best captured there. To detect focusing events, we use the time integrated intensity defined as $$I(x,y)={\int }_{0}^{T}{\eta }^{2}(x,y,t){{{{{\rm{d}}}}}}t$$ where $$T$$ is, approximately, the time when the wavefront reaches the right edge. By definition, the local peaks in $$I(x,y)$$ correspond to focusing events. In Fig. 4b, a typical instance of $$I(x,y)$$ is shown. While plotting, the time integrated intensity is averaged along the $$y$$ axis. This makes the local peaks in intensity visually pronounced. At each point on the $$x$$-axis one can calculate the scintillation index, $$\sigma (x)=({{\langle }}{I}^{2}{{{\rangle}}}_{y}/{{\langle }}I{{{\rangle}}}_{y}^{2})-1$$ (see Fig. 4a). The first prominent peak in $$\sigma (x)$$ provides the location of the first caustic. By varying $${{\langle }}{h}^{2}{{\rangle}}$$, $${L}_{{{{{{\rm{c}}}}}}}$$, and $$\lambda$$, we can, as in the case of ray integration, bring out the effects of the variation of the severity of randomness in the thickness, correlation length and wavelength. Figures 4c, d show that the finite element simulations also largely agree with the trend predicted by our analysis. The independence of $${{\langle }}{l}_{f}{{\rangle}}$$ with wavelength is also confirmed via our finite element simulations (Fig. S3, supplementary information). There is some deviation from predicted scaling relationships at higher values of $${{\langle }}{h}^{2}{{\rangle}}$$, but this is expected, as the weak scattering assumption in the analysis begins to break down at higher value of $${{\langle }}{h}^{2}{{\rangle}}$$. In finite element simulations (Fig. S3, supplementary material) we see that even at lower values of $${{\langle }}{h}^{2}{{\rangle}}$$, the deviation for the shortest waves considered ($$\widetilde{\lambda }=0.07$$) seems to be slightly more pronounced than for longer wavelengths. This may point to the minor role of through-thickness shear which is expected to get more severe as the wavelength decreases. Similar deviation is not seen in ray dynamics simulations (Fig. S2, supplementary material), which is consistent with the formulation of ray equations that ignores through thickness shear.

## Conclusions

The emergence of focusing of bending waves in random thin elastic plates was demonstrated and quantitatively characterised. The scaling of the location of the first focusing event with the mean square randomness of the plate is determined when a pulse is launched from one end of plate. The ray description of the problem, arising from the eikonal equation for plate flexural waves, was reduced to a system of equations consistent with previously known scalings in other physical contexts, viz. $${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}} \sim {L}_{{{{{{\rm{c}}}}}}}$$ and $${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}} \sim {{\langle }}{h}^{2}{{{\rangle}}}^{-1/3}$$. The ray equations were then integrated numerically and found to agree with these scaling relationships. The location of the first caustic was found to be independent of the wavelength and the correlation function that describes the disorder (Methods section). The scaling relationships between the location of focusing and disorder, the correlation length, were also obtained by elastodynamic simulations using the finite element method.

This work focused on thin plates where thickness shear effects are insignificant. The existence, or absence, of branched flows in thick plates where the wavelength is comparable to the thickness and, therefore, thickness shear is significant, remains an open question.

The formation of channels suggests the potential of tailoring and shaping wave paths within such elastic waveguides, especially when combined with acoustic metamaterials21. Since particle separation and manipulation22 involve steering particles to nodal lines, we anticipate that this work may potentially inform the design of such technologies. While the analysis and computations presented here are restricted to thickness variations, the phenomenon reported here would, in principle, be observed for spatial heterogeneity in material properties such as the modulus of elasticity or density. Results presented here could also have interesting implications to metrology of elastic plates, geophysics and seismology.

## Methods

### Analytical proof of scaling from ray dynamics

The equation of motion for flexural waves in a thin elastic plate with spatially non-uniform flexural stiffness is given by19

$$\rho H{\partial }_{{tt}}\eta +{\partial }_{{xx}}{M}_{x}+{\partial }_{{yy}}{M}_{y}+2{\partial }_{{xy}}{M}_{{xy}}=0,$$
(3)

where, $${M}_{x}=D({\partial }_{{xx}}\eta +\nu {\partial }_{{yy}}\eta )$$, and $${M}_{y}=D(\nu {\partial }_{{xx}}\eta +{\partial }_{{yy}}\eta )$$, are bending moments per unit edge length and $${M}_{{xy}}=(1-\nu )D{\partial }_{{xy}}\eta$$ is the twisting moment per unit edge length; $$\rho$$ is the density of the material, $$H(x,y)$$ is the thickness of the plate, $$E$$ is the Young’s modulus, $$\nu$$ is the Poisson ratio, $$D=\frac{E{H}^{3}}{12(1-{\nu }^{2})}$$ is the bending rigidity of the plate. The above governing equation is valid when $$D,\,\rho ,\,E,\,\nu$$ and $$H$$ are slowly varying functions of $$x,y$$. We can write the eikonal approximation of a wave in an inhomogenous thin plates as follows19:

$${\partial }_{\tau }{{{{{\bf{x}}}}}}={(D/\rho H)}^{1/2}{{{{{\bf{k}}}}}}/\omega ,$$
(4)
$${\partial }_{\tau }{{{{{\bf{k}}}}}}=-\frac{1}{4}(\rho H/D)\nabla (D/\rho H),$$
(5)

where, $$\omega$$ is the angular frequency associated with the wavelength of interest, $$\tau =\omega t$$ and $${{{{{\bf{k}}}}}}={[{k}_{x}{k}_{y}]}^{T}$$ is the two dimensional wave vector. In order to demonstrate the scaling relationship associated with branching flow, we consider a plate with spatially uniform density, Poisson’s ratio and Young’s modulus but slowly changing thickness. Thickness can be expressed as $$H(x,y)={H}_{0}(1-h(x,y))$$ where $${H}_{0}$$ is the nominal thickness and $${{{{{\rm{|}}}}}}h(x,y){{{{{\rm{|}}}}}}\ll 1$$. Then we can introduce the constant $${\gamma }^{2}=\frac{E{H}_{0}^{2}}{12(1-{\nu }^{2})\rho }$$ to rewrite $$D/\rho H={\gamma}^{2}(1-h(x,y))^{2}$$. Assume that $$h(x,y)$$ is a random field with zero mean and isotropic spatial correlation characterised by correlation length $${L}_{{{{{{\rm{c}}}}}}}\gg \lambda$$. Then eikonal equations can be rewritten as:

$${\partial }_{t}{{{{{\bf{x}}}}}}=\gamma (1-h){{{{{\bf{k}}}}}}$$
(6)
$${\partial }_{t}{{{{{\bf{k}}}}}}=\frac{\omega }{2}\frac{\nabla h}{(1-h)}.$$
(7)

Barring some constants, the structure of these equations is identical, for monochromatic waves, to those in4,5. Following the arguments presented therein we can arrive at the scaling relationship

$${{\langle }}{l}_{{{{{{\rm{f}}}}}}}{{\rangle}}\propto {L}_{{{{{{\rm{c}}}}}}}{{\langle }}{h}^{2}{{{\rangle}}}^{-1/3}.$$
(8)

### Finite element simulation of elastodynamics

To numerically explore branching flows in elastic plates with randomly varying properties, we consider a thin rectangular plate of non-uniform thickness and dimensions $${L}_{x}\times {L}_{y}$$ modelled using shell finite elements in within ANSYS, a commercial finite element code. The main propagation direction is along $$x$$-axis. The length in this direction is $${L}_{x}(\gg {L}_{y})$$. The thickness of the plate is given by $${H}_{0}(1-h(x,y))$$ where $$h(x,y)$$ is a random field with zero mean and isotropic spatial correlation length $${L}_{{{{{{\rm{c}}}}}}}$$. A MATLAB code was used to generate multiple instances of the random field with desired standard deviation and correlation length. Simulations were carried out within the ANSYS APDL environment using SHELL181 finite elements.

SHELL181 finite elements have both membrane and flexural strain energy taken into account. The degrees-of-freedom for each node are 3 displacements and 3 rotations. Rotational degrees-of-freedom are required in plate/shell finite elements, unlike many 3D elasticity formulations because of the intrinsic simplification through the thickness within the theory of plates and shells. During the simulations, the two in-plane degrees-of-freedom were locked and so was the rotation about the axis normal to the plate. Thus the remaining active degrees-of-freedom for each finite element node are the transverse displacement and two rotations. The element allows specification of different thickness values at each of the nodes and also interpolates thickness within an element. This is crucial as we have a spatially varying flexural rigidity field $$D(x,y)$$. The approach uses variational minimisation of the integral of the Lagrangian of the elastic continuum $$L=T-U$$, where $$T$$ is the kinetic energy and $$U$$ the potential energy. Local basis functions, known as shape functions are used to describe the elastic field in conjunction with unknowns that are treated as the generalised coordinates of the problem. Setting $$\delta {{{{{\rm{\int }}}}}}L{{{{{\rm{d}}}}}}t=0$$ within two arbitrary instances of time, where $$\delta$$ is “the first variation of”23 leads to a set of coupled ordinary differential equations, the spatial variable having been eliminated in the process. These coupled ordinary differential equations were solved using a Newmark-$$\beta$$ scheme24. The details are omitted as the implementation is a part of the commercial code used. Although the element is capable of simulating dynamics of surface structures with intrinsic curvature, such as those in elastic shells, we have simulated a structure with a plate-like flat equilibrium shape.

The right edge is fixed and the two long edges running $$x$$-wise are free. The left edge is excited transversely with a Gaussian pulse of dominant frequency $$\omega$$ to give rise to a pulse composed predominantly of wavelength $$\lambda$$. Using the dispersion relation of a homogeneous plate whose thickness is the same as nominal thickness of the plate under consideration we can obtain $$\omega =\gamma {(2\pi /\lambda )}^{2}$$. The temporal profile of the excitation is shown in Fig. 5.

Simulations were carried out on the normalised domain $$(\widetilde{x},\widetilde{y})\in [0,\,40]\,\times\, [0,\,8]$$ except for the lowest three standard deviation which were done over $$(\widetilde{x},\widetilde{y})\in [0,\,80]\,\times\, [0,\,8]$$. Note that even for the lowest amount of randomness studied $${{\langle }}{\widetilde{l}}_{{{{{{\rm{f}}}}}}}{{\rangle}} \, < \, 40$$, but it was ensured for all simulations the propagation distance was at least twice as much as the expectation value of the first focusing event to avoid biasing the sample. This means that for the highest 7 standard deviations studied, a field of normalised length $$40$$ was sufficient but for that latter a field of normalised length $$80$$ was used. This was done due to the computational load associated with running these simulation. It is apparent that this has not had any significant bias on the sample since there is no discontinuity in the trend of the expectation value of the first focusing events. This can also be seen by looking at the spread of data in the main text of the paper. More details about simulations can be found in Supplementary note 1, Supplementary information.

Energy densities shown in Figs. 2 and S1 are obtained from the finite element elastodynamic simulations by taking temporal and spatial derivatives, numerically, to find kinetic and potential energies at each time instant shown.

### Other correlation functions

A Gaussian correlation function was used to construct the thickness fluctuation field $$h(x,y)$$ for all the results reported in the main text. We also used a power law correlation function of the form $$A(x,y)=(1+({x}^{2}+{y}^{2})/{L}_{{{{{{\rm{c}}}}}}}^{2})^{-2}$$. The scaling remains unaffected (Fig. 6). This is consistent with literature on branching phenomenon in other physical contexts that the scaling of $${{\langle }}{l}_{f}{{\rangle}}$$ remains unchanged if other correlation functions are used4. The only requirement is that the integral of this function over $${{\mathbb{R}}}^{2}$$ be well defined.


text:
How is it possible that an opinion poll claims to know what the whole country thinks just by asking a few hundred people?

Ask a hundred people if they like cats and around 63% will say yes, give or take a few1. If you want to be more accurate, you can even say that the number will be between 53 and 73. What if you asked a thousand people? Perhaps you’ll find 530 to 730 ailurophiles this time?

Wisdom is the daughter of experience “, according to Leonardo da Vinci, and experiencing more responses to our cat question means we can actually make a wiser guess: with a sample size of one thousand people you should find that 600 to 660 of them like cats.

### How this works

First, some terminology. In the first example above, we guessed that 53 to 73 out of 100 people like cats. As a percentage, this is 53% - 73%, or you could also write this as 63% ± 10%. Here, 63% is our estimate and 10% is our margin of error.

Second, a formula. For a sample of size N, the margin of error (as a percentage) is $\frac{98}{\sqrt{N}}$

When we ask 1,000 people, our margin of error is $\frac{98}{\sqrt{1000}}$ = 3%. Combining this with our estimate we get 63% ± 3%, or 600 to 660 people answering yes.

How many people would you need for a 1% margin of error? Working backwards, $N = (\frac{98}{ME})^2$, so a 1% error margin gives $N = (\frac{98}{1})^2$ = 9,604 people.

### This should work most of the time

Let’s imagine you roam the country asking strangers on Tinder if they like cats (statistically, it’s an excellent opening line). Every day you ask 10 strangers, and can expect between 3 and 9 to say yes, but the exact number will change day to day. One day you might get 7, the next day 4, another you might find that all ten people say they like cats.

The formula above calculates a margin of error that’s correct 19 times out of 20, or 95% of the time. This number, also called the 95% confidence level, is baked into the formula so that roughly one day in twenty you’ll find below 3 or above 9 ailurophiles in your Tinder survey.

Perhaps a one in twenty chance of being wrong is too uncertain, but one in a hundred might be acceptable? This means using a 99% confidence level, and the formula becomes $\frac{129}{\sqrt{N}}$ instead. Using this new formula, you’ll find that if you want to conduct your Tinder poll at a 99% confidence level, you can now expect to find between 2 and 10 cat lovers each day.

### When it doesn’t work so well

Some towns are dog towns, some towns are cat towns2. Sometimes people will give the answer they think you want to hear, sometimes people later change their minds and decide they prefer dinosaurs as pets. These are all examples of sampling bias, which we’ll cover in a future article.

If you want to try this at home and estimate what percentage of people in your area like cats, you can use the margin of error to do this. If you ask 100 people and 80 say yes, then your estimate is 80% with a 10% margin of error (at a 95% confidence level). In other words, there’s a 19 out of 20 chance that the actual percentage is somewhere between 70% to 90%.

There will always be a margin of error in any election poll. This is fine, we can’t poll the whole country, but do remember that the headline figure is not completely accurate. A large sample size will give a smaller margin of error. This can however be gamed by using a lower confidence level, so for reliable results check for both a large sample size and a high confidence level.

1. 63% of US adults like cats either “a little” or “a lot”. Poll conducted Oct 2009 by GfK Roper Public Affairs & Media, involving telephone interviews with 1,967 US adults and has a margin of error of ±2.2%.

2. In Vermont, 50% of households own a cat, compared with only 25% of households in Utah. Alabama is definitely more of a dog state (44% of households) than a cat state (27% of households).


text:
# Search for new physics in events with two soft oppositely charged leptons and missing transverse momentum in proton-proton collisions at $\sqrt s$ = 13 TeV

## Abstract

A search is presented for new physics in events with two low-momentum, oppositely charged leptons (electrons or muons) and missing transverse momentum in proton-proton collisions at a centre-of-mass energy of 13 TeV. The data collected using the CMS detector at the LHC correspond to an integrated luminosity of 35.9. The observed event yields are consistent with the expectations from the standard model. The results are interpreted in terms of pair production of charginos and neutralinos ( and ) with nearly degenerate masses, as expected in natural supersymmetry models with light higgsinos, as well as in terms of the pair production of top squarks ( ), when the lightest neutralino and the top squark have similar masses. At 95% confidence level, wino-like masses are excluded up to 230 GeV for a mass difference of 20 GeV relative to the lightest neutralino. In the higgsino-like model, masses are excluded up to 168 GeV for the same mass difference. For pair production, top squark masses up to 450 GeV are excluded for a mass difference of 40 GeV relative to the lightest neutralino.

## Abstract

A search is presented for new physics in events with two low-momentum, oppositely charged leptons (electrons or muons) and missing transverse momentum in proton-proton collisions at a centre-of-mass energy of 13 TeV. The data collected using the CMS detector at the LHC correspond to an integrated luminosity of 35.9. The observed event yields are consistent with the expectations from the standard model. The results are interpreted in terms of pair production of charginos and neutralinos ( and ) with nearly degenerate masses, as expected in natural supersymmetry models with light higgsinos, as well as in terms of the pair production of top squarks ( ), when the lightest neutralino and the top squark have similar masses. At 95% confidence level, wino-like masses are excluded up to 230 GeV for a mass difference of 20 GeV relative to the lightest neutralino. In the higgsino-like model, masses are excluded up to 168 GeV for the same mass difference. For pair production, top squark masses up to 450 GeV are excluded for a mass difference of 40 GeV relative to the lightest neutralino.

## Statistics

### Citations

Dimensions.ai Metrics
50 citations in Web of Science®
64 citations in Scopus®

### Altmetrics

11 downloads since deposited on 26 Feb 2019
Detailed statistics